plesty-lib 0.3.5.dev2__tar.gz → 0.3.5.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/CHANGELOG.md +68 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/PKG-INFO +1 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/base_device.md +6 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/test_helper.md +25 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/gl-secret-detection-report.json +5 -5
- plesty_lib-0.3.5.dev3/plesty/lib/device/device_utils.py +231 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/funcs.py +18 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/client_field_test.py +88 -9
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/field_test.py +330 -24
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/report_artifact.py +7 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/__init__.py +17 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/apt.py +9 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/serial.py +5 -0
- plesty_lib-0.3.5.dev3/plesty/lib/traffic/serial_ports.py +86 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/qt/plot.py +2 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/qt/shell.py +1 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_client_field_test.py +123 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_funcs.py +64 -1
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_field_test.py +250 -2
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_report_artifact.py +7 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_traffic_drivers.py +101 -0
- plesty_lib-0.3.5.dev2/plesty/lib/device/device_utils.py +0 -108
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/.gitignore +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/.gitlab-ci.yml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/LICENSE +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/LICENSES/LGPL-3.0-or-later.txt +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/README.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/REUSE.toml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/analyzer.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/api_reference.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/architecture.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/contributing.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/data_schemas.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/cmd_solver.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/composite_device.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/error_handling.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/func_system.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/index.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/logging_system.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/param_system.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/device/traffic_manager.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/experiment.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/imgs/device_layers.svg +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/imgs/device_standard.svg +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/imgs/plesty_framework.svg +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/index.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/monitor.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/quickstart.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/toc.yaml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/docs/ui.md +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/analyzer/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/analyzer/base_analyzer.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/data/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/data/array.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/data/ctype_manager.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/data/io.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/data/table.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/data/types.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/data/units.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/async_wrapper.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/base_apt_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/base_device_sync.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/base_visa_scpi_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/composite_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/doc.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/params.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/device/telemetry.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/experiment/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/experiment/assets/preflight.yaml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/experiment/base_experiment.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/experiment/journal.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/experiment/preflight.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/experiment/runs.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/experiment/schedule.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/monitor/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/monitor/assets/viz.yaml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/monitor/base_monitor.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/monitor/sources.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/monitor/viz.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/service/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/service/resource_manager.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/service/tcp_ip_client.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/service/tcp_ip_server.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/sim/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/sim/apt.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/sim/data_generator.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/sim/demo_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/solver/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/solver/iceblock.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/solver/scpi.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/device_func_system.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/device_param_system.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/device_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/experiment_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/field_test_concurrency.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/grouped_param_keys.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/resource_allocation.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/schema_params.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/test/schema_refresh.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/apt_protocol.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/tcp_ip.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/usb_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/traffic/visa.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/assets/palette.json +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/assets/shell.qss +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/monitor_panel.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/panel.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/qt/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/qt/app.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/qt/recorder.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/ui/theme.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/utils/__init__.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/utils/config.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/utils/dll_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/utils/error_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/utils/logger.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/utils/registry.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/plesty/lib/utils/settings.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/pyproject.toml +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_analyzer.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_async_wrapper_threading.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_composite_config.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_config.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_data_array.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_data_io_hdf5.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_data_types.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_demo_device.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_apt.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_base.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_data_path.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_params.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_scpi.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_device_telemetry.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_experiment.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_experiment_pipeline.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_experiment_preflight.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_experiment_runs.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_misc.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_monitor.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_schema_params.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_schema_refresh.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_service_manager.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_settings.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_shipped_imports.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_sim.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_solver.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_tcp_ip_resources.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_traffic_orphan.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_traffic_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_ui.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_utils.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_visa_discovery.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/tests/test_viz.py +0 -0
- {plesty_lib-0.3.5.dev2 → plesty_lib-0.3.5.dev3}/uv.lock +0 -0
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5.dev3 — 2026-08-18
|
|
4
|
+
|
|
5
|
+
Development pre-release for field-testing operation kinds (#39): operations
|
|
6
|
+
declare what they do and the field test drives a motion out and back, a
|
|
7
|
+
lifecycle op once, a control op at rest; plus the pol-pl rig findings #35
|
|
8
|
+
and #38. Same line as 0.3.5.dev2 — the final release is 0.4.0.
|
|
9
|
+
|
|
10
|
+
- **Operations declare what they do, and the field test drives them by
|
|
11
|
+
kind** (#39). `@expose_to_api(kind=…)` and a schema `kind` block record
|
|
12
|
+
one of `read | motion | acquire | lifecycle | control | configure`
|
|
13
|
+
(`plesty.lib.device.device_utils.OpKind`; `FuncDoc.kind`, the describe
|
|
14
|
+
payload and `function_docs()` carry it). `FieldTestPipeline` acts on it
|
|
15
|
+
instead of asking the developer per operation: a `motion` op is driven
|
|
16
|
+
one `step` out from what its `position_key` reports and put back (the
|
|
17
|
+
functions gate checks it came back; the stability gate repeats out and
|
|
18
|
+
back), a `lifecycle` op runs once when allow-listed and never on repeat,
|
|
19
|
+
a `control` op once at rest after the motions (skipped when nothing
|
|
20
|
+
moved), a `configure` op is never called. `op_kinds={…}` overrides the
|
|
21
|
+
module's declaration. On plesty-k10cr1 the operation every pol-pl row
|
|
22
|
+
calls could not be tested at all: `move_absolute(position)` was called
|
|
23
|
+
bare and failed on the missing argument.
|
|
24
|
+
- **The client tier drives operations by kind too.** `ClientFieldTest`
|
|
25
|
+
takes `op_kinds=` (the generated `field_test_client.py` passes the host
|
|
26
|
+
tier's): a motion is queried through `position_key`, moved one step out
|
|
27
|
+
and back over the wire as many pairs as the host tier's call count, a
|
|
28
|
+
lifecycle op once, a control op once after the motions, a configure op
|
|
29
|
+
never; a motion without `position_key`/`target`/`step` fails the gate.
|
|
30
|
+
- **A provocation that breaks is a failed gate, not a forced timeout.**
|
|
31
|
+
The drain and recovery gates called the slow op with the payload as one
|
|
32
|
+
positional dict (`getattr(device, slow_op)(dict(payload))`), so an
|
|
33
|
+
`@expose_to_api` slow op raised `TypeError` before any transport
|
|
34
|
+
traffic — and both gates counted the exception as an abandoned call and
|
|
35
|
+
passed. The call now goes through the operation's own call form, only a
|
|
36
|
+
timeout-class error counts as forced, and a motion slow op is a delta
|
|
37
|
+
from where the part is, moved back afterwards (`motion_restored` in the
|
|
38
|
+
evidence). The slow-op payload is prepared before the timeout is
|
|
39
|
+
shortened, so reading the start position cannot itself time out.
|
|
40
|
+
|
|
41
|
+
- **A serial port that is held says so** (#35). pyserial reports a wrong
|
|
42
|
+
port name and a port open in another process with the same
|
|
43
|
+
`SerialException`, and on the K10CR1 rig `Access is denied` on the right
|
|
44
|
+
`COM4` — Kinesis had the stage loaded — cost the session to driver and
|
|
45
|
+
VCP checks. `TrafficManager.open` now asks the subclass to explain a
|
|
46
|
+
failure (`_explain_open_error`, `None` by default) and appends the answer
|
|
47
|
+
to the raised message. `AptTrafficManager` and `SerialTrafficManager`
|
|
48
|
+
answer for EACCES/EBUSY: on Windows and for EBUSY, the port exists but is
|
|
49
|
+
open elsewhere — close Kinesis/APT (a loaded stage locks the FTDI device
|
|
50
|
+
via D2XX and blocks the VCP), stray interpreters, serial terminals, replug
|
|
51
|
+
to drop a stale handle; on Linux EACCES, add the user to `dialout`. The
|
|
52
|
+
port's `list_ports` description and hwid follow, so the user sees at once
|
|
53
|
+
whether the address is the intended device. Windows sets no `errno` on
|
|
54
|
+
the exception, so the number is read out of the message text. Other
|
|
55
|
+
failures (`ENOENT`, timeouts) are reported as before.
|
|
56
|
+
- `ModuleInfo.from_project` and the field-test version lookup no longer
|
|
57
|
+
answer for whatever repository `GIT_DIR` names: a git hook exports it for
|
|
58
|
+
the repository being pushed, and every `git -C <root>` in `report_artifact`
|
|
59
|
+
followed it — a field test run from a pre-push hook reported the hook's
|
|
60
|
+
remote and version, and the pre-push suite failed on the tmp-dir module
|
|
61
|
+
test. `GIT_*` is scrubbed from the subprocess environment.
|
|
62
|
+
|
|
63
|
+
- **Client-tier `functions` gate no longer demands a dict of every
|
|
64
|
+
operation** (#38). It required `isinstance(answer, dict)` for each
|
|
65
|
+
allow-listed operation, while the host tier asks that only of
|
|
66
|
+
schema-declared ones; on lightfield_spectrometer `acquire` (`None`) and
|
|
67
|
+
`get_recent_file` (`str`) came back 21 times each with no failure and
|
|
68
|
+
the gate still failed. A call that returns is the pass; the answer's
|
|
69
|
+
type is recorded in the evidence.
|
|
70
|
+
|
|
3
71
|
## 0.3.5.dev2 — 2026-08-17
|
|
4
72
|
|
|
5
73
|
Development pre-release for field-testing the rig start-up (#36, #37):
|
|
@@ -261,7 +261,12 @@ model.standard_methods # the common device API (connect/write/query/reset/...)
|
|
|
261
261
|
`functions` unifies the two ways a device gains operations: schema-registered
|
|
262
262
|
operations (`source="schema"`) and `@expose_to_api` methods
|
|
263
263
|
(`source="custom"`); framework plumbing is kept out and surfaced separately
|
|
264
|
-
under `standard_methods` (`source="standard"`).
|
|
264
|
+
under `standard_methods` (`source="standard"`). Each `FuncDoc` also carries
|
|
265
|
+
its `kind` — what the operation does to the world (`read`, `motion`,
|
|
266
|
+
`acquire`, `lifecycle`, `control`, `configure`), declared with
|
|
267
|
+
`@expose_to_api(kind=..., position_key=..., step=...)` or a `"kind"` block in
|
|
268
|
+
`schema_func.json` and used by the field test to drive the operation
|
|
269
|
+
(see *test_helper*, "Kinds"). Per-parameter and return
|
|
265
270
|
descriptions are parsed from Google-style docstring `Args:`/`Returns:`
|
|
266
271
|
sections — write those in your device methods and the generated documentation
|
|
267
272
|
inherits them.
|
|
@@ -455,7 +455,7 @@ if __name__ == "__main__":
|
|
|
455
455
|
| 2 | `test_connect_lifecycle` | connect / identity / `check_errors` / disconnect / reconnect, each timed; identity stable across the reconnect |
|
|
456
456
|
| 3 | `test_param_roundtrip` | every writable parameter round-trips; the value read back is written back, so device state is unchanged |
|
|
457
457
|
| 4 | `test_param_constraints` | hardware MIN/MAX and option sets against the schema envelope — a schema wider than the hardware fails |
|
|
458
|
-
| 5 | `test_functions` | every operation on the safe allow-list
|
|
458
|
+
| 5 | `test_functions` | every operation on the safe allow-list can be called — by kind: a motion one step out and back, a lifecycle op once, a control op once at rest after the motions |
|
|
459
459
|
| 6 | `test_buffer_drain` | a query abandoned mid-flight must not leave its answer for the next query to consume |
|
|
460
460
|
| 7 | `test_error_recovery` | the device recovers from a forced timeout, and the first command after a reconnect succeeds |
|
|
461
461
|
| 8 | `test_stability` | every queryable parameter and safe op repeated `repetitions` times to collect the statistics |
|
|
@@ -464,6 +464,29 @@ if __name__ == "__main__":
|
|
|
464
464
|
(a `"safe": true` entry in `schema_func.json` is forwarded automatically). Without an allow-list
|
|
465
465
|
the function gate is skipped — no motion or emission operation ever runs unattended.
|
|
466
466
|
|
|
467
|
+
**Kinds.** *How* an allow-listed operation is called comes from what the module declares it
|
|
468
|
+
does — `@expose_to_api(kind=...)` or a `"kind"` block in `schema_func.json`, read back as
|
|
469
|
+
`OpKind` (`plesty.lib.device.device_utils`):
|
|
470
|
+
|
|
471
|
+
| kind | the gates |
|
|
472
|
+
|---|---|
|
|
473
|
+
| `read` (default) | called with its declared payload, repeated as it is |
|
|
474
|
+
| `motion` | driven from what `position_key` reports: `target = start ± step` (`relative=True` sends the step itself), then put back; the functions gate fails if it does not come back, the stability gate repeats out-and-back; a motion `slow_op` is a delta from the start and is moved back after the provocation |
|
|
475
|
+
| `acquire` | as `read`, but recorded as acting |
|
|
476
|
+
| `lifecycle` (home, reset) | once, when allow-listed; never repeated |
|
|
477
|
+
| `control` (stop, abort) | once, at rest, after the motions; skipped when nothing moved |
|
|
478
|
+
| `configure` | never called — the round-trip gate covers settings |
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
@expose_to_api(kind="motion", position_key="MO.Position", step=1.0)
|
|
482
|
+
def move_absolute(self, position: float) -> float: ...
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`op_kinds={"move_absolute": OpKind("motion", position_key="MO.Position", step=1.0)}` on the
|
|
486
|
+
pipeline overrides the declaration wholesale. A motion without `position_key`, `target` (the
|
|
487
|
+
first required argument by default) or `step` fails the gate rather than guessing how far a part
|
|
488
|
+
may move.
|
|
489
|
+
|
|
467
490
|
**Key parameters:**
|
|
468
491
|
|
|
469
492
|
| Parameter | Default | Description |
|
|
@@ -471,6 +494,7 @@ the function gate is skipped — no motion or emission operation ever runs unatt
|
|
|
471
494
|
| `repetitions` | `20` | Per-command repeat count in the stability gate |
|
|
472
495
|
| `report_path` | `None` | Base path; `run()` writes `<base>.jsonl` (appended) and `<base>.md` |
|
|
473
496
|
| `safe_ops` / `unsafe_ops` | `None` | Operation allow-list and opt-out list |
|
|
497
|
+
| `op_kinds` | `None` | Per-operation `OpKind` overriding the module's declaration |
|
|
474
498
|
| `op_payloads` | `None` | Explicit payload per operation instead of a generated one |
|
|
475
499
|
| `ignore_keys` | `None` | Config keys excluded from every parameter gate |
|
|
476
500
|
| `drain_probe` | auto | The two distinguishable keys used by the drain and recovery gates |
|
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
"version": "8.30.1"
|
|
20
20
|
},
|
|
21
21
|
"type": "secret_detection",
|
|
22
|
-
"start_time": "2026-08-
|
|
23
|
-
"end_time": "2026-08-
|
|
22
|
+
"start_time": "2026-08-18T13:39:46",
|
|
23
|
+
"end_time": "2026-08-18T13:39:47",
|
|
24
24
|
"status": "success",
|
|
25
25
|
"observability": {
|
|
26
26
|
"events": [
|
|
27
27
|
{
|
|
28
28
|
"event": "collect_secrets_analyzer_scan_metrics_from_pipeline",
|
|
29
|
-
"time_s": 0.
|
|
29
|
+
"time_s": 0.527730295,
|
|
30
30
|
"exit_code": 0,
|
|
31
31
|
"git_strategy": "FetchShallow",
|
|
32
|
-
"repo_size_kb":
|
|
32
|
+
"repo_size_kb": 709,
|
|
33
33
|
"commit_count": 1,
|
|
34
|
-
"bytes_scanned":
|
|
34
|
+
"bytes_scanned": 317,
|
|
35
35
|
"pipeline_type": "Tag"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Plesty Development Team
|
|
2
|
+
# SPDX-FileContributor: Yunshuang Yuan
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
"""Utility functions for device connectivity and configuration."""
|
|
7
|
+
|
|
8
|
+
import socket
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
import functools
|
|
12
|
+
import inspect
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResponseParser:
|
|
16
|
+
"""Parser to be used in the base device class. Once registered along with a config parameter,
|
|
17
|
+
the parser will be called to parse the raw response from `_query_()` function of the device.
|
|
18
|
+
The parsed result will be used to update the corresponding configuration parameter in the
|
|
19
|
+
`query()` function.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __call__(self, response, param, **kwargs) -> Any:
|
|
23
|
+
"""Parse the raw response from the device and return a structured result."""
|
|
24
|
+
# Call the parse method implemented by subclasses
|
|
25
|
+
return self.parse(response, param=param, **kwargs)
|
|
26
|
+
|
|
27
|
+
def parse(self, response, param, **kwargs) -> Any:
|
|
28
|
+
"""Alias for __call__, allowing explicit parsing calls."""
|
|
29
|
+
raise NotImplementedError(
|
|
30
|
+
"The parse method should be implemented by subclasses of ResponseParser."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# define decoretor for device methods that require operatability check
|
|
35
|
+
def operatable(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
36
|
+
"""Decorator that checks device operatability before executing the wrapped function."""
|
|
37
|
+
if inspect.iscoroutinefunction(func):
|
|
38
|
+
|
|
39
|
+
@functools.wraps(func)
|
|
40
|
+
async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
41
|
+
if not self.is_operatable:
|
|
42
|
+
raise RuntimeError("Device is not operatable. Please connect to the device first.")
|
|
43
|
+
return await func(self, *args, **kwargs)
|
|
44
|
+
|
|
45
|
+
return async_wrapper
|
|
46
|
+
else:
|
|
47
|
+
|
|
48
|
+
@functools.wraps(func)
|
|
49
|
+
def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
50
|
+
if not self.is_operatable:
|
|
51
|
+
raise RuntimeError("Device is not operatable. Please connect to the device first.")
|
|
52
|
+
return func(self, *args, **kwargs)
|
|
53
|
+
|
|
54
|
+
return sync_wrapper
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# define decoretor for device methods that require error checking after execution
|
|
58
|
+
def error_check(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
59
|
+
"""Decorator that checks for device errors after executing the wrapped function."""
|
|
60
|
+
if inspect.iscoroutinefunction(func):
|
|
61
|
+
|
|
62
|
+
@functools.wraps(func)
|
|
63
|
+
async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
64
|
+
result = await func(self, *args, **kwargs)
|
|
65
|
+
errors = await self.check_errors()
|
|
66
|
+
if errors:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"Device reported errors after executing '{func.__name__}': {errors}"
|
|
69
|
+
)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
return async_wrapper
|
|
73
|
+
else:
|
|
74
|
+
|
|
75
|
+
@functools.wraps(func)
|
|
76
|
+
def sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
77
|
+
result = func(self, *args, **kwargs)
|
|
78
|
+
errors = self.check_errors()
|
|
79
|
+
if errors:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
f"Device reported errors after executing '{func.__name__}': {errors}"
|
|
82
|
+
)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
return sync_wrapper
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_local_ip() -> str:
|
|
89
|
+
"""Return the local IP address of this machine."""
|
|
90
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
91
|
+
try:
|
|
92
|
+
# doesn't need to be reachable
|
|
93
|
+
s.connect(("8.8.8.8", 80))
|
|
94
|
+
ip = s.getsockname()[0]
|
|
95
|
+
finally:
|
|
96
|
+
s.close()
|
|
97
|
+
return ip
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#: What an operation does to the world, as far as an unattended test needs to
|
|
101
|
+
#: know. Declared once by the module; read by the field test, which decides per
|
|
102
|
+
#: kind how an operation is called, repeated, made slow and put back — instead
|
|
103
|
+
#: of asking the developer to re-derive it for every operation.
|
|
104
|
+
OP_KINDS: tuple[str, ...] = ("read", "motion", "acquire", "lifecycle", "control", "configure")
|
|
105
|
+
|
|
106
|
+
#: Attribute the exposure marker and the kind are stored under on the method.
|
|
107
|
+
EXPOSE_MARKER = "__plesty_expose_in_api__"
|
|
108
|
+
OP_KIND_MARKER = "__plesty_op_kind__"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class OpKind:
|
|
113
|
+
"""The class of an operation and, for motion, what the test needs to drive it.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
kind: One of :data:`OP_KINDS`. ``read`` returns a value and changes
|
|
117
|
+
nothing. ``motion`` moves a part whose position a parameter reports.
|
|
118
|
+
``acquire`` emits or exposes (a shutter, an exposure) and produces
|
|
119
|
+
data. ``lifecycle`` is homing, reset, calibration — done once, never
|
|
120
|
+
on repeat. ``control`` is stop/abort — meaningful only with a motion
|
|
121
|
+
in flight. ``configure`` changes a setting that outlives the call.
|
|
122
|
+
position_key: Motion only — the configuration key that reports where
|
|
123
|
+
the moving part is, in the same units the operation takes.
|
|
124
|
+
target: Motion only — the argument that receives the target. Defaults
|
|
125
|
+
to the operation's first required argument.
|
|
126
|
+
relative: Motion only — the target is a signed step from the current
|
|
127
|
+
position rather than an absolute position.
|
|
128
|
+
step: Motion only — how far the field test may move the part, in
|
|
129
|
+
``position_key`` units. ``None`` leaves it to the test configuration.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
kind: str = "read"
|
|
133
|
+
position_key: str | None = None
|
|
134
|
+
target: str | None = None
|
|
135
|
+
relative: bool = False
|
|
136
|
+
step: float | None = None
|
|
137
|
+
|
|
138
|
+
def __post_init__(self) -> None:
|
|
139
|
+
"""Reject a kind nothing knows how to handle."""
|
|
140
|
+
if self.kind not in OP_KINDS:
|
|
141
|
+
raise ValueError(f"unknown operation kind {self.kind!r}; expected one of {OP_KINDS}")
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def acts(self) -> bool:
|
|
145
|
+
"""Whether the operation changes the world rather than only reading it."""
|
|
146
|
+
return self.kind != "read"
|
|
147
|
+
|
|
148
|
+
def with_defaults(self, **fields: Any) -> "OpKind":
|
|
149
|
+
"""Return a copy where every ``None`` field is filled from *fields*."""
|
|
150
|
+
filled = {name: value for name, value in fields.items() if getattr(self, name) is None}
|
|
151
|
+
return replace(self, **filled) if filled else self
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> dict[str, Any]:
|
|
154
|
+
"""Serialise for a describe payload or a report."""
|
|
155
|
+
return {
|
|
156
|
+
"kind": self.kind,
|
|
157
|
+
"position_key": self.position_key,
|
|
158
|
+
"target": self.target,
|
|
159
|
+
"relative": self.relative,
|
|
160
|
+
"step": self.step,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_options(cls, options: dict[str, Any] | None) -> "OpKind":
|
|
165
|
+
"""Build from registration options or a schema entry (``kind``, ``position_key``, …).
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
options: A mapping that may carry the fields of this class. Unknown
|
|
169
|
+
keys are ignored; a missing ``kind`` is ``read``.
|
|
170
|
+
"""
|
|
171
|
+
options = options or {}
|
|
172
|
+
return cls(
|
|
173
|
+
kind=str(options.get("kind") or "read"),
|
|
174
|
+
position_key=options.get("position_key"),
|
|
175
|
+
target=options.get("target"),
|
|
176
|
+
relative=bool(options.get("relative", False)),
|
|
177
|
+
step=options.get("step"),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def op_kind_of(func: Any) -> OpKind:
|
|
182
|
+
"""Return the :class:`OpKind` a method was exposed with (``read`` when undeclared)."""
|
|
183
|
+
if isinstance(func, (staticmethod, classmethod)):
|
|
184
|
+
func = func.__func__
|
|
185
|
+
return getattr(func, OP_KIND_MARKER, None) or OpKind()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def expose_to_api(
|
|
189
|
+
func: Any = None,
|
|
190
|
+
*,
|
|
191
|
+
kind: str = "read",
|
|
192
|
+
position_key: str | None = None,
|
|
193
|
+
target: str | None = None,
|
|
194
|
+
relative: bool = False,
|
|
195
|
+
step: float | None = None,
|
|
196
|
+
) -> Any:
|
|
197
|
+
"""Mark a device method so it is listed by the describe endpoint.
|
|
198
|
+
|
|
199
|
+
Bare ``@expose_to_api`` marks a ``read``. With arguments it also declares
|
|
200
|
+
what the operation does, which the field test needs to call it unattended::
|
|
201
|
+
|
|
202
|
+
@expose_to_api(kind="motion", position_key="MO.Position")
|
|
203
|
+
def move_absolute(self, position: float) -> float: ...
|
|
204
|
+
|
|
205
|
+
@expose_to_api(kind="motion", position_key="MO.Position", relative=True)
|
|
206
|
+
def move_relative(self, step: float) -> float: ...
|
|
207
|
+
|
|
208
|
+
@expose_to_api(kind="lifecycle")
|
|
209
|
+
def home_stage(self) -> bool: ...
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
func: The method, when used without parentheses.
|
|
213
|
+
kind: One of :data:`OP_KINDS`.
|
|
214
|
+
position_key: Motion — the parameter that reports the moving part's position.
|
|
215
|
+
target: Motion — the argument taking the target; the first required one by default.
|
|
216
|
+
relative: Motion — the target is a step from the current position.
|
|
217
|
+
step: Motion — how far the field test may move, in ``position_key`` units.
|
|
218
|
+
"""
|
|
219
|
+
op_kind = OpKind(
|
|
220
|
+
kind=kind, position_key=position_key, target=target, relative=relative, step=step
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def decorate(method: Any) -> Any:
|
|
224
|
+
real = method.__func__ if isinstance(method, (staticmethod, classmethod)) else method
|
|
225
|
+
setattr(real, EXPOSE_MARKER, True)
|
|
226
|
+
setattr(real, OP_KIND_MARKER, op_kind)
|
|
227
|
+
return method
|
|
228
|
+
|
|
229
|
+
if func is not None:
|
|
230
|
+
return decorate(func)
|
|
231
|
+
return decorate
|
|
@@ -14,7 +14,7 @@ import re
|
|
|
14
14
|
import reprlib
|
|
15
15
|
import numpy as np
|
|
16
16
|
|
|
17
|
-
from plesty.lib.device.device_utils import ResponseParser
|
|
17
|
+
from plesty.lib.device.device_utils import OpKind, ResponseParser, op_kind_of
|
|
18
18
|
from plesty.lib.data import (
|
|
19
19
|
PlestyArray,
|
|
20
20
|
PlestyTable2D,
|
|
@@ -192,6 +192,9 @@ class FuncDoc:
|
|
|
192
192
|
description: Human-readable description (from the schema or docstring).
|
|
193
193
|
iparams: Input parameters.
|
|
194
194
|
oparams: Output values.
|
|
195
|
+
kind: What the operation does to the world (see
|
|
196
|
+
:class:`~plesty.lib.device.device_utils.OpKind`); ``read`` when
|
|
197
|
+
the module declared nothing.
|
|
195
198
|
"""
|
|
196
199
|
|
|
197
200
|
name: str
|
|
@@ -199,6 +202,7 @@ class FuncDoc:
|
|
|
199
202
|
description: str
|
|
200
203
|
iparams: list["FuncParam"]
|
|
201
204
|
oparams: list["FuncOutput"]
|
|
205
|
+
kind: OpKind = field(default_factory=OpKind)
|
|
202
206
|
|
|
203
207
|
|
|
204
208
|
#: Size-bounded repr for operation log lines, so array-valued parameters do
|
|
@@ -450,6 +454,7 @@ class FunctionSystem:
|
|
|
450
454
|
description=str(meta.get("registration_options", {}).get("description", "")),
|
|
451
455
|
iparams=list(meta["iparams"]),
|
|
452
456
|
oparams=list(meta["oparams"]),
|
|
457
|
+
kind=OpKind.from_options(meta.get("registration_options")),
|
|
453
458
|
)
|
|
454
459
|
)
|
|
455
460
|
|
|
@@ -523,6 +528,7 @@ class FunctionSystem:
|
|
|
523
528
|
description=docstring,
|
|
524
529
|
iparams=iparams,
|
|
525
530
|
oparams=oparams,
|
|
531
|
+
kind=op_kind_of(func),
|
|
526
532
|
)
|
|
527
533
|
|
|
528
534
|
def register_from_op_schema(
|
|
@@ -584,6 +590,13 @@ class FunctionSystem:
|
|
|
584
590
|
# Marks an operation the on-hardware field test may call
|
|
585
591
|
# unattended (see plesty.lib.test.field_test.FieldTestPipeline).
|
|
586
592
|
options.setdefault("safe", bool(op_cfg["safe"]))
|
|
593
|
+
# What the operation does (OpKind): the field test drives a
|
|
594
|
+
# "motion" differently from a "read". Validated here so a typo in
|
|
595
|
+
# the schema fails at registration, not in a test on hardware.
|
|
596
|
+
for name in ("kind", "position_key", "target", "relative", "step"):
|
|
597
|
+
if name in op_cfg:
|
|
598
|
+
options.setdefault(name, op_cfg[name])
|
|
599
|
+
OpKind.from_options(options)
|
|
587
600
|
|
|
588
601
|
self.register_func(
|
|
589
602
|
op_name,
|
|
@@ -821,7 +834,10 @@ class FunctionSystem:
|
|
|
821
834
|
"output": self._signature_output_docs(signature, default_type="Any"),
|
|
822
835
|
"description": description,
|
|
823
836
|
"parser": None,
|
|
824
|
-
"registration_options": {
|
|
837
|
+
"registration_options": {
|
|
838
|
+
"source": "expose-to-api",
|
|
839
|
+
**op_kind_of(func).to_dict(),
|
|
840
|
+
},
|
|
825
841
|
"runtime_request_shape": {"op": name, "params": "dict[str, Any]"},
|
|
826
842
|
}
|
|
827
843
|
|
|
@@ -67,6 +67,7 @@ from pathlib import Path
|
|
|
67
67
|
from collections.abc import Sequence
|
|
68
68
|
from typing import Any
|
|
69
69
|
|
|
70
|
+
from plesty.lib.device.device_utils import OpKind
|
|
70
71
|
from plesty.lib.test.report_artifact import (
|
|
71
72
|
Coverage,
|
|
72
73
|
Instrument,
|
|
@@ -148,6 +149,13 @@ class ClientFieldTest:
|
|
|
148
149
|
safe_ops: Served functions the run may call. Nothing not named here is
|
|
149
150
|
called: the client tier inherits the host tier's safety judgement
|
|
150
151
|
rather than making a second one that could disagree with it.
|
|
152
|
+
op_kinds: What each operation does, per name, as the host tier was
|
|
153
|
+
told (:class:`~plesty.lib.device.device_utils.OpKind` or its
|
|
154
|
+
fields as a mapping). The client cannot read the module's
|
|
155
|
+
declaration through ``describe``'s flat method list, so the same
|
|
156
|
+
kinds are handed over: a ``motion`` is driven one ``step`` out
|
|
157
|
+
from what its ``position_key`` reports and back, a ``lifecycle``
|
|
158
|
+
once, a ``control`` once after the motions, a ``configure`` never.
|
|
151
159
|
resources: Lockable resources to contend for. Two clients ask for
|
|
152
160
|
these at once, and the second must be refused.
|
|
153
161
|
expected_types: Parameter key to the type it is declared as. The
|
|
@@ -182,6 +190,7 @@ class ClientFieldTest:
|
|
|
182
190
|
safe_ops: Sequence[str] | None = None,
|
|
183
191
|
resources: Sequence[str] | None = None,
|
|
184
192
|
expected_types: dict[str, type] | None = None,
|
|
193
|
+
op_kinds: dict[str, OpKind | dict[str, Any]] | None = None,
|
|
185
194
|
fixed_threading: bool = True,
|
|
186
195
|
label: str = "",
|
|
187
196
|
instrument: Instrument | None = None,
|
|
@@ -200,6 +209,10 @@ class ClientFieldTest:
|
|
|
200
209
|
self.address = address or f"tcp://127.0.0.1:{_free_port()}"
|
|
201
210
|
self.params = list(params or [])
|
|
202
211
|
self.safe_ops = list(safe_ops or [])
|
|
212
|
+
self.op_kinds: dict[str, OpKind] = {
|
|
213
|
+
name: kind if isinstance(kind, OpKind) else OpKind.from_options(dict(kind))
|
|
214
|
+
for name, kind in (op_kinds or {}).items()
|
|
215
|
+
}
|
|
203
216
|
self.resources = list(resources or [])
|
|
204
217
|
self._expected_types = dict(expected_types or {})
|
|
205
218
|
self.fixed_threading = fixed_threading
|
|
@@ -451,23 +464,89 @@ class ClientFieldTest:
|
|
|
451
464
|
|
|
452
465
|
started = time.perf_counter()
|
|
453
466
|
problems: list[str] = []
|
|
454
|
-
|
|
467
|
+
answered: dict[str, str] = {}
|
|
468
|
+
skipped: dict[str, str] = {}
|
|
469
|
+
kinds = {name: self.op_kinds.get(name, OpKind()) for name in self.safe_ops}
|
|
470
|
+
# Motions first, controls after — a stop follows something that moved.
|
|
471
|
+
ordered = sorted(self.safe_ops, key=lambda name: kinds[name].kind == "control")
|
|
472
|
+
moved = False
|
|
473
|
+
for name in ordered:
|
|
474
|
+
kind = kinds[name]
|
|
475
|
+
if kind.kind == "configure":
|
|
476
|
+
skipped[name] = "configure: changes a setting that outlives the call; never called"
|
|
477
|
+
continue
|
|
478
|
+
if kind.kind == "control" and not moved:
|
|
479
|
+
skipped[name] = "control: no motion operation is allow-listed to act on"
|
|
480
|
+
continue
|
|
455
481
|
try:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
482
|
+
if kind.kind == "motion":
|
|
483
|
+
answer = self._motion_over_the_wire(client, name, kind)
|
|
484
|
+
moved = True
|
|
485
|
+
else:
|
|
486
|
+
answer = self._timed_call(f"op:{name}", client.call, name)
|
|
487
|
+
# A lifecycle or control operation is done once, whatever
|
|
488
|
+
# the host tier's count: it homes, resets or stops.
|
|
489
|
+
if kind.kind in ("read", "acquire"):
|
|
490
|
+
for _ in range(self._repeat(f"op:{name}") - 1):
|
|
491
|
+
self._timed_call(f"op:{name}", client.call, name)
|
|
459
492
|
except Exception as exc: # noqa: BLE001 - the reason is the result
|
|
460
493
|
problems.append(f"{name}: raised {type(exc).__name__}: {exc}")
|
|
461
494
|
continue
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
495
|
+
# A call that came back is the whole claim of this tier: the
|
|
496
|
+
# transport carried the request and its answer. What the answer
|
|
497
|
+
# is shaped like is the host tier's business — it demands a dict
|
|
498
|
+
# of a schema operation and nothing of an @expose_to_api method,
|
|
499
|
+
# and describe() flattens both into one list, so demanding a
|
|
500
|
+
# dict here failed acquire (None) and get_recent_file (str) on a
|
|
501
|
+
# module whose experiment consumes exactly those (lib#38).
|
|
502
|
+
answered[name] = type(answer).__name__
|
|
503
|
+
self._called.add(name)
|
|
466
504
|
|
|
467
505
|
elapsed = time.perf_counter() - started
|
|
468
506
|
if problems:
|
|
469
507
|
return GateResult("functions", FAIL, elapsed, error="; ".join(problems))
|
|
470
|
-
|
|
508
|
+
evidence: dict[str, Any] = {"called": sorted(self._called), "answered": answered}
|
|
509
|
+
if skipped:
|
|
510
|
+
evidence["skipped"] = skipped
|
|
511
|
+
return GateResult("functions", PASS, elapsed, evidence=evidence)
|
|
512
|
+
|
|
513
|
+
def _motion_over_the_wire(self, client: Any, name: str, kind: OpKind) -> Any:
|
|
514
|
+
"""Drive a motion one step out and back, as many times as the host tier did.
|
|
515
|
+
|
|
516
|
+
The host tier's count for a motion is out-and-back pairs, so half of
|
|
517
|
+
it is the number of pairs to make here; at least one.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
client: A connected client.
|
|
521
|
+
name: The motion operation.
|
|
522
|
+
kind: Its declaration — ``position_key``, ``target`` and ``step``
|
|
523
|
+
must all be known.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
The answer to the last call.
|
|
527
|
+
|
|
528
|
+
Raises:
|
|
529
|
+
ValueError: If the declaration is incomplete.
|
|
530
|
+
"""
|
|
531
|
+
if kind.position_key is None or kind.target is None or kind.step is None:
|
|
532
|
+
missing = [f for f in ("position_key", "target", "step") if getattr(kind, f) is None]
|
|
533
|
+
raise ValueError(
|
|
534
|
+
f"motion operation '{name}' declares no {'/'.join(missing)};"
|
|
535
|
+
" the client tier will not guess how far a part may move."
|
|
536
|
+
)
|
|
537
|
+
start = float(
|
|
538
|
+
self._timed_call(f"query:{kind.position_key}", client.query, kind.position_key)
|
|
539
|
+
)
|
|
540
|
+
if kind.relative:
|
|
541
|
+
out, back = {kind.target: kind.step}, {kind.target: -kind.step}
|
|
542
|
+
else:
|
|
543
|
+
out, back = {kind.target: start + kind.step}, {kind.target: start}
|
|
544
|
+
pairs = max(1, self._repeat(f"op:{name}") // 2)
|
|
545
|
+
answer: Any = None
|
|
546
|
+
for _ in range(pairs):
|
|
547
|
+
self._timed_call(f"op:{name}", lambda: client.call(name, **out))
|
|
548
|
+
answer = self._timed_call(f"op:{name}", lambda: client.call(name, **back))
|
|
549
|
+
return answer
|
|
471
550
|
|
|
472
551
|
def test_session_survives_a_bad_result(self, client: Any) -> GateResult:
|
|
473
552
|
"""A call whose result JSON cannot carry must not end the session.
|