8 mock gates that any PLESTY device module inherits — no hardware required
plesty-libDevicePipeline is a class in plesty.lib.test.device_pipeline. Instantiate it once in your test file with a VISA-free mock device class and the paths to your JSON schemas. Then call its 8 gate methods from plain pytest functions — one line each.
It handles all the heavy lifting: schema loading and validation, mock solver binding, lifecycle management, assertion logic, and error reporting. You provide the mock device; DevicePipeline does the rest.
DevicePipeline is also what Gate d1 of plesty check validates. When module_type = "device" is set in your pyproject.toml, the check command verifies all 8 gate functions exist in tests/ and pass without hardware.
connect(); most skip it entirelyThe mock device and schemas flow into a single PIPELINE instance. All 8 gate methods are called from standalone pytest functions — no class, no fixture setup.
schema_param.json and schema_func.json. Validates that both are well-formed JSON and contain the required top-level structure.json.load().get_config_list() on the mock device to get all declared parameter keys, then calls get_config(key) for each one. Asserts every key resolves without raising.init(), missing SCPI command mappings, keys declared in the schema but not wired to the solver.init() in your mock device class. Ensure SCPISolver is assigned to self._param_solver before any param access.get_config(key) and writes a valid value back via set_config(key, value) using a mock solver. Asserts no exceptions are raised.SCPISolver write format (wfmt) is set and matches the SCPI command expected by each parameter.schema_func.json) against a mock solver. Asserts each returns a dict — not None, not raises, not a raw string.FuncMeta but whose mock response can't be cast to the expected type, custom op solvers that return the wrong shape._mock_tm.send_command.return_value to a string that can be cast to the schema's output_dtype (e.g. "1.234" for float).with device: ...). Asserts is_operatable is True inside the block and False after the block exits.connect() not setting self._tm, disconnect() not clearing solvers, __enter__/__exit__ not calling connect/disconnect.connect(), assign self._tm = self._mock_tm. In disconnect(), set self._tm = None and self._param_solver = None.identity() on a connected mock device. Asserts the return value is a non-empty string.identity() implementation, returning None, returning empty string, returning a non-string type.identity() in your mock class to return a fixed vendor IDN string: return "VENDOR,MODEL,SN00001,1.0.0".check_errors() on a healthy mock device (with no errors injected). Asserts the return is an empty list [].check_errors() returning None, raising an exception, returning a non-list type, or returning a non-empty list on a healthy device.check_errors() to return list[str]. Wire _mock_tm.send_command.return_value = '0,"No error"' so parsing returns an empty error list.dev.state (the property that snapshots all param values) and asserts the returned dict's keys are a superset of get_config_list(). Every declared param must appear in the state snapshot._get_state(), incomplete state snapshots that would break experiment logging._get_state(), iterate get_config_list() and include every key. Don't manually list param names — use the schema as the authoritative source.# tests/test_mydevice.py — complete worked example from __future__ import annotations import uuid from typing import Any from unittest.mock import MagicMock from plesty.lib.solver.scpi import SCPISolver from plesty.lib.test.device_pipeline import DevicePipeline from plesty.mydevice.base_device import _MyDeviceOpSolver from plesty.mydevice.device import Device # ── Step 1: VISA-free mock device ───────────────────────────────────────────── class _MockMyDevice(Device): """VISA-free mock. Each instance gets a unique address to avoid registry collisions when tests run in parallel.""" def __init__(self) -> None: super().__init__( address=f"USB::MOCK::{uuid.uuid4().hex[:8].upper()}::INSTR" ) self._mock_tm: MagicMock = MagicMock() self._mock_tm.is_open = True self._mock_tm.send_command.return_value = '0,"No error"' self._tm = self._mock_tm def init(self, main: Any = None) -> None: self._param_solver = SCPISolver(qfmt="{cmd}?", wfmt="{cmd} {value}") self.bind_op_solver(_MyDeviceOpSolver(self._mock_tm)) def connect(self) -> bool: if self._param_solver is None: self.init() self._mock_tm.is_open = True self._tm = self._mock_tm return True def disconnect(self) -> None: self._tm = None self._param_solver = None def identity(self) -> str: return "VENDOR,MODEL,SN00001,1.0.0" # ── Step 2: Instantiate the pipeline (shared across all gate tests) ─────────── PIPELINE = DevicePipeline( _MockMyDevice, param_schema="plesty/mydevice/schema_param.json", op_schema="plesty/mydevice/schema_func.json", ) # ── Step 3: Write the 8 gate functions (required by plesty check Gate d1) ───── def test_schema_integrity() -> None: PIPELINE.test_schema_integrity() def test_param_key_resolution() -> None: PIPELINE.test_param_key_resolution() def test_params_mock() -> None: PIPELINE.test_params_mock() def test_funcs_mock() -> None: PIPELINE.test_funcs_mock() def test_lifecycle() -> None: PIPELINE.test_lifecycle() def test_identity() -> None: PIPELINE.test_identity() def test_check_errors() -> None: PIPELINE.test_check_errors() def test_state_coverage() -> None: PIPELINE.test_state_coverage()
_MockMyDevice ReplacesThe mock device replaces the real VISA traffic manager (self._tm) with a MagicMock instance. Three attributes must be wired correctly:
# Required mock wiring in __init__ self._mock_tm.is_open = True # → makes is_operatable return True self._mock_tm.send_command.return_value = '0,"No error"' # → check_errors() returns [] self._tm = self._mock_tm # → routes all solver calls through mock # Methods you must implement in _MockMyDevice # init() — bind SCPISolver + op solver # connect() — set self._tm = self._mock_tm, return True # disconnect() — set self._tm = None, self._param_solver = None # identity() — return a fixed IDN string # Everything else is inherited from plesty.lib.device.Device
from plesty.lib.test.device_pipeline import DevicePipeline
plesty-lib >= 0.2.6