Skip to content

Testing in CI

Ralf Steinert edited this page Nov 10, 2026 · 6 revisions

Two ways to get simulated instruments into automated tests: the pytest plugin (scpi-sim installed in the test environment) or the Docker image (scpi-sim as a separate service).

pytest plugin

pip install scpi-sim registers the plugin. The scpi_bench fixture starts a fresh bench for every test, on free ports, and stops it afterwards — tests cannot leak instrument state into each other and can run in parallel.

import pyvisa


def test_rail_voltage(scpi_bench):
    rm = pyvisa.ResourceManager("@py")
    opts = dict(read_termination="\n", write_termination="\n", timeout=2000)
    psu = rm.open_resource(scpi_bench.resource("psu"), **opts)
    daq = rm.open_resource(scpi_bench.resource("daq"), **opts)
    psu.write("INST OUT1; VOLT 12; OUTP:SEL ON;:OUTP:GEN ON")
    assert abs(float(daq.query("MEAS:VOLT:DC? (@101)")) - 12) < 0.01
    rm.close()
pytest --scpi-bench examples/profiles/bench.yaml
  • Which bench: --scpi-bench FILE, or scpi_bench = FILE in pytest.ini / [tool.pytest.ini_options]; without either a generic psu, dmm, mux.
  • scpi_bench.resource(name) → TCPIP0::127.0.0.1::<port>::SOCKET; scpi_bench.resource(name, "hislip") and "vxi11" give the LAN protocol strings — the fixture serves both (without port mapper).
  • scpi_bench.<name> is the instrument object: inject faults (scpi_bench.daq.inject("offset", value=0.5)) or set inputs directly.
  • scpi_bench_factory(path_or_dict) starts more benches in one test.

Your driver instead of raw PyVISA

Most projects wrap instruments in a driver class that takes a resource string. Point it at the fixture:

from mylab.drivers import Hmp4040   # your code


def test_driver_sets_and_reads_back(scpi_bench):
    psu = Hmp4040(scpi_bench.resource("psu"))
    psu.set_output(channel=1, volts=12, amps=1)
    assert psu.current(channel=1) == 0.5

The driver does not know it is talking to a simulator.

Docker image as a service container

When the tests should not import scpi-sim — other languages, a test system that only takes resource strings, or an end-to-end pipeline — run the image as a service. GitHub Actions:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      instruments:
        image: ghcr.io/steitec/scpi-sim:0.7.4
        ports:
          - 5025:5025     # psu
          - 5026:5026     # dmm
          - 5027:5027     # mux
          - 4880:4880     # HiSLIP
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: pip install -r requirements.txt
      - run: pytest tests/hardware
        env:
          PSU_RESOURCE: TCPIP0::localhost::5025::SOCKET
          DMM_RESOURCE: TCPIP0::localhost::hislip_dmm::INSTR

GitLab CI works the same way with services: and the alias as host name.

Your own bench file goes into the container with a volume, or build a small image on top:

FROM ghcr.io/steitec/scpi-sim:0.7.4
COPY bench.yaml /bench/bench.yaml

The image runs scpi-sim serve --hislip --vxi11 --vxi11-port 5098 --bench /bench/bench.yaml --host 0.0.0.0 and exposes 5025–5028, 5099 (control port for faults), 4880 (HiSLIP), 5098 and 111 (VXI-11).

Pin the version (:0.7.4, not :latest) so a new scpi-sim release does not change your pipeline unannounced.

Faults in a pipeline

With the service container, faults come through the control port (publish 5099 for this, only inside the CI network — it has no authentication):

pip install scpi-sim       # only for the command line client
scpi-sim fault dmm no_response count=1 --control-port 5099
scpi-sim fault --list

Patterns for what to test: Fault injection recipes.

Tips

  • Timeouts: keep them short in tests (timeout=2000). The simulator answers in well under a millisecond; a long timeout only slows down the tests that check timeouts.
  • Noise: generic multimeters add a little noise by default. Compare with a tolerance, or set noise: 0 (and seed: for reproducible noise) in the bench file.
  • Several instruments: every instrument has its own connection. Finish a switching command with *OPC? before measuring with another instrument, as on a real bench: ROUT:CLOS (@101);*OPC?.

Clone this wiki locally