Plesty Documentation

Gate d1 — Device API Pipeline

Write the eight DevicePipeline mock-gate test functions that Gate d1 requires.

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__
    param_schema="assets/param_schema.json",
    op_schema="assets/op_schema.json",
)


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()


@pytest.mark.hardware
def test_hardware_schema_refresh():
    PIPELINE.test_hardware_schema_refresh()

The eight mock gates

Gate Function What it checks
1 test_schema_integrity param_schema.json and op_schema.json are valid JSON; all params have a valid type (int, float, str, bool); 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()

Gate 9 (test_resource_allocation) and Gate 10 (test_hardware_schema_refresh) are optional — Gate d1 only requires gates 1–8.

Hardware gate

The @pytest.mark.hardware test requires a real device. Skip it 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="assets/param_schema.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="assets/param_schema.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.