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]ofpyproject.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.