Plesty Documentation

Testing Devices Without Hardware

The eight DevicePipeline mock gates gate d1 enforces — generated by plesty init mock-test, or written by hand.

Gate d1 verifies that your device implements the full PLESTY device API contract. It runs entirely with a mock transport — no real hardware needed.

Note: Gate d1 is activated only when module_type = "device" is set in [tool.plesty] of pyproject.toml. Experiment and analyzer modules skip it automatically.

Complete test file

# tests/test_pipeline.py
import pytest
from plesty.lib.test.device_pipeline import DevicePipeline
from plesty.power_meter import PowermeterDevice

PIPELINE = DevicePipeline(
    PowermeterDevice,
    "mock-address",           # positional arg forwarded to __init__
    sensor_type="S155C",      # keyword arg forwarded to __init__
)


def test_schema_integrity():
    PIPELINE.test_schema_integrity()


def test_param_key_resolution():
    PIPELINE.test_param_key_resolution()


def test_params_mock():
    PIPELINE.test_params_mock()


def test_funcs_mock():
    PIPELINE.test_funcs_mock()


def test_lifecycle():
    PIPELINE.test_lifecycle()


def test_identity():
    PIPELINE.test_identity()


def test_check_errors():
    PIPELINE.test_check_errors()


def test_state_coverage():
    PIPELINE.test_state_coverage()

You do not have to type this. plesty init mock-test writes it for you: it probes the module for a construction that runs without hardware, verifies that construction against the gates it will have to pass, and writes the test file. Run it inside an existing device project — unlike device or experiment, it adds to a module rather than creating one.

If you would rather have one test than eight, the pipeline can run them all itself — which is the shape the generator produces:

PIPELINE = DevicePipeline(Device, address="mock")


def test_mock_pipeline() -> None:
    PIPELINE.run_mock_pipeline()

Both satisfy gate d1. Eight named tests tell you which gate failed from the test name alone; the aggregate is shorter.

The eight mock gates

Gate Function What it checks
1 test_schema_integrity What the running device declares is coherent: every parameter doc_model() reports has a valid type, and there are no duplicate keys
2 test_param_key_resolution Every key in get_config_list() resolves via get_config(key) and param.name matches the bare key
3 test_params_mock All config params round-trip: read-only params are queried; read-write params are written then queried back (mock solver)
4 test_funcs_mock All registered operations return a dict with the correct output keys (mock solver)
5 test_lifecycle Context manager completes; is_operatable is True after connect()
6 test_identity identity() returns a non-empty string
7 test_check_errors check_errors() returns [] on a healthy mock device
8 test_state_coverage device.state keys are a superset of get_config_list()

Nothing in this pipeline reads a schema file. Gate 1 validates what doc_model() reports, because the running device is the only authority: parameters registered in Python never appear in the JSON at all, and for a grouped schema the file's keys are not the keys get_config_list() answers. A gate reading the files would be checking something the device does not use.

Note also that the numbers above are this page's running order and nothing else. Three other numbered sequences exist — the field test's own eight, and plesty check's fourteen — so "gate 8" on its own is ambiguous. Say the name.

If you are coming from the ten-gate pipeline

Two members are gone, and neither was ever required by d1:

  • test_hardware_schema_refresh (was gate 10) is superseded by FieldTestPipeline.propose_schema_update, which proposes a reviewed change instead of silently writing a *_refreshed.json beside the original.
  • test_resource_allocation (was gate 9) moved out whole: call plesty.lib.test.resource_allocation.assert_resource_manager_client_allocation directly. It spawns a server and three client processes, which is not something to hide behind a method named like a mock gate.

Hardware tests

A test that needs a real instrument is marked @pytest.mark.hardware. None of the eight gates above is one — they all run against the mock. Skip hardware tests in CI by adding to pyproject.toml:

[tool.pytest.ini_options]
markers = ["hardware: tests that require real hardware (skipped in CI)"]
addopts = "-m 'not hardware'"

Making the device testable without hardware

The device constructor must work without a real instrument. The standard pattern is to use a mock transport when the transport is None or a flag is set:

class PowermeterDevice(BaseDeviceSyncModel):
    def __init__(self, address: str, sensor_type: str = "S155C", _mock: bool = False):
        super().__init__(id=address, param_schema=str(Path(__file__).parent / "schema_param.json"))
        self._address = address
        self._mock = _mock

    def init(self, main=None) -> None:
        if self._mock:
            # Use a simple mock that returns "0" for any command
            self.tm = MockTrafficManager()
        else:
            self.tm = VisaTrafficManager(self._address)
        self.solver = SCPISolver()

Then in the pipeline:

PIPELINE = DevicePipeline(
    PowermeterDevice,
    "mock-address",
    sensor_type="S155C",
    _mock=True,
    param_schema=str(Path(__file__).parent / "schema_param.json"),
)

CI=true and Gate d1

Gate d1 runs locally as part of plesty check --standard quantum. It is not one of the CI-only gates. The DevicePipeline test suite runs via pytest under Gate 7 (test coverage), and plesty check counts the d1 test functions explicitly to confirm they exist.