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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Instrument accessors for test functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from testerkit.execution._state import get_current_step_aliases
|
|
8
|
+
from testerkit.models.instrument import InstrumentRecord
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _instrument_not_found(alias: str, target: str, instruments: dict[str, Any]) -> KeyError:
|
|
12
|
+
"""Build a KeyError for a missing instrument, listing available roles."""
|
|
13
|
+
available = ", ".join(sorted(instruments)) or "(none)"
|
|
14
|
+
if alias != target:
|
|
15
|
+
return KeyError(
|
|
16
|
+
f"Alias '{alias}' targets '{target}' which is not in "
|
|
17
|
+
f"station instruments. Available: {available}"
|
|
18
|
+
)
|
|
19
|
+
return KeyError(f"No instrument with role '{alias}'. Available: {available}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InstrumentAccessor:
|
|
23
|
+
"""Callable accessor for instruments by role, with type-based grouping."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, instruments: dict[str, Any], records: dict[str, InstrumentRecord]):
|
|
26
|
+
self._instruments = instruments
|
|
27
|
+
self._records = records
|
|
28
|
+
|
|
29
|
+
def _current_aliases(self) -> dict[str, str]:
|
|
30
|
+
"""Get current step aliases from the active ContextVar."""
|
|
31
|
+
return get_current_step_aliases()
|
|
32
|
+
|
|
33
|
+
def __call__(self, role: str) -> Any:
|
|
34
|
+
"""Get instrument by role name, resolving aliases. Raises KeyError with available roles."""
|
|
35
|
+
aliases = self._current_aliases()
|
|
36
|
+
resolved = aliases.get(role, role)
|
|
37
|
+
if resolved not in self._instruments:
|
|
38
|
+
raise _instrument_not_found(role, resolved, self._instruments)
|
|
39
|
+
return self._instruments[resolved]
|
|
40
|
+
|
|
41
|
+
def by_type(self, driver_path: str) -> dict[str, Any]:
|
|
42
|
+
"""Get all instruments matching a driver class import path."""
|
|
43
|
+
return {
|
|
44
|
+
role: self._instruments[role]
|
|
45
|
+
for role, record in self._records.items()
|
|
46
|
+
if record.driver == driver_path and role in self._instruments
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def roles(self) -> list[str]:
|
|
50
|
+
"""List available instrument role names, including active aliases."""
|
|
51
|
+
aliases = self._current_aliases()
|
|
52
|
+
names = set(self._instruments.keys())
|
|
53
|
+
names.update(aliases.keys())
|
|
54
|
+
return sorted(names)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Runner-neutral traceability audit.
|
|
2
|
+
|
|
3
|
+
Walks the just-completed step's measurements and reports any that
|
|
4
|
+
are missing required traceability fields. Works for any runner that
|
|
5
|
+
records measurements through :class:`RunScope` — pytest, OpenHTF,
|
|
6
|
+
unittest wrappers all share this check.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def audit_traceability(logger_inst: Any, *, strict: bool, spec_active: bool) -> None:
|
|
15
|
+
"""Check the current step's measurements for traceability completeness.
|
|
16
|
+
|
|
17
|
+
Walks measurements recorded during the just-completed test and
|
|
18
|
+
flags any that lack required fields:
|
|
19
|
+
|
|
20
|
+
* ``step_path`` — always required (populated by the runner's step
|
|
21
|
+
wrapper).
|
|
22
|
+
* ``spec_ref`` OR ``uut_pin`` — required only when ``spec_active``
|
|
23
|
+
is True (i.e. a part spec is loaded for the session). Runs
|
|
24
|
+
without a spec exercise the graceful-degradation path and are
|
|
25
|
+
not penalized for lacking pin/spec references.
|
|
26
|
+
|
|
27
|
+
In ``strict`` mode, raises :class:`AssertionError` if any
|
|
28
|
+
measurement is incomplete so the test fails. Otherwise returns
|
|
29
|
+
silently — the caller decides whether to surface the issues.
|
|
30
|
+
"""
|
|
31
|
+
steps = getattr(getattr(logger_inst, "test_run", None), "steps", None)
|
|
32
|
+
if not steps:
|
|
33
|
+
return
|
|
34
|
+
step = steps[-1]
|
|
35
|
+
|
|
36
|
+
incomplete: list[str] = []
|
|
37
|
+
for vec in step.vectors:
|
|
38
|
+
for m in vec.measurements:
|
|
39
|
+
missing: list[str] = []
|
|
40
|
+
if not m.step_path:
|
|
41
|
+
missing.append("step_path")
|
|
42
|
+
if spec_active and not m.spec_ref and not m.uut_pin:
|
|
43
|
+
missing.append("spec_ref/uut_pin")
|
|
44
|
+
if missing:
|
|
45
|
+
incomplete.append(f"{m.name}: missing {', '.join(missing)}")
|
|
46
|
+
|
|
47
|
+
if incomplete and strict:
|
|
48
|
+
raise AssertionError(
|
|
49
|
+
"--strict-traceability: measurements missing required fields:\n "
|
|
50
|
+
+ "\n ".join(incomplete)
|
|
51
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Runner-neutral cascade walk — sidecar + profile → merged :class:`TestEntry`.
|
|
2
|
+
|
|
3
|
+
Re-exports :func:`testerkit.execution.sidecar.merged_test_entry` and adds
|
|
4
|
+
:func:`cascade_for` that combines sidecar lookup, profile lookup, and
|
|
5
|
+
merge into a single call. Each runner's plugin invokes this once per
|
|
6
|
+
test to get the typed :class:`TestEntry` it needs to inject markers
|
|
7
|
+
from.
|
|
8
|
+
|
|
9
|
+
Inputs are runner-neutral (a :class:`SidecarConfig` and the active
|
|
10
|
+
:class:`ProfileConfig`, plus class / function names). Pytest's
|
|
11
|
+
plugin reads the sidecar via ``load_sidecar(module_file)`` and the
|
|
12
|
+
profile via ``get_active_profile()``; another runner reads its
|
|
13
|
+
host's equivalents and calls the same function.
|
|
14
|
+
|
|
15
|
+
Also exposes :func:`find_unmatched_profile_keys` — a profile typo
|
|
16
|
+
detector. A profile that lists ``tests.test_typo`` when no test by
|
|
17
|
+
that name exists in the collected suite is a production-screen
|
|
18
|
+
silent no-op; warn loudly. The runner's plugin gathers the test IDs
|
|
19
|
+
its host collected and asks this module which profile keys map to
|
|
20
|
+
nothing.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Iterable
|
|
26
|
+
|
|
27
|
+
from testerkit.execution.sidecar import _merge_entry_into, merged_test_entry
|
|
28
|
+
from testerkit.models.project import ProfileConfig
|
|
29
|
+
from testerkit.models.test_config import SidecarConfig, TestEntry
|
|
30
|
+
|
|
31
|
+
__all__ = ["cascade_for", "find_unmatched_profile_keys", "merged_test_entry"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cascade_for(
|
|
35
|
+
sidecar: SidecarConfig | None,
|
|
36
|
+
profile: ProfileConfig | None,
|
|
37
|
+
cls_name: str | None,
|
|
38
|
+
func_name: str | None,
|
|
39
|
+
) -> TestEntry:
|
|
40
|
+
"""Build the merged :class:`TestEntry` for one test.
|
|
41
|
+
|
|
42
|
+
Cascade order: sidecar (file → class → leaf) then profile
|
|
43
|
+
(root → class → leaf), with last-wins per field. Either source
|
|
44
|
+
may be ``None``.
|
|
45
|
+
"""
|
|
46
|
+
merged = TestEntry()
|
|
47
|
+
if sidecar is not None:
|
|
48
|
+
_merge_entry_into(merged, merged_test_entry(sidecar, cls_name, func_name))
|
|
49
|
+
if profile is not None:
|
|
50
|
+
_merge_entry_into(merged, merged_test_entry(profile, cls_name, func_name))
|
|
51
|
+
return merged
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_unmatched_profile_keys(
|
|
55
|
+
profile: ProfileConfig,
|
|
56
|
+
test_ids: Iterable[tuple[str | None, str]],
|
|
57
|
+
) -> list[str]:
|
|
58
|
+
"""Return profile.tests keys that match no collected test.
|
|
59
|
+
|
|
60
|
+
``test_ids`` is the iterable of ``(class_name, func_name)`` pairs
|
|
61
|
+
the runner collected. Classless module-level tests pass
|
|
62
|
+
``class_name=None``. Returns formatted ``profile.tests[...]``
|
|
63
|
+
paths in declared order — caller emits a warning if non-empty.
|
|
64
|
+
"""
|
|
65
|
+
func_names: set[str] = set()
|
|
66
|
+
methods_by_class: dict[str, set[str]] = {}
|
|
67
|
+
class_names: set[str] = set()
|
|
68
|
+
for cls_name, func_name in test_ids:
|
|
69
|
+
func_names.add(func_name)
|
|
70
|
+
if cls_name is not None:
|
|
71
|
+
class_names.add(cls_name)
|
|
72
|
+
methods_by_class.setdefault(cls_name, set()).add(func_name)
|
|
73
|
+
|
|
74
|
+
unmatched: list[str] = []
|
|
75
|
+
for key, entry in profile.tests.items():
|
|
76
|
+
if key in class_names:
|
|
77
|
+
for nested_key in entry.tests:
|
|
78
|
+
if nested_key in methods_by_class[key]:
|
|
79
|
+
continue
|
|
80
|
+
unmatched.append(f" profile.tests[{key!r}].tests[{nested_key!r}]")
|
|
81
|
+
continue
|
|
82
|
+
if key in func_names:
|
|
83
|
+
continue
|
|
84
|
+
unmatched.append(f" profile.tests[{key!r}]")
|
|
85
|
+
return unmatched
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""Resolve ``testerkit_characteristics`` / ``testerkit_connections`` markers to fixture connections.
|
|
2
|
+
|
|
3
|
+
The two markers compose into a single ordered list of
|
|
4
|
+
:class:`FixtureConnection` instances that the test body iterates via
|
|
5
|
+
``ctx.connections``. ``testerkit_characteristics`` supplies the
|
|
6
|
+
characteristic list (each char's ``resolved_pins``);
|
|
7
|
+
``testerkit_connections`` supplies an explicit named-connection list or
|
|
8
|
+
an instrument-channel selector. When both are present the connection
|
|
9
|
+
list narrows the union of chars' pin sets; when only one is present
|
|
10
|
+
the resolver derives the iteration set from it directly.
|
|
11
|
+
|
|
12
|
+
Pulled out of ``plugin.py`` so that file stays focused on pytest hook
|
|
13
|
+
registration. The pytest fixture wrapping this resolver
|
|
14
|
+
(``_testerkit_resolve_connections``) lives in ``plugin.py``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import KeysView, Sequence, ValuesView
|
|
20
|
+
from contextvars import Token
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
from testerkit.execution._state import (
|
|
24
|
+
push_active_characteristic,
|
|
25
|
+
push_active_connection,
|
|
26
|
+
reset_active_characteristic,
|
|
27
|
+
reset_active_connection,
|
|
28
|
+
)
|
|
29
|
+
from testerkit.models.test_config import FixtureConfig, FixtureConnection
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
import pytest
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConnectionResolutionError(Exception):
|
|
36
|
+
"""Raised when ``testerkit_characteristics`` / ``testerkit_connections`` cannot resolve.
|
|
37
|
+
|
|
38
|
+
Runner-neutral: the pytest adapter catches this and re-raises as
|
|
39
|
+
``ConnectionResolutionError`` at the fixture boundary; other runners adapt
|
|
40
|
+
to their own user-error type.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConnectionIterator:
|
|
45
|
+
"""Iterable + name-addressable view over the bound :class:`FixtureConnection` list.
|
|
46
|
+
|
|
47
|
+
Built by ``_testerkit_resolve_connections`` from a test's spec/connections markers.
|
|
48
|
+
The instance is iterable (drives the ``_active_connection_var``
|
|
49
|
+
ContextVar so drivers can route to the *current* connection) and
|
|
50
|
+
also supports ``__getitem__`` / ``keys()`` / ``values()`` /
|
|
51
|
+
``__contains__`` for passive name lookup. Iteration is single-use
|
|
52
|
+
and follows insertion order; lookup never pushes the ContextVar.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
connections: list[FixtureConnection],
|
|
58
|
+
conn_to_char: dict[str, str] | None = None,
|
|
59
|
+
_parent: ConnectionIterator | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._connections = connections
|
|
62
|
+
self._by_name: dict[str, FixtureConnection] = {c.name: c for c in connections}
|
|
63
|
+
# Per-connection char lookup, keyed by connection name. Built
|
|
64
|
+
# by ``resolve_test_connections`` from the spec; empty when no
|
|
65
|
+
# chars are in scope (pure ``testerkit_connections`` bringup).
|
|
66
|
+
self._conn_to_char: dict[str, str] = conn_to_char or {}
|
|
67
|
+
# Parent iterator (for ``for_characteristic`` sub-iterators) so
|
|
68
|
+
# iterating the child also marks the parent ``started`` —
|
|
69
|
+
# actual iteration, not iterator construction, is what the
|
|
70
|
+
# consumption guard cares about.
|
|
71
|
+
self._parent: ConnectionIterator | None = _parent
|
|
72
|
+
self._idx = 0
|
|
73
|
+
self._conn_token: Token[FixtureConnection | None] | None = None
|
|
74
|
+
self._char_token: Token[str | None] | None = None
|
|
75
|
+
self.started = False
|
|
76
|
+
|
|
77
|
+
def __iter__(self) -> ConnectionIterator:
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __next__(self) -> FixtureConnection:
|
|
81
|
+
self._reset_tokens()
|
|
82
|
+
if self._idx >= len(self._connections):
|
|
83
|
+
raise StopIteration
|
|
84
|
+
connection = self._connections[self._idx]
|
|
85
|
+
self._idx += 1
|
|
86
|
+
self.started = True
|
|
87
|
+
if self._parent is not None:
|
|
88
|
+
self._parent.started = True
|
|
89
|
+
self._conn_token = push_active_connection(connection)
|
|
90
|
+
char = self._conn_to_char.get(connection.name)
|
|
91
|
+
if char is not None:
|
|
92
|
+
self._char_token = push_active_characteristic(char)
|
|
93
|
+
return connection
|
|
94
|
+
|
|
95
|
+
def __len__(self) -> int:
|
|
96
|
+
return len(self._connections)
|
|
97
|
+
|
|
98
|
+
def __getitem__(self, key: str | int) -> FixtureConnection:
|
|
99
|
+
if isinstance(key, int):
|
|
100
|
+
return self._connections[key]
|
|
101
|
+
return self._by_name[key]
|
|
102
|
+
|
|
103
|
+
def __contains__(self, key: object) -> bool:
|
|
104
|
+
return key in self._by_name
|
|
105
|
+
|
|
106
|
+
def keys(self) -> KeysView[str]:
|
|
107
|
+
return self._by_name.keys()
|
|
108
|
+
|
|
109
|
+
def values(self) -> ValuesView[FixtureConnection]:
|
|
110
|
+
return self._by_name.values()
|
|
111
|
+
|
|
112
|
+
def for_characteristic(self, char_id: str) -> ConnectionIterator:
|
|
113
|
+
"""Return a fresh iterator scoped to connections owned by ``char_id``.
|
|
114
|
+
|
|
115
|
+
Iterates only the subset whose ``conn_to_char`` mapping matches
|
|
116
|
+
``char_id``. Each ``__next__`` pushes the active-char ContextVar
|
|
117
|
+
to ``char_id`` (so ``verify`` stamps the right
|
|
118
|
+
``characteristic_id``) AND marks the parent iterator
|
|
119
|
+
``started`` — so the consumption guard in
|
|
120
|
+
``_testerkit_resolve_connections`` sees real iteration, not just
|
|
121
|
+
iterator construction. Building a sub-iterator and never
|
|
122
|
+
iterating it does NOT count as consumption.
|
|
123
|
+
"""
|
|
124
|
+
subset = [
|
|
125
|
+
conn for conn in self._connections if self._conn_to_char.get(conn.name) == char_id
|
|
126
|
+
]
|
|
127
|
+
return ConnectionIterator(subset, {c.name: char_id for c in subset}, _parent=self)
|
|
128
|
+
|
|
129
|
+
def _reset_tokens(self) -> None:
|
|
130
|
+
if self._char_token is not None:
|
|
131
|
+
reset_active_characteristic(self._char_token)
|
|
132
|
+
self._char_token = None
|
|
133
|
+
if self._conn_token is not None:
|
|
134
|
+
reset_active_connection(self._conn_token)
|
|
135
|
+
self._conn_token = None
|
|
136
|
+
|
|
137
|
+
def cleanup(self) -> None:
|
|
138
|
+
"""Pop any lingering active-connection / active-char tokens on teardown."""
|
|
139
|
+
self._reset_tokens()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _per_char_pins(characteristics: Sequence[str], spec_ctx: Any) -> dict[str, list[str]]:
|
|
143
|
+
"""Return per-char ``resolved_pins`` lists, validating each char.
|
|
144
|
+
|
|
145
|
+
Keys preserve user-listed order; values are each char's pin list.
|
|
146
|
+
Raises if the spec context is missing or a char is unknown.
|
|
147
|
+
"""
|
|
148
|
+
if spec_ctx is None:
|
|
149
|
+
raise ConnectionResolutionError(
|
|
150
|
+
f"testerkit_characteristics({list(characteristics)!r}) "
|
|
151
|
+
"requires a part (load via --part=<id-or-path> / parts/ auto-discovery)."
|
|
152
|
+
)
|
|
153
|
+
out: dict[str, list[str]] = {}
|
|
154
|
+
for char_id in characteristics:
|
|
155
|
+
char = spec_ctx.part.characteristics.get(char_id)
|
|
156
|
+
if char is None:
|
|
157
|
+
raise ConnectionResolutionError(
|
|
158
|
+
f"Characteristic {char_id!r} not found in part {spec_ctx.part.id!r}."
|
|
159
|
+
)
|
|
160
|
+
out[char_id] = list(char.resolved_pins)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _pick_connection_for_pin(
|
|
165
|
+
pin_id: str,
|
|
166
|
+
target_function: Any,
|
|
167
|
+
pin_net: str | None,
|
|
168
|
+
fixture_cfg: FixtureConfig,
|
|
169
|
+
) -> FixtureConnection | None:
|
|
170
|
+
"""Pick the fixture connection that routes ``(pin, function)``.
|
|
171
|
+
|
|
172
|
+
Match priority, highest first:
|
|
173
|
+
|
|
174
|
+
1. **Exact**: char's ``function`` and conn's ``function`` are both set
|
|
175
|
+
and equal — the strongest match.
|
|
176
|
+
2. **Pin-only**: at least one side has no ``function``. Either the
|
|
177
|
+
char doesn't declare what function it needs (backward-compatible
|
|
178
|
+
with pre-function fixtures) or the conn is generic
|
|
179
|
+
(function-unset). The first such conn for the pin wins.
|
|
180
|
+
|
|
181
|
+
Connections where both sides set ``function`` but the values differ
|
|
182
|
+
are skipped — a wired-up DC connection isn't valid for an AC char.
|
|
183
|
+
Match by ``uut_pin`` first; falls back to net match when
|
|
184
|
+
``uut_pin`` is unset on the connection.
|
|
185
|
+
"""
|
|
186
|
+
exact_match: FixtureConnection | None = None
|
|
187
|
+
pin_only_match: FixtureConnection | None = None
|
|
188
|
+
for conn in fixture_cfg.connections.values():
|
|
189
|
+
pin_hit = conn.uut_pin == pin_id or (pin_net is not None and conn.net == pin_net)
|
|
190
|
+
if not pin_hit:
|
|
191
|
+
continue
|
|
192
|
+
if target_function is not None and conn.function == target_function:
|
|
193
|
+
exact_match = exact_match or conn
|
|
194
|
+
elif target_function is None or conn.function is None:
|
|
195
|
+
# At least one side is None — pin-only fallback.
|
|
196
|
+
pin_only_match = pin_only_match or conn
|
|
197
|
+
# else: both sides set but different — skip (e.g. char wants
|
|
198
|
+
# ac_voltage but this conn is dc_voltage-only).
|
|
199
|
+
return exact_match or pin_only_match
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _resolve_chars_to_connections(
|
|
203
|
+
characteristics: Sequence[str],
|
|
204
|
+
spec_ctx: Any,
|
|
205
|
+
fixture_cfg: FixtureConfig,
|
|
206
|
+
) -> tuple[list[FixtureConnection], dict[str, str]]:
|
|
207
|
+
"""Match each char's ``(pin, function)`` against the fixture.
|
|
208
|
+
|
|
209
|
+
Returns a deduplicated connection list (preserving char order, then
|
|
210
|
+
char-pin order) plus a ``conn_name → char_id`` lookup. When the
|
|
211
|
+
same fixture connection is selected by multiple chars, the
|
|
212
|
+
first-listing char owns it (matching marker order).
|
|
213
|
+
|
|
214
|
+
Edge case: when two chars share a pin but neither declares
|
|
215
|
+
``function:`` (or both share the same function), they bind to the
|
|
216
|
+
same connection — only the first-listed char owns it in
|
|
217
|
+
``conn_to_char``. ``ctx.connections.for_characteristic(<later_char>)``
|
|
218
|
+
will return an empty subset for the loser. To avoid this, declare
|
|
219
|
+
distinct ``function:`` values on the chars and the matching
|
|
220
|
+
connections (the per-function tie-break in
|
|
221
|
+
:func:`_pick_connection_for_pin`), or put the chars on different
|
|
222
|
+
pins.
|
|
223
|
+
"""
|
|
224
|
+
matched: list[FixtureConnection] = []
|
|
225
|
+
conn_to_char: dict[str, str] = {}
|
|
226
|
+
seen_names: set[str] = set()
|
|
227
|
+
for char_id in characteristics:
|
|
228
|
+
char = spec_ctx.part.characteristics.get(char_id)
|
|
229
|
+
if char is None:
|
|
230
|
+
continue
|
|
231
|
+
for pin_id in char.resolved_pins:
|
|
232
|
+
pin = spec_ctx.part.pins.get(pin_id)
|
|
233
|
+
net = pin.net if pin else None
|
|
234
|
+
conn = _pick_connection_for_pin(pin_id, char.function, net, fixture_cfg)
|
|
235
|
+
if conn is None or conn.name in seen_names:
|
|
236
|
+
continue
|
|
237
|
+
matched.append(conn)
|
|
238
|
+
conn_to_char[conn.name] = char_id
|
|
239
|
+
seen_names.add(conn.name)
|
|
240
|
+
return matched, conn_to_char
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _resolve_connections_payload(
|
|
244
|
+
mark: Any,
|
|
245
|
+
) -> tuple[list[str] | None, dict[str, Any] | None]:
|
|
246
|
+
"""Return ``(names, instrument_channels)`` from a ``testerkit_connections`` mark.
|
|
247
|
+
|
|
248
|
+
The marker is a shape-discriminated one-of:
|
|
249
|
+
|
|
250
|
+
* Positional list of strings → bind by fixture-connection name
|
|
251
|
+
(``@pytest.mark.testerkit_connections(["vout_measure"])``).
|
|
252
|
+
* Kwargs by instrument name → bind by instrument → channel selectors
|
|
253
|
+
(``@pytest.mark.testerkit_connections(dmm=["ch1"])``).
|
|
254
|
+
|
|
255
|
+
Exactly one shape may appear per marker invocation. Pydantic already
|
|
256
|
+
enforces the one-of at the YAML layer (``TestEntry.connections`` is a
|
|
257
|
+
``list[str] | dict[str, Any]`` discriminated union); this function
|
|
258
|
+
is the parallel guard for the inline-marker call site.
|
|
259
|
+
"""
|
|
260
|
+
args = list(getattr(mark, "args", ()) or ())
|
|
261
|
+
kwargs = dict(getattr(mark, "kwargs", {}) or {})
|
|
262
|
+
if args and kwargs:
|
|
263
|
+
raise ConnectionResolutionError(
|
|
264
|
+
"testerkit_connections takes EITHER a positional list of "
|
|
265
|
+
"connection names OR instrument kwargs, not both. Got "
|
|
266
|
+
f"args={args!r} kwargs={kwargs!r}."
|
|
267
|
+
)
|
|
268
|
+
if args:
|
|
269
|
+
positional: Any = args[0] if len(args) == 1 else None
|
|
270
|
+
if not isinstance(positional, list):
|
|
271
|
+
raise ConnectionResolutionError(
|
|
272
|
+
"testerkit_connections positional form takes one list of "
|
|
273
|
+
f"connection names. Got {args!r}."
|
|
274
|
+
)
|
|
275
|
+
names: list[Any] = positional
|
|
276
|
+
if not all(isinstance(n, str) for n in names):
|
|
277
|
+
raise ConnectionResolutionError(
|
|
278
|
+
f"testerkit_connections names must be strings; got {names!r}."
|
|
279
|
+
)
|
|
280
|
+
return [str(n) for n in names], None
|
|
281
|
+
if kwargs:
|
|
282
|
+
return None, kwargs
|
|
283
|
+
raise ConnectionResolutionError(
|
|
284
|
+
"testerkit_connections requires either a positional list of "
|
|
285
|
+
"connection names or instrument kwargs."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _named_connections(
|
|
290
|
+
names: list[str],
|
|
291
|
+
fixture_cfg: FixtureConfig | None,
|
|
292
|
+
char_pins: set[str] | None,
|
|
293
|
+
) -> list[FixtureConnection]:
|
|
294
|
+
"""Resolve ``connections=[name, ...]`` against the fixture, in user-listed order."""
|
|
295
|
+
if fixture_cfg is None:
|
|
296
|
+
raise ConnectionResolutionError(
|
|
297
|
+
"testerkit_connections(connections=...) requires a fixture config; "
|
|
298
|
+
"connection names are only meaningful relative to a fixture YAML."
|
|
299
|
+
)
|
|
300
|
+
resolved: list[FixtureConnection] = []
|
|
301
|
+
for name in names:
|
|
302
|
+
conn = fixture_cfg.connections.get(name)
|
|
303
|
+
if conn is None:
|
|
304
|
+
raise ConnectionResolutionError(
|
|
305
|
+
f"Fixture connection {name!r} not found in fixture config."
|
|
306
|
+
)
|
|
307
|
+
resolved.append(conn)
|
|
308
|
+
if char_pins is not None:
|
|
309
|
+
invalid = [conn.name for conn in resolved if conn.uut_pin not in char_pins]
|
|
310
|
+
if invalid:
|
|
311
|
+
raise ConnectionResolutionError(
|
|
312
|
+
f"testerkit_connections names {invalid} resolve to pins outside "
|
|
313
|
+
f"the union of declared characteristics' pin sets {sorted(char_pins)}."
|
|
314
|
+
)
|
|
315
|
+
return resolved
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _channel_selectors(
|
|
319
|
+
instrument_channels: dict[str, Any],
|
|
320
|
+
fixture_cfg: FixtureConfig | None,
|
|
321
|
+
char_pins: set[str] | None,
|
|
322
|
+
) -> list[FixtureConnection]:
|
|
323
|
+
"""Resolve ``instrument_channels={inst: [chs]}`` in user-listed order.
|
|
324
|
+
|
|
325
|
+
With a fixture, matches each (instrument, channel) selector against
|
|
326
|
+
fixture connections; with ``char_pins`` set, every match must be in
|
|
327
|
+
that pin set. Without a fixture, synthesizes minimal
|
|
328
|
+
:class:`FixtureConnection` stubs from the (instrument, channel)
|
|
329
|
+
tuples — usable for early bringup where the UUT-pin mapping isn't
|
|
330
|
+
yet captured.
|
|
331
|
+
"""
|
|
332
|
+
if fixture_cfg is None:
|
|
333
|
+
stubs: list[FixtureConnection] = []
|
|
334
|
+
for inst_name, channels in instrument_channels.items():
|
|
335
|
+
if channels == "all":
|
|
336
|
+
raise ConnectionResolutionError(
|
|
337
|
+
f"testerkit_connections instrument_channels[{inst_name!r}]='all' "
|
|
338
|
+
"requires a fixture config."
|
|
339
|
+
)
|
|
340
|
+
for ch in channels:
|
|
341
|
+
ch_str = str(ch)
|
|
342
|
+
stubs.append(
|
|
343
|
+
FixtureConnection(
|
|
344
|
+
name=f"{inst_name}_ch{ch_str}",
|
|
345
|
+
instrument=inst_name,
|
|
346
|
+
instrument_channel=ch_str,
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
return stubs
|
|
350
|
+
|
|
351
|
+
matched: list[FixtureConnection] = []
|
|
352
|
+
for inst_name, channels in instrument_channels.items():
|
|
353
|
+
if channels == "all":
|
|
354
|
+
inst_matches = [
|
|
355
|
+
conn for conn in fixture_cfg.connections.values() if conn.instrument == inst_name
|
|
356
|
+
]
|
|
357
|
+
else:
|
|
358
|
+
inst_matches = []
|
|
359
|
+
for ch in channels:
|
|
360
|
+
ch_str = str(ch)
|
|
361
|
+
hit = next(
|
|
362
|
+
(
|
|
363
|
+
conn
|
|
364
|
+
for conn in fixture_cfg.connections.values()
|
|
365
|
+
if conn.instrument == inst_name and conn.instrument_channel == ch_str
|
|
366
|
+
),
|
|
367
|
+
None,
|
|
368
|
+
)
|
|
369
|
+
if hit is None:
|
|
370
|
+
raise ConnectionResolutionError(
|
|
371
|
+
f"testerkit_connections instrument_channels[{inst_name!r}]={ch_str!r} "
|
|
372
|
+
"matched no fixture connection."
|
|
373
|
+
)
|
|
374
|
+
inst_matches.append(hit)
|
|
375
|
+
matched.extend(inst_matches)
|
|
376
|
+
|
|
377
|
+
if char_pins is not None:
|
|
378
|
+
invalid = [
|
|
379
|
+
f"{conn.instrument}/{conn.instrument_channel}"
|
|
380
|
+
for conn in matched
|
|
381
|
+
if conn.uut_pin not in char_pins
|
|
382
|
+
]
|
|
383
|
+
if invalid:
|
|
384
|
+
raise ConnectionResolutionError(
|
|
385
|
+
f"testerkit_connections selected channels {invalid} resolve to pins outside "
|
|
386
|
+
f"the union of declared characteristics' pin sets {sorted(char_pins)}."
|
|
387
|
+
)
|
|
388
|
+
return matched
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def resolve_test_connections(
|
|
392
|
+
characteristics: Sequence[str],
|
|
393
|
+
conn_marker: pytest.Mark | None,
|
|
394
|
+
spec_ctx: Any,
|
|
395
|
+
fixture_cfg: FixtureConfig | None,
|
|
396
|
+
) -> tuple[list[FixtureConnection], dict[str, str]]:
|
|
397
|
+
"""Resolve the iterable connection set + per-connection char lookup.
|
|
398
|
+
|
|
399
|
+
The two markers compose:
|
|
400
|
+
|
|
401
|
+
* **chars only** — derive connections from each char's
|
|
402
|
+
``(pin, function)``, matched against the fixture. Empty list
|
|
403
|
+
when ``characteristics`` is empty AND no ``conn_marker``.
|
|
404
|
+
* **connections only** — explicit lookup against the fixture, or
|
|
405
|
+
synthesized stubs when ``instrument_channels`` is given without
|
|
406
|
+
a fixture.
|
|
407
|
+
* **chars + connections** — connections narrows the union of the
|
|
408
|
+
chars' pin sets. Iteration order follows the user-listed
|
|
409
|
+
connections; every selected connection must lie within the
|
|
410
|
+
union pin set. Fixtureless ``instrument_channels`` with
|
|
411
|
+
characteristics in scope is rejected — there is no ``uut_pin``
|
|
412
|
+
to validate against (drop the characteristics for pure bringup,
|
|
413
|
+
or load a fixture).
|
|
414
|
+
|
|
415
|
+
Returns a tuple of ``(connections, conn_to_char)`` where
|
|
416
|
+
``conn_to_char`` maps each connection's ``name`` to the char that
|
|
417
|
+
owns it (used by ``ConnectionIterator`` to push the active-char
|
|
418
|
+
ContextVar per connection). The lookup is empty for the
|
|
419
|
+
pure-``testerkit_connections`` bringup case.
|
|
420
|
+
"""
|
|
421
|
+
if not characteristics and conn_marker is None:
|
|
422
|
+
return [], {}
|
|
423
|
+
|
|
424
|
+
per_char_pins: dict[str, list[str]] = {}
|
|
425
|
+
if characteristics:
|
|
426
|
+
per_char_pins = _per_char_pins(characteristics, spec_ctx)
|
|
427
|
+
|
|
428
|
+
if conn_marker is None:
|
|
429
|
+
if fixture_cfg is None:
|
|
430
|
+
# Chars in scope but no fixture loaded → ctx.connections is
|
|
431
|
+
# an empty iterator. Tests that only resolve limits (and
|
|
432
|
+
# never iterate connections) work fine in this mode; tests
|
|
433
|
+
# that DO iterate trip the post-run "declared but didn't
|
|
434
|
+
# consume" guard in _testerkit_resolve_connections.
|
|
435
|
+
return [], {}
|
|
436
|
+
return _resolve_chars_to_connections(characteristics, spec_ctx, fixture_cfg)
|
|
437
|
+
|
|
438
|
+
union_pins: set[str] | None = None
|
|
439
|
+
if per_char_pins:
|
|
440
|
+
union_pins = {pin for pins in per_char_pins.values() for pin in pins}
|
|
441
|
+
|
|
442
|
+
names, instrument_channels = _resolve_connections_payload(conn_marker)
|
|
443
|
+
if names is not None:
|
|
444
|
+
connections = _named_connections(names, fixture_cfg, union_pins)
|
|
445
|
+
else:
|
|
446
|
+
assert instrument_channels is not None
|
|
447
|
+
# Tighten bug #15: chars in scope + fixtureless instrument_channels
|
|
448
|
+
# would silently bypass the per-char pin-set cross-check (no
|
|
449
|
+
# uut_pin to validate against). That makes testerkit_characteristics
|
|
450
|
+
# dead weight in this branch — flag it loud.
|
|
451
|
+
if characteristics and fixture_cfg is None:
|
|
452
|
+
raise ConnectionResolutionError(
|
|
453
|
+
f"testerkit_characteristics({list(characteristics)!r}) cannot "
|
|
454
|
+
"validate fixtureless instrument_channels selectors — there "
|
|
455
|
+
"is no uut_pin to cross-check against. Drop "
|
|
456
|
+
"testerkit_characteristics for pure bringup, or load a "
|
|
457
|
+
"fixture."
|
|
458
|
+
)
|
|
459
|
+
connections = _channel_selectors(instrument_channels, fixture_cfg, union_pins)
|
|
460
|
+
|
|
461
|
+
conn_to_char = _connections_to_char_map(connections, per_char_pins, spec_ctx)
|
|
462
|
+
return connections, conn_to_char
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _connections_to_char_map(
|
|
466
|
+
connections: list[FixtureConnection],
|
|
467
|
+
per_char_pins: dict[str, list[str]],
|
|
468
|
+
spec_ctx: Any,
|
|
469
|
+
) -> dict[str, str]:
|
|
470
|
+
"""Build the ``conn.name → char_id`` lookup for explicit ``testerkit_connections`` paths.
|
|
471
|
+
|
|
472
|
+
For each connection, pick the first char (in user-declared order)
|
|
473
|
+
whose pin set covers the connection's ``uut_pin``. Connections
|
|
474
|
+
outside any char's pin set (only possible for fixtureless
|
|
475
|
+
``instrument_channels`` stubs without a spec) get no entry.
|
|
476
|
+
"""
|
|
477
|
+
if not per_char_pins:
|
|
478
|
+
return {}
|
|
479
|
+
out: dict[str, str] = {}
|
|
480
|
+
for conn in connections:
|
|
481
|
+
pin_id = conn.uut_pin
|
|
482
|
+
if pin_id is None and spec_ctx is not None and conn.net is not None:
|
|
483
|
+
net_pins = spec_ctx.part.get_pins_by_net(conn.net)
|
|
484
|
+
pin_id = net_pins[0] if net_pins else None
|
|
485
|
+
if pin_id is None:
|
|
486
|
+
continue
|
|
487
|
+
for char_id, pins in per_char_pins.items():
|
|
488
|
+
if pin_id in pins:
|
|
489
|
+
out[conn.name] = char_id
|
|
490
|
+
break
|
|
491
|
+
return out
|