plesty.lib.test.device_pipeline =============================== .. py:module:: plesty.lib.test.device_pipeline .. autoapi-nested-parse:: Standard device API test pipeline for PLESTY device modules. Eight gates, no hardware. This is the contract SDK gate d1 enforces, and every gate here runs in CI on every push. **Hardware belongs to the field test.** Until v0.4.0 this class also carried a multi-client resource-contention gate and a schema-refresh gate that needed a real instrument, numbered 9 and 10 — so the class had ten gates of which ``run_mock_pipeline()`` ran eight, and no hub module ever called the other two. :class:`~plesty.lib.test.field_test.FieldTestPipeline` is where hardware is exercised now; see *Migrating from the ten-gate pipeline* below. What passing here does and does not mean: these gates verify that a module's declarations are coherent and that its parameter and operation systems work against a mock. They say nothing about the command-format contract, driver binding, or transport pathologies — those have no mock equivalent and are what the field test is for. A module that passes all eight can still fail on the instrument, by design. Import ``DevicePipeline`` in a device module's test suite and call the individual gate methods from pytest test functions, or call ``run_mock_pipeline()`` to run all gates in one shot. ``plesty init mock-test`` generates the whole file from the module's own declarations. Typical usage in a device module — the whole file:: # tests/test_mydevice.py from plesty.lib.test.device_pipeline import DevicePipeline from plesty.mydevice import Device PIPELINE = DevicePipeline(Device, address="mock") def test_mock_pipeline(): PIPELINE.run_mock_pipeline() Gate summary ------------ Gate 1 ``test_schema_integrity`` — the device's declarations are coherent. Gate 2 ``test_param_key_resolution`` — every key in get_config_list() resolves. Gate 3 ``test_params_mock`` — all config params round-trip (mock solver). Gate 4 ``test_funcs_mock`` — all operations return a dict (mock solver). Gate 5 ``test_lifecycle`` — connect / is_operatable / disconnect. Gate 6 ``test_identity`` — identity() returns a non-empty string. Gate 7 ``test_check_errors`` — check_errors() returns [] on healthy mock. Gate 8 ``test_state_coverage`` — dev.state keys ⊇ get_config_list(). The numbers are the docs' running order and nothing else — three other numbered gate sequences exist (the field test's own eight, ``plesty check``'s fourteen), so a bare "gate 8" is ambiguous. Say the name. **Nothing here reads a schema file.** Gate 1 validates what ``doc_model()`` reports, because the device is the only authority: parameters registered in Python are invisible in the JSON, and for a grouped schema the file's keys are not the keys ``get_config_list()`` answers. A gate reading the files checks something the running device does not use. Migrating from the ten-gate pipeline ------------------------------------ ``test_hardware_schema_refresh`` (was gate 10) is superseded by :meth:`~plesty.lib.test.field_test.FieldTestPipeline.propose_schema_update`, which proposes a reviewed change instead of silently writing a ``*_refreshed.json`` beside the original. The underlying :func:`plesty.lib.test.schema_params.test_real_device_refreshes_schema_defaults_and_types` is unchanged and still importable if the old behaviour is wanted. ``test_resource_allocation`` (was gate 9) moved out whole: call :func:`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 — and importing it is what pulled ``pytest`` into every consumer of this module (plesty-lib#19). Attributes ---------- .. autoapisummary:: plesty.lib.test.device_pipeline._VALID_PARAM_TYPES Classes ------- .. autoapisummary:: plesty.lib.test.device_pipeline.DevicePipeline Functions --------- .. autoapisummary:: plesty.lib.test.device_pipeline._check_parameters plesty.lib.test.device_pipeline._check_param_bounds plesty.lib.test.device_pipeline._check_runtime_keys plesty.lib.test.device_pipeline._check_operations Module Contents --------------- .. py:class:: DevicePipeline(device_cls: type, *args: Any, seed: int = 123, **kwargs: Any) Standard test pipeline for a PLESTY device API module. :param device_cls: The device class under test. :param \*args: Positional arguments forwarded to ``device_cls.__init__``. :param seed: RNG seed passed to ``DataGenerator`` for reproducible mock values. :param \*\*kwargs: Keyword arguments forwarded to ``device_cls.__init__``. :param Store device class: :param construction arguments: :param and pipeline configuration.: .. py:attribute:: device_cls .. py:attribute:: args :value: () .. py:attribute:: kwargs .. py:attribute:: seed :value: 123 .. py:method:: test_schema_integrity() -> None Gate 1: what the device declares is coherent. Read from ``doc_model()``, not from the schema files. The device is the only authority, and reading the JSON instead goes wrong twice, silently: anything registered in Python — ``register_config`` in ``__init__``, an ``@expose_to_api`` method — is invisible in the files, and for a grouped schema the keys are wrong, because ``get_config_list()`` answers ``prefix.key`` while the file is keyed by the bare name. That is the group-name against command-prefix mismatch three modules hit independently. A gate reading the files checks something the running device does not use. Checks that every parameter declares a usable type, that its declared bounds and options agree with that type, that no two parameters collide on the runtime key, and that every operation output declares a type. .. py:method:: test_param_key_resolution() -> None Gate 2: every key in get_config_list() resolves via get_config(key). Also verifies that param.name matches the bare (non-prefixed) part of each key, confirming the grouped-key invariant is upheld. .. py:method:: test_params_mock(ignore_keys: list[str] | None = None) -> None Gate 3: all config params round-trip through the mock solver. Read-only params are queried; read-write params are written then queried back. Write-only params are skipped. .. py:method:: test_funcs_mock(ignore_ops: list[str] | None = None) -> None Gate 4: all registered operations return a dict with the mock solver. A mock op solver is bound that generates schema-valid outputs from ``FuncOutput`` metadata, so no hardware is required. .. py:method:: test_lifecycle() -> None Gate 5: context manager completes; is_operatable is True after connect. The device must implement ``init()``, ``connect()``, and ``disconnect()`` in a way that does not require real hardware (e.g., by accepting a mock transport as a constructor argument). .. py:method:: test_identity() -> None Gate 6: identity() returns a non-empty string. The device must override identity() — returning a mock IDN string is acceptable in mock mode. .. py:method:: test_check_errors() -> None Gate 7: check_errors() returns an empty list on a healthy mock device. .. py:method:: test_state_coverage() -> None Gate 8: dev.state keys are a superset of get_config_list(). The mock param solver is applied so that query() calls succeed without hardware. Extra keys in state (beyond the config list) are permitted and reported as informational. .. py:method:: run_mock_pipeline(ignore_keys: list[str] | None = None, ignore_ops: list[str] | None = None) -> None Run all eight gates and raise with everything that failed. Every gate runs even after one fails, so a single run reports all of it rather than one thing at a time. This is what SDK gate d1 looks for, and what ``plesty init mock-test`` generates. :param ignore_keys: Config parameter keys to skip in the parameter gate. :param ignore_ops: Operation names to skip in the operation gate. .. py:data:: _VALID_PARAM_TYPES :type: tuple[type, Ellipsis] .. py:function:: _check_parameters(groups: dict[str, list[Any]]) -> list[str] Return every incoherence in the device's declared parameters. :param groups: ``doc_model().parameters`` — parameters by configuration group. :returns: One message per problem found, empty when there are none. .. py:function:: _check_param_bounds(where: str, param: Any, dtype: type) -> list[str] Return the option and access-mode problems of one parameter. Only what registration does not already reject. ``register_config`` validates that ``min_value`` is not above ``max_value``, and that a *default* is within its type and options — but never that the options themselves match the declared type, and never that a parameter is not declared readable and writable-only at once. The mock generator draws from this metadata, so either produces a value the device then rejects. :param where: The parameter's qualified name, for the message. :param param: The declared parameter. :param dtype: Its declared type. :returns: One message per problem found. .. py:function:: _check_runtime_keys(keys: list[str]) -> list[str] Return a message per runtime key claimed by more than one parameter. :param keys: What ``get_config_list()`` answers. :returns: One message per collision, empty when every key is unique. .. py:function:: _check_operations(functions: list[Any]) -> list[str] Return every incoherence in the device's declared operations. :param functions: ``doc_model().functions`` — the device's own operations. :returns: One message per problem found.