testerkit 0.4.0__py3-none-any.whl
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.
- testerkit/__init__.py +68 -0
- testerkit/_docs/_assets/operator-ui/channels/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/dashboard/runs.png +0 -0
- testerkit/_docs/_assets/operator-ui/dashboard/stations.png +0 -0
- testerkit/_docs/_assets/operator-ui/events/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/events/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/chart.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/plot-controls.png +0 -0
- testerkit/_docs/_assets/operator-ui/files/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/launch/form.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/pareto.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/ppk.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/yield.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-header.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-measurements.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-overview.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-steps.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/stats.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/designer.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/docs.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/fixtures.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/instruments.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/parts.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/profiles.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/stations.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/tests.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/uuts.png +0 -0
- testerkit/_docs/concepts/configuration/capabilities.md +447 -0
- testerkit/_docs/concepts/configuration/fixtures.md +335 -0
- testerkit/_docs/concepts/configuration/index.md +14 -0
- testerkit/_docs/concepts/configuration/parts.md +276 -0
- testerkit/_docs/concepts/configuration/stations.md +214 -0
- testerkit/_docs/concepts/data/data-stores.md +199 -0
- testerkit/_docs/concepts/data/event-log.md +185 -0
- testerkit/_docs/concepts/data/event-sourcing.md +76 -0
- testerkit/_docs/concepts/data/flight-streaming.md +58 -0
- testerkit/_docs/concepts/data/index.md +17 -0
- testerkit/_docs/concepts/data/sessions.md +78 -0
- testerkit/_docs/concepts/data/three-verbs.md +260 -0
- testerkit/_docs/concepts/execution/index.md +13 -0
- testerkit/_docs/concepts/execution/outcomes.md +199 -0
- testerkit/_docs/concepts/execution/step-hierarchy.md +170 -0
- testerkit/_docs/concepts/execution/step-manifest.md +115 -0
- testerkit/_docs/concepts/index.md +40 -0
- testerkit/_docs/concepts/overview/ai-integration.md +57 -0
- testerkit/_docs/concepts/overview/architecture.md +357 -0
- testerkit/_docs/concepts/overview/index.md +14 -0
- testerkit/_docs/concepts/overview/platform-vs-framework.md +147 -0
- testerkit/_docs/concepts/overview/pytest.md +53 -0
- testerkit/_docs/concepts/overview/tiers.md +46 -0
- testerkit/_docs/how-to/catalog/datasheet-to-test.md +169 -0
- testerkit/_docs/how-to/catalog/index.md +11 -0
- testerkit/_docs/how-to/configuration/configuring-stations.md +364 -0
- testerkit/_docs/how-to/configuration/custom-drivers.md +484 -0
- testerkit/_docs/how-to/configuration/index.md +13 -0
- testerkit/_docs/how-to/configuration/mock-mode.md +342 -0
- testerkit/_docs/how-to/data/benchmarking.md +120 -0
- testerkit/_docs/how-to/data/capture-an-artifact.md +84 -0
- testerkit/_docs/how-to/data/capture-waveform.md +110 -0
- testerkit/_docs/how-to/data/choosing-a-channel-verb.md +108 -0
- testerkit/_docs/how-to/data/compare-runs.md +120 -0
- testerkit/_docs/how-to/data/export-results.md +104 -0
- testerkit/_docs/how-to/data/find-flaky-tests.md +108 -0
- testerkit/_docs/how-to/data/grafana-dashboards.md +148 -0
- testerkit/_docs/how-to/data/index.md +23 -0
- testerkit/_docs/how-to/data/mcp-debug-failures.md +125 -0
- testerkit/_docs/how-to/data/mcp-query-runs.md +120 -0
- testerkit/_docs/how-to/data/querying-channels.md +117 -0
- testerkit/_docs/how-to/data/querying-events.md +85 -0
- testerkit/_docs/how-to/data/stream-live-channel.md +134 -0
- testerkit/_docs/how-to/execution/custom-operator-ui.md +136 -0
- testerkit/_docs/how-to/execution/index.md +21 -0
- testerkit/_docs/how-to/execution/limits.md +193 -0
- testerkit/_docs/how-to/execution/managing-sessions.md +100 -0
- testerkit/_docs/how-to/execution/multi-uut-testing.md +211 -0
- testerkit/_docs/how-to/execution/operator-prompts.md +151 -0
- testerkit/_docs/how-to/execution/profiles.md +304 -0
- testerkit/_docs/how-to/execution/spec-driven-testing.md +169 -0
- testerkit/_docs/how-to/execution/test-context.md +212 -0
- testerkit/_docs/how-to/execution/traceability.md +252 -0
- testerkit/_docs/how-to/execution/vector-expansion.md +394 -0
- testerkit/_docs/how-to/execution/writing-tests.md +363 -0
- testerkit/_docs/how-to/index.md +59 -0
- testerkit/_docs/how-to/overview/index.md +11 -0
- testerkit/_docs/how-to/overview/mcp-integration.md +168 -0
- testerkit/_docs/how-to/overview/operator-ui-tour.md +234 -0
- testerkit/_docs/integration/data/grafana.md +162 -0
- testerkit/_docs/integration/data/index.md +15 -0
- testerkit/_docs/integration/data/lakehouse-import.md +226 -0
- testerkit/_docs/integration/data/logging.md +131 -0
- testerkit/_docs/integration/data/results-api.md +51 -0
- testerkit/_docs/integration/index.md +20 -0
- testerkit/_docs/integration/runtime/harness.md +281 -0
- testerkit/_docs/integration/runtime/index.md +13 -0
- testerkit/_docs/integration/runtime/instruments.md +284 -0
- testerkit/_docs/integration/runtime/pytest-existing.md +315 -0
- testerkit/_docs/reference/catalog/cookbook.md +389 -0
- testerkit/_docs/reference/catalog/index.md +11 -0
- testerkit/_docs/reference/catalog/schema.md +375 -0
- testerkit/_docs/reference/cli.md +660 -0
- testerkit/_docs/reference/configuration.md +406 -0
- testerkit/_docs/reference/data/channels-schema.md +145 -0
- testerkit/_docs/reference/data/event-types.md +554 -0
- testerkit/_docs/reference/data/events-schema.md +120 -0
- testerkit/_docs/reference/data/files-schema.md +141 -0
- testerkit/_docs/reference/data/index.md +18 -0
- testerkit/_docs/reference/data/models.md +2229 -0
- testerkit/_docs/reference/data/outputs.md +82 -0
- testerkit/_docs/reference/data/parquet-schema.md +529 -0
- testerkit/_docs/reference/data/performance-limits.md +224 -0
- testerkit/_docs/reference/data/query-api.md +247 -0
- testerkit/_docs/reference/index.md +69 -0
- testerkit/_docs/reference/operator-ui/channels/detail.md +82 -0
- testerkit/_docs/reference/operator-ui/channels/list.md +70 -0
- testerkit/_docs/reference/operator-ui/dashboard.md +87 -0
- testerkit/_docs/reference/operator-ui/designer.md +167 -0
- testerkit/_docs/reference/operator-ui/events.md +89 -0
- testerkit/_docs/reference/operator-ui/files.md +74 -0
- testerkit/_docs/reference/operator-ui/fixtures.md +100 -0
- testerkit/_docs/reference/operator-ui/instruments.md +126 -0
- testerkit/_docs/reference/operator-ui/launch.md +79 -0
- testerkit/_docs/reference/operator-ui/live.md +102 -0
- testerkit/_docs/reference/operator-ui/measurements.md +159 -0
- testerkit/_docs/reference/operator-ui/metrics.md +217 -0
- testerkit/_docs/reference/operator-ui/parts.md +98 -0
- testerkit/_docs/reference/operator-ui/profiles.md +103 -0
- testerkit/_docs/reference/operator-ui/results/detail.md +214 -0
- testerkit/_docs/reference/operator-ui/results/list.md +106 -0
- testerkit/_docs/reference/operator-ui/stations.md +98 -0
- testerkit/_docs/reference/operator-ui/tests.md +173 -0
- testerkit/_docs/reference/operator-ui/uuts.md +67 -0
- testerkit/_docs/reference/overview/index.md +11 -0
- testerkit/_docs/reference/overview/pytest-native.md +142 -0
- testerkit/_docs/reference/overview/skills.md +177 -0
- testerkit/_docs/reference/pytest/fixtures.md +342 -0
- testerkit/_docs/reference/pytest/index.md +12 -0
- testerkit/_docs/reference/pytest/markers.md +257 -0
- testerkit/_docs/reference/runtime/api.md +246 -0
- testerkit/_docs/reference/runtime/client.md +280 -0
- testerkit/_docs/reference/runtime/connect.md +140 -0
- testerkit/_docs/reference/runtime/index.md +13 -0
- testerkit/_docs/tutorial/01-first-test.md +113 -0
- testerkit/_docs/tutorial/02-mock-instruments.md +88 -0
- testerkit/_docs/tutorial/03-fixtures.md +169 -0
- testerkit/_docs/tutorial/04-limits.md +139 -0
- testerkit/_docs/tutorial/05-configuration.md +194 -0
- testerkit/_docs/tutorial/06-specifications.md +273 -0
- testerkit/_docs/tutorial/07-real-instruments.md +245 -0
- testerkit/_docs/tutorial/08-capabilities.md +223 -0
- testerkit/_docs/tutorial/09-production.md +374 -0
- testerkit/_docs/tutorial/10-live-monitoring.md +156 -0
- testerkit/_docs/tutorial/11-waveforms-and-evidence.md +169 -0
- testerkit/_docs/tutorial/12-continuous-monitoring.md +145 -0
- testerkit/_docs/tutorial/13-parallel-testing.md +233 -0
- testerkit/_docs/tutorial/index.md +61 -0
- testerkit/_docs/tutorial/quickstart.md +282 -0
- testerkit/analysis/__init__.py +9 -0
- testerkit/analysis/_common.py +24 -0
- testerkit/analysis/measurement_facets.py +524 -0
- testerkit/analysis/measurements_query.py +1458 -0
- testerkit/analysis/metrics.py +389 -0
- testerkit/analysis/runs_query.py +434 -0
- testerkit/analysis/steps_query.py +413 -0
- testerkit/api/__init__.py +6 -0
- testerkit/api/_mime.py +71 -0
- testerkit/api/app.py +1171 -0
- testerkit/api/dialogs/__init__.py +36 -0
- testerkit/api/dialogs/manager.py +617 -0
- testerkit/api/dialogs/models.py +79 -0
- testerkit/api/models.py +80 -0
- testerkit/api/responses.py +205 -0
- testerkit/api/runner.py +222 -0
- testerkit/api/schemas.py +311 -0
- testerkit/benchmark/__init__.py +34 -0
- testerkit/benchmark/concurrency.py +248 -0
- testerkit/benchmark/core.py +232 -0
- testerkit/benchmark/runner.py +628 -0
- testerkit/benchmark/scenario.py +313 -0
- testerkit/benchmark/system.py +191 -0
- testerkit/benchmark/workloads.py +507 -0
- testerkit/catalog/__init__.py +6 -0
- testerkit/catalog/generic/generic_dmm.yaml +61 -0
- testerkit/catalog/generic/generic_eload.yaml +31 -0
- testerkit/catalog/generic/generic_fgen.yaml +56 -0
- testerkit/catalog/generic/generic_oscilloscope.yaml +45 -0
- testerkit/catalog/generic/generic_psu.yaml +77 -0
- testerkit/catalog/generic/generic_smu.yaml +73 -0
- testerkit/channels.py +538 -0
- testerkit/cli/__init__.py +32 -0
- testerkit/cli/_common.py +113 -0
- testerkit/cli/_time.py +179 -0
- testerkit/cli/benchmark_cmd.py +56 -0
- testerkit/cli/catalog_cmd.py +39 -0
- testerkit/cli/daemon.py +157 -0
- testerkit/cli/data_cmd.py +1040 -0
- testerkit/cli/discover_cmd.py +94 -0
- testerkit/cli/docs_cmd.py +100 -0
- testerkit/cli/instrument.py +172 -0
- testerkit/cli/mcp_cmd.py +37 -0
- testerkit/cli/metrics.py +366 -0
- testerkit/cli/project.py +312 -0
- testerkit/cli/root.py +14 -0
- testerkit/cli/runs.py +412 -0
- testerkit/cli/schema_cmd.py +87 -0
- testerkit/cli/serve_cmd.py +50 -0
- testerkit/cli/setup_cmd.py +604 -0
- testerkit/cli/station.py +294 -0
- testerkit/cli/validate.py +112 -0
- testerkit/client.py +463 -0
- testerkit/connect.py +635 -0
- testerkit/data/__init__.py +8 -0
- testerkit/data/_accumulator_pool.py +455 -0
- testerkit/data/_atomic.py +59 -0
- testerkit/data/_collection_indices.py +67 -0
- testerkit/data/_daemon_lifecycle.py +535 -0
- testerkit/data/_duckdb_daemon.py +695 -0
- testerkit/data/_duckdb_flight_server.py +813 -0
- testerkit/data/_event_filters.py +15 -0
- testerkit/data/_event_reader.py +101 -0
- testerkit/data/_flight_errors.py +199 -0
- testerkit/data/_flight_query.py +202 -0
- testerkit/data/_flight_retry.py +148 -0
- testerkit/data/_flight_subscribe.py +67 -0
- testerkit/data/_index_epoch.py +342 -0
- testerkit/data/_ipc_writer.py +185 -0
- testerkit/data/_json_safe.py +63 -0
- testerkit/data/_process.py +18 -0
- testerkit/data/_push_relay.py +124 -0
- testerkit/data/_runs_duckdb_daemon.py +2704 -0
- testerkit/data/_session_reaper.py +110 -0
- testerkit/data/_sql_helpers.py +30 -0
- testerkit/data/_store.py +24 -0
- testerkit/data/backends/__init__.py +7 -0
- testerkit/data/backends/_event_accumulator.py +957 -0
- testerkit/data/backends/_row_helpers.py +1172 -0
- testerkit/data/backends/parquet.py +1143 -0
- testerkit/data/backends/protocol.py +35 -0
- testerkit/data/channels/__init__.py +8 -0
- testerkit/data/channels/_flight_daemon.py +75 -0
- testerkit/data/channels/client.py +242 -0
- testerkit/data/channels/flight_manager.py +88 -0
- testerkit/data/channels/index.py +664 -0
- testerkit/data/channels/models.py +418 -0
- testerkit/data/channels/server.py +162 -0
- testerkit/data/channels/store.py +1481 -0
- testerkit/data/data_dir.py +61 -0
- testerkit/data/duckdb_manager.py +79 -0
- testerkit/data/event_log.py +384 -0
- testerkit/data/event_store.py +768 -0
- testerkit/data/events.py +1025 -0
- testerkit/data/exporters/__init__.py +6 -0
- testerkit/data/exporters/_helpers.py +40 -0
- testerkit/data/exporters/csv_exporter.py +168 -0
- testerkit/data/exporters/hdf5.py +234 -0
- testerkit/data/exporters/json_exporter.py +229 -0
- testerkit/data/exporters/mdf4.py +201 -0
- testerkit/data/exporters/stdf.py +255 -0
- testerkit/data/exporters/tdms.py +292 -0
- testerkit/data/files/__init__.py +71 -0
- testerkit/data/files/_backend.py +207 -0
- testerkit/data/files/_catalog_daemon.py +84 -0
- testerkit/data/files/catalog.py +218 -0
- testerkit/data/files/catalog_manager.py +325 -0
- testerkit/data/files/models.py +60 -0
- testerkit/data/files/serializers.py +414 -0
- testerkit/data/files/store.py +463 -0
- testerkit/data/files/streaming.py +783 -0
- testerkit/data/models.py +604 -0
- testerkit/data/ref.py +152 -0
- testerkit/data/retention.py +297 -0
- testerkit/data/run_store.py +367 -0
- testerkit/data/runs_duckdb_manager.py +103 -0
- testerkit/data/schema_dispatch.py +191 -0
- testerkit/data/schema_migrate.py +76 -0
- testerkit/data/schema_versions.py +85 -0
- testerkit/data/schemas.py +288 -0
- testerkit/data/subscribers/__init__.py +48 -0
- testerkit/data/subscribers/_base.py +20 -0
- testerkit/data/subscribers/_output_file.py +25 -0
- testerkit/data/subscribers/replay.py +55 -0
- testerkit/environment.py +84 -0
- testerkit/execution/__init__.py +11 -0
- testerkit/execution/_git.py +190 -0
- testerkit/execution/_state.py +740 -0
- testerkit/execution/accessors.py +54 -0
- testerkit/execution/audit.py +51 -0
- testerkit/execution/cascade.py +85 -0
- testerkit/execution/connections.py +491 -0
- testerkit/execution/harness.py +1931 -0
- testerkit/execution/instrument_events.py +42 -0
- testerkit/execution/limits.py +154 -0
- testerkit/execution/metadata.py +102 -0
- testerkit/execution/mocks.py +63 -0
- testerkit/execution/profiles.py +616 -0
- testerkit/execution/run_scope.py +1331 -0
- testerkit/execution/session_scope.py +196 -0
- testerkit/execution/sidecar.py +280 -0
- testerkit/execution/site_runner.py +705 -0
- testerkit/execution/sites.py +183 -0
- testerkit/execution/sync.py +294 -0
- testerkit/execution/uut_provider.py +231 -0
- testerkit/execution/vectors.py +205 -0
- testerkit/execution/verify.py +396 -0
- testerkit/expand.py +75 -0
- testerkit/files.py +235 -0
- testerkit/fixtures/__init__.py +6 -0
- testerkit/fixtures/manager.py +357 -0
- testerkit/grafana/__init__.py +1 -0
- testerkit/grafana/bootstrap.py +61 -0
- testerkit/grafana/cli.py +314 -0
- testerkit/grafana/dashboards/asset_utilization.json +92 -0
- testerkit/grafana/dashboards/channel_explorer.json +77 -0
- testerkit/grafana/dashboards/event_log.json +92 -0
- testerkit/grafana/dashboards/failure_pareto.json +104 -0
- testerkit/grafana/dashboards/measurement_distribution.json +97 -0
- testerkit/grafana/dashboards/measurement_trend.json +161 -0
- testerkit/grafana/dashboards/station_comparison.json +95 -0
- testerkit/grafana/dashboards/test_duration.json +104 -0
- testerkit/grafana/dashboards/unit_traceability.json +73 -0
- testerkit/grafana/dashboards/yield_overview.json +189 -0
- testerkit/grafana/provisioning/dashboards.yaml.j2 +12 -0
- testerkit/grafana/provisioning/datasources.yaml.j2 +17 -0
- testerkit/grafana/server.py +282 -0
- testerkit/init.py +724 -0
- testerkit/instruments/__init__.py +10 -0
- testerkit/instruments/base.py +82 -0
- testerkit/instruments/discovery/__init__.py +62 -0
- testerkit/instruments/discovery/_base.py +174 -0
- testerkit/instruments/discovery/lxi.py +152 -0
- testerkit/instruments/discovery/ni.py +95 -0
- testerkit/instruments/discovery/serial.py +73 -0
- testerkit/instruments/discovery/visa.py +98 -0
- testerkit/instruments/lifecycle.py +213 -0
- testerkit/instruments/loader.py +79 -0
- testerkit/instruments/locks.py +178 -0
- testerkit/instruments/mocks.py +230 -0
- testerkit/instruments/observer.py +270 -0
- testerkit/instruments/observers/__init__.py +36 -0
- testerkit/instruments/observers/_base.py +68 -0
- testerkit/instruments/observers/daqmx.py +70 -0
- testerkit/instruments/observers/descriptor.py +69 -0
- testerkit/instruments/observers/generic.py +92 -0
- testerkit/instruments/observers/lantz.py +31 -0
- testerkit/instruments/observers/modbus.py +96 -0
- testerkit/instruments/observers/motion.py +68 -0
- testerkit/instruments/observers/ni_modular.py +73 -0
- testerkit/instruments/observers/ophyd.py +80 -0
- testerkit/instruments/observers/pymeasure.py +120 -0
- testerkit/instruments/observers/qcodes.py +66 -0
- testerkit/instruments/observers/scpi.py +88 -0
- testerkit/instruments/observers/tektronix.py +57 -0
- testerkit/instruments/observers/visa.py +103 -0
- testerkit/instruments/pool.py +459 -0
- testerkit/instruments/proxy.py +59 -0
- testerkit/instruments/route_manager.py +329 -0
- testerkit/instruments/routed_proxy.py +88 -0
- testerkit/instruments/server.py +508 -0
- testerkit/instruments/switch.py +43 -0
- testerkit/instruments/visa.py +294 -0
- testerkit/matching/__init__.py +6 -0
- testerkit/matching/service.py +969 -0
- testerkit/mcp/__init__.py +6 -0
- testerkit/mcp/server.py +681 -0
- testerkit/mcp/tools.py +1868 -0
- testerkit/models/__init__.py +18 -0
- testerkit/models/capability.py +598 -0
- testerkit/models/catalog.py +76 -0
- testerkit/models/data_options.py +153 -0
- testerkit/models/enums.py +334 -0
- testerkit/models/instrument.py +152 -0
- testerkit/models/instrument_asset.py +25 -0
- testerkit/models/part.py +271 -0
- testerkit/models/part_manifest.py +96 -0
- testerkit/models/project.py +114 -0
- testerkit/models/station.py +140 -0
- testerkit/models/test_config.py +748 -0
- testerkit/ontology/__init__.py +39 -0
- testerkit/ontology/schema.py +188 -0
- testerkit/ontology/testerkit.yaml +1420 -0
- testerkit/parts/__init__.py +8 -0
- testerkit/parts/context.py +181 -0
- testerkit/parts/folder.py +251 -0
- testerkit/parts/loader.py +27 -0
- testerkit/prompts/__init__.py +17 -0
- testerkit/prompts/core.py +133 -0
- testerkit/pytest_plugin/__init__.py +1287 -0
- testerkit/pytest_plugin/autouse.py +369 -0
- testerkit/pytest_plugin/helpers.py +354 -0
- testerkit/pytest_plugin/hooks.py +1982 -0
- testerkit/pytest_plugin/markers.py +203 -0
- testerkit/pytest_plugin/retry.py +37 -0
- testerkit/pytest_plugin/sweeps.py +117 -0
- testerkit/queries.py +78 -0
- testerkit/reports/__init__.py +6 -0
- testerkit/reports/core.py +492 -0
- testerkit/reports/datasheet.py +831 -0
- testerkit/reports/templates/datasheet.html +383 -0
- testerkit/reports/templates/default.html +153 -0
- testerkit/sbom.py +142 -0
- testerkit/schema_export.py +101 -0
- testerkit/signals.py +63 -0
- testerkit/skills/README.md +58 -0
- testerkit/skills/templates/project-instructions.md +125 -0
- testerkit/skills/testerkit-analysis/SKILL.md +87 -0
- testerkit/skills/testerkit-capture/SKILL.md +114 -0
- testerkit/skills/testerkit-data/SKILL.md +138 -0
- testerkit/skills/testerkit-datasheets/SKILL.md +100 -0
- testerkit/skills/testerkit-datasheets/agents/scaffold-writer.md +115 -0
- testerkit/skills/testerkit-datasheets/agents/section-extractor.md +98 -0
- testerkit/skills/testerkit-datasheets/agents/section-reviewer.md +153 -0
- testerkit/skills/testerkit-datasheets/agents/section-splitter.md +89 -0
- testerkit/skills/testerkit-datasheets/agents/section-writer.md +226 -0
- testerkit/skills/testerkit-datasheets/references/catalog-pipeline.md +271 -0
- testerkit/skills/testerkit-datasheets/references/process-queue.md +42 -0
- testerkit/skills/testerkit-datasheets/references/scaffold.md +156 -0
- testerkit/skills/testerkit-datasheets/references/test-pipeline.md +312 -0
- testerkit/skills/testerkit-debug/SKILL.md +127 -0
- testerkit/skills/testerkit-interactive/SKILL.md +188 -0
- testerkit/skills/testerkit-interactive/references/live-ui-patterns.md +102 -0
- testerkit/skills/testerkit-mocks/SKILL.md +128 -0
- testerkit/skills/testerkit-parts/SKILL.md +126 -0
- testerkit/skills/testerkit-profiles/SKILL.md +139 -0
- testerkit/skills/testerkit-sites/SKILL.md +140 -0
- testerkit/skills/testerkit-stations/SKILL.md +146 -0
- testerkit/skills/testerkit-tests/SKILL.md +136 -0
- testerkit/store.py +1639 -0
- testerkit/ui/__init__.py +71 -0
- testerkit/ui/_asgi.py +162 -0
- testerkit/ui/app.py +32 -0
- testerkit/ui/components/__init__.py +13 -0
- testerkit/ui/components/artifact_viewer.py +250 -0
- testerkit/ui/components/channel_values.py +159 -0
- testerkit/ui/components/event_timeline.py +222 -0
- testerkit/ui/components/execution_gantt.py +210 -0
- testerkit/ui/components/file_streams.py +147 -0
- testerkit/ui/components/instrument_activity.py +146 -0
- testerkit/ui/components/session_table.py +118 -0
- testerkit/ui/pages/__init__.py +25 -0
- testerkit/ui/pages/channels/__init__.py +4 -0
- testerkit/ui/pages/channels/detail.py +932 -0
- testerkit/ui/pages/channels/list.py +515 -0
- testerkit/ui/pages/dashboard.py +236 -0
- testerkit/ui/pages/designer/__init__.py +5 -0
- testerkit/ui/pages/designer/graph.py +635 -0
- testerkit/ui/pages/designer/matching.py +663 -0
- testerkit/ui/pages/designer/page.py +593 -0
- testerkit/ui/pages/designer/properties.py +591 -0
- testerkit/ui/pages/designer/state.py +468 -0
- testerkit/ui/pages/docs/__init__.py +6 -0
- testerkit/ui/pages/docs/index.py +95 -0
- testerkit/ui/pages/docs/page.py +698 -0
- testerkit/ui/pages/events/__init__.py +3 -0
- testerkit/ui/pages/events/list.py +263 -0
- testerkit/ui/pages/explore.py +1108 -0
- testerkit/ui/pages/files/__init__.py +4 -0
- testerkit/ui/pages/files/detail.py +426 -0
- testerkit/ui/pages/files/list.py +447 -0
- testerkit/ui/pages/fixtures/__init__.py +8 -0
- testerkit/ui/pages/fixtures/detail.py +280 -0
- testerkit/ui/pages/fixtures/edit.py +370 -0
- testerkit/ui/pages/fixtures/list.py +180 -0
- testerkit/ui/pages/fixtures/new.py +195 -0
- testerkit/ui/pages/instruments/__init__.py +4 -0
- testerkit/ui/pages/instruments/detail.py +320 -0
- testerkit/ui/pages/instruments/edit.py +482 -0
- testerkit/ui/pages/instruments/list.py +249 -0
- testerkit/ui/pages/instruments/new.py +200 -0
- testerkit/ui/pages/launch.py +261 -0
- testerkit/ui/pages/live.py +124 -0
- testerkit/ui/pages/metrics_page.py +1229 -0
- testerkit/ui/pages/parts/__init__.py +11 -0
- testerkit/ui/pages/parts/detail.py +202 -0
- testerkit/ui/pages/parts/edit.py +310 -0
- testerkit/ui/pages/parts/list.py +160 -0
- testerkit/ui/pages/parts/new.py +170 -0
- testerkit/ui/pages/parts/requirements.py +153 -0
- testerkit/ui/pages/parts/stations.py +199 -0
- testerkit/ui/pages/profiles/__init__.py +4 -0
- testerkit/ui/pages/profiles/detail.py +113 -0
- testerkit/ui/pages/profiles/list.py +86 -0
- testerkit/ui/pages/results/__init__.py +4 -0
- testerkit/ui/pages/results/detail.py +631 -0
- testerkit/ui/pages/results/list.py +230 -0
- testerkit/ui/pages/stations/__init__.py +4 -0
- testerkit/ui/pages/stations/detail.py +256 -0
- testerkit/ui/pages/stations/edit.py +273 -0
- testerkit/ui/pages/stations/list.py +180 -0
- testerkit/ui/pages/stations/new.py +311 -0
- testerkit/ui/pages/tests/__init__.py +4 -0
- testerkit/ui/pages/tests/detail.py +152 -0
- testerkit/ui/pages/tests/list.py +194 -0
- testerkit/ui/pages/uuts/__init__.py +4 -0
- testerkit/ui/pages/uuts/list.py +97 -0
- testerkit/ui/shared/__init__.py +8 -0
- testerkit/ui/shared/components.py +1826 -0
- testerkit/ui/shared/dialogs.py +105 -0
- testerkit/ui/shared/event_binding.py +194 -0
- testerkit/ui/shared/layout.py +314 -0
- testerkit/ui/shared/services.py +1719 -0
- testerkit/ui/shared/timestamps.py +21 -0
- testerkit/ui/static/branding/favicon-16.png +0 -0
- testerkit/ui/static/branding/favicon-32.png +0 -0
- testerkit/ui/static/branding/favicon-48.png +0 -0
- testerkit/ui/static/branding/testerkit-icon-256.png +0 -0
- testerkit/ui/static/branding/testerkit-mark.svg +1 -0
- testerkit/ui/static/branding/testerkit-wordmark.svg +1 -0
- testerkit/ui/static/global.css +465 -0
- testerkit/utils/__init__.py +17 -0
- testerkit/utils/enum_meta.py +800 -0
- testerkit/utils/paths.py +75 -0
- testerkit/utils/ranges.py +257 -0
- testerkit/validation.py +82 -0
- testerkit/verbs.py +142 -0
- testerkit-0.4.0.dist-info/METADATA +250 -0
- testerkit-0.4.0.dist-info/RECORD +521 -0
- testerkit-0.4.0.dist-info/WHEEL +4 -0
- testerkit-0.4.0.dist-info/entry_points.txt +5 -0
- testerkit-0.4.0.dist-info/licenses/LICENSE +191 -0
testerkit/utils/paths.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Centralized search path management for TesterKit resources.
|
|
2
|
+
|
|
3
|
+
This module provides a single source of truth for where different
|
|
4
|
+
resource types (parts, stations, instruments, etc.) are located.
|
|
5
|
+
|
|
6
|
+
All paths are relative to the project root (defaults to cwd).
|
|
7
|
+
Run the server from the project directory: `cd myproject && testerkit serve`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ResourceType(StrEnum):
|
|
17
|
+
"""Types of resources in the TesterKit ecosystem."""
|
|
18
|
+
|
|
19
|
+
PARTS = "parts" # Part YAML files
|
|
20
|
+
STATIONS = "stations" # Station configurations
|
|
21
|
+
INSTRUMENTS = "instruments" # Instrument library definitions
|
|
22
|
+
FIXTURES = "fixtures" # Test fixture definitions
|
|
23
|
+
TESTS = "tests" # Test configurations
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_root(project_root: Path | None = None) -> Path:
|
|
27
|
+
"""Resolve project root, defaulting to cwd."""
|
|
28
|
+
return project_root if project_root is not None else Path.cwd()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_search_paths(
|
|
32
|
+
resource_type: ResourceType,
|
|
33
|
+
project_root: Path | None = None,
|
|
34
|
+
) -> list[Path]:
|
|
35
|
+
"""Get search paths for a resource type.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
resource_type: Type of resource to find paths for.
|
|
39
|
+
project_root: Project root directory. Defaults to cwd.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List containing the single path for this resource type, if it exists.
|
|
43
|
+
"""
|
|
44
|
+
path = _resolve_root(project_root) / resource_type.value
|
|
45
|
+
if path.is_dir():
|
|
46
|
+
return [path]
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_all_search_paths(
|
|
51
|
+
project_root: Path | None = None,
|
|
52
|
+
) -> dict[ResourceType, list[Path]]:
|
|
53
|
+
"""Get search paths for all resource types."""
|
|
54
|
+
return {rt: get_search_paths(rt, project_root) for rt in ResourceType}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Convenience aliases
|
|
58
|
+
def get_part_paths(project_root: Path | None = None) -> list[Path]:
|
|
59
|
+
"""Get search paths for part folders."""
|
|
60
|
+
return get_search_paths(ResourceType.PARTS, project_root)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_station_paths(project_root: Path | None = None) -> list[Path]:
|
|
64
|
+
"""Get search paths for station configurations."""
|
|
65
|
+
return get_search_paths(ResourceType.STATIONS, project_root)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_instrument_paths(project_root: Path | None = None) -> list[Path]:
|
|
69
|
+
"""Get search paths for instrument library."""
|
|
70
|
+
return get_search_paths(ResourceType.INSTRUMENTS, project_root)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_fixture_paths(project_root: Path | None = None) -> list[Path]:
|
|
74
|
+
"""Get search paths for test fixtures."""
|
|
75
|
+
return get_search_paths(ResourceType.FIXTURES, project_root)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Range expansion utilities for pins, channels, and numeric values.
|
|
2
|
+
|
|
3
|
+
Supports SCPI-style inclusive ranges (hardware industry standard):
|
|
4
|
+
- "1:4" → [1, 2, 3, 4] (inclusive, unlike Python)
|
|
5
|
+
- "GPIO[0:7]" → ["GPIO0", "GPIO1", ..., "GPIO7"]
|
|
6
|
+
- "1,3:5,8" → [1, 3, 4, 5, 8] (non-contiguous)
|
|
7
|
+
- "GPIO[0,2,4:6]" → ["GPIO0", "GPIO2", "GPIO4", "GPIO5", "GPIO6"]
|
|
8
|
+
- "-40:125:55" → [-40, 15, 70, 125] (numeric with step)
|
|
9
|
+
- "0.1:0.5:0.1" → [0.1, 0.2, 0.3, 0.4, 0.5] (float with step)
|
|
10
|
+
|
|
11
|
+
Prior Art:
|
|
12
|
+
- SCPI (IEEE 488.2): (@1:4) → 1,2,3,4 (inclusive)
|
|
13
|
+
- Verilog/VHDL: [7:0] → 8-bit bus (inclusive)
|
|
14
|
+
- NI DAQmx: ai0:3 → 4 channels (inclusive)
|
|
15
|
+
- Python slice: [0:4] → 0,1,2,3 (exclusive - different!)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def expand_range(spec: str | list | int) -> list[str]:
|
|
24
|
+
"""Expand a range specification to a list of string items.
|
|
25
|
+
|
|
26
|
+
Handles three input types:
|
|
27
|
+
1. List: Pass through, converting items to strings
|
|
28
|
+
2. String with prefix[range]: Expand prefix to each number (GPIO[0:2] → GPIO0, GPIO1, GPIO2)
|
|
29
|
+
3. String with numeric range: Expand numbers (1:4 → 1, 2, 3, 4)
|
|
30
|
+
4. Single value: Return as single-item list
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
spec: Range string, list of items, or single value
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of expanded items as strings
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> expand_range("1:4")
|
|
40
|
+
['1', '2', '3', '4']
|
|
41
|
+
>>> expand_range("GPIO[0:2]")
|
|
42
|
+
['GPIO0', 'GPIO1', 'GPIO2']
|
|
43
|
+
>>> expand_range("GPIO[0,2,4:6]")
|
|
44
|
+
['GPIO0', 'GPIO2', 'GPIO4', 'GPIO5', 'GPIO6']
|
|
45
|
+
>>> expand_range(["A", "B"]) # List pass-through
|
|
46
|
+
['A', 'B']
|
|
47
|
+
>>> expand_range("TP_VOUT") # Single item
|
|
48
|
+
['TP_VOUT']
|
|
49
|
+
>>> expand_range(1) # Single int
|
|
50
|
+
['1']
|
|
51
|
+
"""
|
|
52
|
+
# List: pass through with string conversion
|
|
53
|
+
if isinstance(spec, list):
|
|
54
|
+
return [str(item) for item in spec]
|
|
55
|
+
|
|
56
|
+
# Single int/float: convert to string list
|
|
57
|
+
if isinstance(spec, (int, float)):
|
|
58
|
+
return [str(spec)]
|
|
59
|
+
|
|
60
|
+
# Must be string from here
|
|
61
|
+
if not isinstance(spec, str):
|
|
62
|
+
return [str(spec)]
|
|
63
|
+
|
|
64
|
+
# Check for prefix[range] pattern: GPIO[0:7], CH[1:4], ai[0:15]
|
|
65
|
+
prefix_match = re.match(r"^([A-Za-z_][A-Za-z0-9_.]*)\[(.+)\]$", spec)
|
|
66
|
+
if prefix_match:
|
|
67
|
+
prefix = prefix_match.group(1)
|
|
68
|
+
range_part = prefix_match.group(2)
|
|
69
|
+
numbers = _expand_int_range(range_part)
|
|
70
|
+
return [f"{prefix}{n}" for n in numbers]
|
|
71
|
+
|
|
72
|
+
# Pure numeric range with optional step: 1:4 or 1:10:2 or 1,3:5
|
|
73
|
+
# Must start with digit or negative sign
|
|
74
|
+
if re.match(r"^-?[\d]", spec) and ":" in spec:
|
|
75
|
+
# Check if this looks like a numeric range (no letters except 'e' for scientific notation)
|
|
76
|
+
if re.match(r"^[\d,:\s.\-eE]+$", spec):
|
|
77
|
+
numbers = _expand_int_range(spec)
|
|
78
|
+
return [str(n) for n in numbers]
|
|
79
|
+
|
|
80
|
+
# Comma-separated without colons: 1,3,5
|
|
81
|
+
if "," in spec and ":" not in spec and re.match(r"^[\d,\s.\-]+$", spec):
|
|
82
|
+
numbers = _expand_int_range(spec)
|
|
83
|
+
return [str(n) for n in numbers]
|
|
84
|
+
|
|
85
|
+
# Single item (no range pattern detected)
|
|
86
|
+
return [spec]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _expand_int_range(spec: str) -> list[int]:
|
|
90
|
+
"""Expand comma-separated ranges of integers.
|
|
91
|
+
|
|
92
|
+
Uses inclusive ranges (SCPI-style):
|
|
93
|
+
- "1:4" → [1, 2, 3, 4]
|
|
94
|
+
- "1,3,5" → [1, 3, 5]
|
|
95
|
+
- "1:3,5,7:9" → [1, 2, 3, 5, 7, 8, 9]
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
spec: Range specification string
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of integers
|
|
102
|
+
"""
|
|
103
|
+
result = []
|
|
104
|
+
for part in spec.split(","):
|
|
105
|
+
part = part.strip()
|
|
106
|
+
if ":" in part:
|
|
107
|
+
bounds = part.split(":")
|
|
108
|
+
if len(bounds) == 2:
|
|
109
|
+
# Simple range: 1:4 → 1, 2, 3, 4
|
|
110
|
+
start, end = int(bounds[0]), int(bounds[1])
|
|
111
|
+
step = 1 if start <= end else -1
|
|
112
|
+
result.extend(range(start, end + step, step))
|
|
113
|
+
# Note: start:stop:step format handled by expand_numeric_range for floats
|
|
114
|
+
else:
|
|
115
|
+
result.append(int(part))
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def expand_numeric_range(spec: str | list | int | float) -> list[float]:
|
|
120
|
+
"""Expand a numeric range specification to a list of float values.
|
|
121
|
+
|
|
122
|
+
Supports:
|
|
123
|
+
- Explicit list: [0.1, 0.2, 0.3] → pass through
|
|
124
|
+
- Simple range: "1:4" → [1, 2, 3, 4]
|
|
125
|
+
- Range with step: "-40:125:55" → [-40, 15, 70, 125]
|
|
126
|
+
- Float range: "0.1:0.5:0.1" → [0.1, 0.2, 0.3, 0.4, 0.5]
|
|
127
|
+
- Comma-separated: "3.3,5.0,12.0" → [3.3, 5.0, 12.0]
|
|
128
|
+
- Mixed: "0,0.5:2:0.5,5" → [0, 0.5, 1.0, 1.5, 2.0, 5]
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
spec: Range string or list of values
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of float values
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
>>> expand_numeric_range("-40:125:55")
|
|
138
|
+
[-40.0, 15.0, 70.0, 125.0]
|
|
139
|
+
>>> expand_numeric_range("0.1:0.5:0.1")
|
|
140
|
+
[0.1, 0.2, 0.3, 0.4, 0.5]
|
|
141
|
+
>>> expand_numeric_range([1, 2, 3])
|
|
142
|
+
[1.0, 2.0, 3.0]
|
|
143
|
+
"""
|
|
144
|
+
# List: pass through with float conversion
|
|
145
|
+
if isinstance(spec, list):
|
|
146
|
+
return [float(v) for v in spec]
|
|
147
|
+
|
|
148
|
+
# Single numeric value
|
|
149
|
+
if isinstance(spec, (int, float)):
|
|
150
|
+
return [float(spec)]
|
|
151
|
+
|
|
152
|
+
# String parsing
|
|
153
|
+
if not isinstance(spec, str):
|
|
154
|
+
return [float(spec)]
|
|
155
|
+
|
|
156
|
+
result: list[float] = []
|
|
157
|
+
|
|
158
|
+
for part in spec.split(","):
|
|
159
|
+
part = part.strip()
|
|
160
|
+
if ":" in part:
|
|
161
|
+
bounds = part.split(":")
|
|
162
|
+
if len(bounds) == 2:
|
|
163
|
+
# Simple range: 1:4 → 1, 2, 3, 4 (step=1)
|
|
164
|
+
start = float(bounds[0])
|
|
165
|
+
stop = float(bounds[1])
|
|
166
|
+
step = 1.0 if start <= stop else -1.0
|
|
167
|
+
result.extend(_generate_float_range(start, stop, step))
|
|
168
|
+
elif len(bounds) == 3:
|
|
169
|
+
# Range with step: -40:125:55
|
|
170
|
+
start = float(bounds[0])
|
|
171
|
+
stop = float(bounds[1])
|
|
172
|
+
step = float(bounds[2])
|
|
173
|
+
result.extend(_generate_float_range(start, stop, step))
|
|
174
|
+
else:
|
|
175
|
+
result.append(float(part))
|
|
176
|
+
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def generate_numeric_range(
|
|
181
|
+
start: float,
|
|
182
|
+
stop: float,
|
|
183
|
+
step: float | None = None,
|
|
184
|
+
count: int | None = None,
|
|
185
|
+
) -> list[float]:
|
|
186
|
+
"""Generate a range of float values with step or count.
|
|
187
|
+
|
|
188
|
+
Exactly one of step or count must be provided. Range is inclusive of stop.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
start: Starting value (inclusive)
|
|
192
|
+
stop: Ending value (inclusive)
|
|
193
|
+
step: Step size between values
|
|
194
|
+
count: Number of values to generate (evenly spaced)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of float values from start to stop (inclusive)
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ValueError: If neither step nor count provided, or both provided
|
|
201
|
+
|
|
202
|
+
Examples:
|
|
203
|
+
>>> generate_numeric_range(0, 5, step=1)
|
|
204
|
+
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
|
|
205
|
+
>>> generate_numeric_range(0, 1, count=3)
|
|
206
|
+
[0.0, 0.5, 1.0]
|
|
207
|
+
"""
|
|
208
|
+
if (step is None) == (count is None):
|
|
209
|
+
raise ValueError("Exactly one of 'step' or 'count' must be provided")
|
|
210
|
+
|
|
211
|
+
if step is not None:
|
|
212
|
+
return _generate_float_range(start, stop, step)
|
|
213
|
+
else:
|
|
214
|
+
# count is not None - generate evenly spaced values
|
|
215
|
+
assert count is not None
|
|
216
|
+
if count == 1:
|
|
217
|
+
return [start]
|
|
218
|
+
step_val = (stop - start) / (count - 1)
|
|
219
|
+
return [start + step_val * i for i in range(count)]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _generate_float_range(start: float, stop: float, step: float) -> list[float]:
|
|
223
|
+
"""Generate a range of float values with step, inclusive of stop.
|
|
224
|
+
|
|
225
|
+
Internal helper - use generate_numeric_range() for public API.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
start: Starting value
|
|
229
|
+
stop: Ending value (inclusive)
|
|
230
|
+
step: Step size (must be non-zero)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of float values from start to stop (inclusive)
|
|
234
|
+
"""
|
|
235
|
+
if step == 0:
|
|
236
|
+
raise ValueError("Step cannot be zero")
|
|
237
|
+
|
|
238
|
+
result: list[float] = []
|
|
239
|
+
current = start
|
|
240
|
+
|
|
241
|
+
# Handle direction
|
|
242
|
+
if step > 0:
|
|
243
|
+
while current <= stop + abs(step) * 1e-9: # Small tolerance for float comparison
|
|
244
|
+
result.append(current)
|
|
245
|
+
current += step
|
|
246
|
+
else:
|
|
247
|
+
while current >= stop - abs(step) * 1e-9: # Small tolerance for float comparison
|
|
248
|
+
result.append(current)
|
|
249
|
+
current += step
|
|
250
|
+
|
|
251
|
+
# Ensure we don't overshoot due to float accumulation
|
|
252
|
+
if result and step > 0 and result[-1] > stop + abs(step) * 0.1:
|
|
253
|
+
result.pop()
|
|
254
|
+
elif result and step < 0 and result[-1] < stop - abs(step) * 0.1:
|
|
255
|
+
result.pop()
|
|
256
|
+
|
|
257
|
+
return result
|
testerkit/validation.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""YAML file validation for all TesterKit config types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from testerkit.schema_export import FileType
|
|
12
|
+
from testerkit.store import FILE_LOADERS, detect_file_type
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_yaml(
|
|
16
|
+
path: Path,
|
|
17
|
+
*,
|
|
18
|
+
file_type: FileType | None = None,
|
|
19
|
+
catalog_dir: Path | None = None,
|
|
20
|
+
) -> list[str]:
|
|
21
|
+
"""Validate a single YAML file against its TesterKit schema.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
path: Path to the YAML file.
|
|
25
|
+
file_type: Explicit type to validate as. If None, auto-detects
|
|
26
|
+
from top-level keys.
|
|
27
|
+
catalog_dir: Root catalog directory (needed for catalog inheritance).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of error strings (empty means valid).
|
|
31
|
+
"""
|
|
32
|
+
resolved_type = file_type or detect_file_type(path)
|
|
33
|
+
|
|
34
|
+
if resolved_type is None:
|
|
35
|
+
if not path.exists():
|
|
36
|
+
return [f"File not found: {path}"]
|
|
37
|
+
return ["Could not determine file type from YAML structure"]
|
|
38
|
+
|
|
39
|
+
if resolved_type == "catalog":
|
|
40
|
+
return _validate_catalog(path, catalog_dir)
|
|
41
|
+
|
|
42
|
+
loader = FILE_LOADERS.get(resolved_type)
|
|
43
|
+
if loader is None:
|
|
44
|
+
return [f"Unknown file type: {resolved_type!r}"]
|
|
45
|
+
return _run_loader(loader, path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _run_loader(loader: Callable, path: Path) -> list[str]:
|
|
49
|
+
"""Run a loader and return validation errors."""
|
|
50
|
+
try:
|
|
51
|
+
loader(path)
|
|
52
|
+
return []
|
|
53
|
+
except ValidationError as exc:
|
|
54
|
+
return _format_validation_error(exc)
|
|
55
|
+
except (yaml.YAMLError, OSError, ValueError) as exc:
|
|
56
|
+
return [str(exc)]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _validate_catalog(path: Path, catalog_dir: Path | None) -> list[str]:
|
|
60
|
+
"""Validate a catalog entry using the full loader (handles inheritance)."""
|
|
61
|
+
from testerkit.store import load_catalog_entry
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
load_catalog_entry(path, catalog_dir=catalog_dir)
|
|
65
|
+
return []
|
|
66
|
+
except ValidationError as exc:
|
|
67
|
+
return _format_validation_error(exc)
|
|
68
|
+
except (yaml.YAMLError, OSError, ValueError) as exc:
|
|
69
|
+
return [str(exc)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _format_validation_error(exc: ValidationError) -> list[str]:
|
|
73
|
+
"""Format Pydantic ValidationError into readable strings."""
|
|
74
|
+
errors = []
|
|
75
|
+
for e in exc.errors():
|
|
76
|
+
loc = ".".join(str(x) for x in e["loc"])
|
|
77
|
+
msg = e["msg"]
|
|
78
|
+
typ = e["type"]
|
|
79
|
+
inp = e.get("input")
|
|
80
|
+
inp_str = f", input_value={inp!r}" if inp is not None else ""
|
|
81
|
+
errors.append(f" {loc}\n {msg} [type={typ}{inp_str}]")
|
|
82
|
+
return errors
|
testerkit/verbs.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Top-level verb functions — ``observe``, ``verify``, ``measure``, ``stream``.
|
|
2
|
+
|
|
3
|
+
The four test-author intent verbs are exposed three ways. Pick the
|
|
4
|
+
one that matches how your code is structured:
|
|
5
|
+
|
|
6
|
+
1. **Top-level imports** (recommended — works from any code, not
|
|
7
|
+
only tests)::
|
|
8
|
+
|
|
9
|
+
from testerkit import observe, verify, measure, stream
|
|
10
|
+
|
|
11
|
+
def my_step(dmm, psu, voltage):
|
|
12
|
+
psu.set_voltage(voltage)
|
|
13
|
+
observe("psu.voltage", voltage)
|
|
14
|
+
verify("rail_v", dmm.measure_voltage(), Limit(low=4.75, high=5.25))
|
|
15
|
+
measure("rail_ripple", ripple(dmm.read_waveform())) # record-only
|
|
16
|
+
|
|
17
|
+
2. **Pytest fixtures** (idiomatic when a test signature already
|
|
18
|
+
takes other fixtures)::
|
|
19
|
+
|
|
20
|
+
def test_rail(observe, verify, dmm, psu):
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
3. **Context methods** (programmatic / non-pytest paths)::
|
|
24
|
+
|
|
25
|
+
with TestHarness("my-run") as harness:
|
|
26
|
+
with harness.vector(...) as ctx:
|
|
27
|
+
ctx.observe("...", value)
|
|
28
|
+
ctx.verify("...", measured, limit)
|
|
29
|
+
|
|
30
|
+
All three shapes route through the same underlying
|
|
31
|
+
:class:`~testerkit.execution.harness.Context` methods, so behavior is
|
|
32
|
+
identical regardless of which shape your code reaches for.
|
|
33
|
+
|
|
34
|
+
Resolution chain: the top-level verbs read the active context from
|
|
35
|
+
:func:`testerkit.execution._state.get_current_context`. The pytest
|
|
36
|
+
``context`` fixture pushes a Context onto that ContextVar; calling
|
|
37
|
+
a verb outside a pytest test (notebooks, scripts, custom UIs)
|
|
38
|
+
raises ``RuntimeError``.
|
|
39
|
+
|
|
40
|
+
**Interactive ``connect()`` mode does NOT populate the active
|
|
41
|
+
Context.** ``StationConnection.start`` wires the channel store and
|
|
42
|
+
event store ContextVars so that :func:`testerkit.channels.write` /
|
|
43
|
+
:func:`testerkit.channels.stream` / :func:`testerkit.files.write` /
|
|
44
|
+
:func:`testerkit.files.stream` work — but it deliberately does not
|
|
45
|
+
push a Context, because there is no test vector to anchor
|
|
46
|
+
observations and verifications to in interactive mode. Reach for
|
|
47
|
+
the store-direct surfaces (``channels.*`` / ``files.*``) in
|
|
48
|
+
notebooks, the operator UI, and bringup scripts. The test-author
|
|
49
|
+
verbs are test-author verbs.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
from typing import Any
|
|
55
|
+
|
|
56
|
+
from testerkit.execution._state import get_current_context
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _active_context() -> Any:
|
|
60
|
+
"""Return the active :class:`Context`, or raise with a useful hint."""
|
|
61
|
+
ctx = get_current_context()
|
|
62
|
+
if ctx is None:
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
"No active TesterKit context. The top-level verbs (observe / "
|
|
65
|
+
"verify / measure / stream) are test-author verbs — they require the "
|
|
66
|
+
"pytest ``context`` fixture, which pushes a Context for the "
|
|
67
|
+
"duration of the test. Outside a pytest test (notebooks, "
|
|
68
|
+
"scripts, custom UIs), use the store-direct surfaces instead: "
|
|
69
|
+
"``testerkit.channels.write`` / ``testerkit.channels.stream`` for "
|
|
70
|
+
"channels, ``testerkit.files.write`` / ``testerkit.files.stream`` "
|
|
71
|
+
"for artifacts. Those work inside a ``connect(...)`` block "
|
|
72
|
+
"and don't require an active test context."
|
|
73
|
+
)
|
|
74
|
+
return ctx
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def observe(key: str, value: Any, *, namespace: str | None = None, unit: str | None = None) -> None:
|
|
78
|
+
"""Record an observation (→ output lane in the EAV store).
|
|
79
|
+
|
|
80
|
+
Thin top-level pass-through to
|
|
81
|
+
:meth:`testerkit.execution.harness.Context.observe`. See that method
|
|
82
|
+
for the full polymorphic dispatch rules (scalar / Waveform /
|
|
83
|
+
numeric_array / blob / URI / sink-handle).
|
|
84
|
+
"""
|
|
85
|
+
_active_context().observe(key, value, namespace=namespace, unit=unit)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def verify(
|
|
89
|
+
name: str,
|
|
90
|
+
value: float | int | None,
|
|
91
|
+
limit: Any = None,
|
|
92
|
+
*,
|
|
93
|
+
characteristic: str | None = None,
|
|
94
|
+
namespace: str | None = None,
|
|
95
|
+
unit: str | None = None,
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""Record + judge a measurement (→ measurement row).
|
|
98
|
+
|
|
99
|
+
Thin top-level pass-through to
|
|
100
|
+
:meth:`testerkit.execution.harness.Context.verify`. See that method
|
|
101
|
+
for limit-resolution rules + ``MissingLimitError`` semantics.
|
|
102
|
+
"""
|
|
103
|
+
return _active_context().verify(
|
|
104
|
+
name, value, limit, characteristic=characteristic, namespace=namespace, unit=unit
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def measure(
|
|
109
|
+
name: str,
|
|
110
|
+
value: float | int | None,
|
|
111
|
+
limit: Any = None,
|
|
112
|
+
*,
|
|
113
|
+
characteristic: str | None = None,
|
|
114
|
+
namespace: str | None = None,
|
|
115
|
+
unit: str | None = None,
|
|
116
|
+
) -> Any:
|
|
117
|
+
"""Record a measurement without judging it (→ measurement row).
|
|
118
|
+
|
|
119
|
+
The record-only sibling of ``verify`` — stamps one measurement row
|
|
120
|
+
with ``Outcome.DONE`` and never raises on a missing limit. Use when
|
|
121
|
+
a value should be captured but not pass/fail judged
|
|
122
|
+
(characterization, diagnostics, logged context). Thin top-level
|
|
123
|
+
pass-through to :meth:`testerkit.execution.harness.Context.measure`.
|
|
124
|
+
"""
|
|
125
|
+
return _active_context().measure(
|
|
126
|
+
name, value, limit, characteristic=characteristic, namespace=namespace, unit=unit
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def stream(name: str, sample: Any, *, namespace: str | None = None, unit: str | None = None) -> str:
|
|
131
|
+
"""Append one sample to a channel (→ ``channel://`` URI).
|
|
132
|
+
|
|
133
|
+
Thin top-level pass-through to
|
|
134
|
+
:meth:`testerkit.execution.harness.Context.stream`. Strictly
|
|
135
|
+
orthogonal to ``observe`` — never writes to the outputs lane on
|
|
136
|
+
the active vector; wire to a vector explicitly via
|
|
137
|
+
``observe(name, sink)`` if association is wanted.
|
|
138
|
+
"""
|
|
139
|
+
return _active_context().stream(name, sample, namespace=namespace, unit=unit)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
__all__ = ["observe", "verify", "measure", "stream"]
|