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,455 @@
|
|
|
1
|
+
"""Per-run event projection pool for the runs daemon's materializer.
|
|
2
|
+
|
|
3
|
+
Owns the in-memory, in-flight projection of run events that the
|
|
4
|
+
runs daemon serves alongside its parquet-finalized rows AND uses to
|
|
5
|
+
write parquets at ``RunEnded`` time.
|
|
6
|
+
|
|
7
|
+
One :class:`~testerkit.data.backends._event_accumulator.EventAccumulator`
|
|
8
|
+
per active ``run_id`` accumulates events until either:
|
|
9
|
+
|
|
10
|
+
* The daemon's dispatch handler sees ``RunEnded`` (real, or
|
|
11
|
+
synthesized by the orphan sweep). It writes the canonical
|
|
12
|
+
parquet via :func:`~testerkit.data.backends.parquet.materialize_run_to_parquet`,
|
|
13
|
+
ingests into ``runs_materialized`` / ``steps_materialized`` /
|
|
14
|
+
``measurements_materialized``, then emits ``RunMaterialized``
|
|
15
|
+
to the events bus. Receipt of ``RunMaterialized`` evicts the
|
|
16
|
+
pool entry.
|
|
17
|
+
* The orphan sweep detects a dead producer (pid no longer
|
|
18
|
+
exists) or wall-clock timeout, synthesizes a
|
|
19
|
+
``RunEnded(outcome="aborted")`` event into the events bus.
|
|
20
|
+
The synthetic event flows through the same dispatch path
|
|
21
|
+
above — same materialize → emit → evict sequence.
|
|
22
|
+
|
|
23
|
+
The pool is the runs daemon's "head block" in the
|
|
24
|
+
Prometheus / LSM-tree / materialized-view pattern: in-memory
|
|
25
|
+
projection of recent events, regenerable from the events
|
|
26
|
+
daemon's IPC log via replay on daemon respawn.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import threading
|
|
33
|
+
from datetime import UTC, datetime
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import pyarrow as pa
|
|
37
|
+
|
|
38
|
+
from testerkit.data.backends._event_accumulator import EventAccumulator
|
|
39
|
+
from testerkit.data.events import (
|
|
40
|
+
InstrumentConnected,
|
|
41
|
+
InstrumentReserved,
|
|
42
|
+
MeasurementRecorded,
|
|
43
|
+
Observation,
|
|
44
|
+
RunEnded,
|
|
45
|
+
RunMaterialized,
|
|
46
|
+
RunStarted,
|
|
47
|
+
SessionEnded,
|
|
48
|
+
SessionStarted,
|
|
49
|
+
StepEnded,
|
|
50
|
+
StepsDiscovered,
|
|
51
|
+
StepStarted,
|
|
52
|
+
VectorEnded,
|
|
53
|
+
VectorStarted,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Map ``event_type`` strings to Pydantic event classes for dict→typed
|
|
60
|
+
# conversion. The runs daemon receives events as dicts from the
|
|
61
|
+
# events daemon over Flight; the accumulator works with typed events.
|
|
62
|
+
_EVENT_CLASSES: dict[str, type] = {
|
|
63
|
+
"session.started": SessionStarted,
|
|
64
|
+
"session.ended": SessionEnded,
|
|
65
|
+
"run.started": RunStarted,
|
|
66
|
+
"run.ended": RunEnded,
|
|
67
|
+
"run.materialized": RunMaterialized,
|
|
68
|
+
"test.steps_discovered": StepsDiscovered,
|
|
69
|
+
"test.step_started": StepStarted,
|
|
70
|
+
"test.step_ended": StepEnded,
|
|
71
|
+
"test.measurement": MeasurementRecorded,
|
|
72
|
+
"test.observation": Observation,
|
|
73
|
+
"test.vector_started": VectorStarted,
|
|
74
|
+
"test.vector_ended": VectorEnded,
|
|
75
|
+
"fixture.instrument_connected": InstrumentConnected,
|
|
76
|
+
"instrument.reserved": InstrumentReserved,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AccumulatorPool:
|
|
81
|
+
"""Thread-safe pool of per-run :class:`EventAccumulator` instances.
|
|
82
|
+
|
|
83
|
+
All public methods are safe to call from the events-subscription
|
|
84
|
+
watcher thread (which feeds events) and from the Flight server
|
|
85
|
+
threads (which read snapshots) concurrently.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self._lock = threading.RLock()
|
|
90
|
+
self._accs: dict[str, EventAccumulator] = {} # run_id → accumulator
|
|
91
|
+
# Producer pid per session_id, captured from SessionStarted and cleared
|
|
92
|
+
# on SessionEnded. The RUN orphan sweep resolves a run back to its
|
|
93
|
+
# producer pid through this (pid-death force-closes a run).
|
|
94
|
+
self._session_pid: dict[str, int] = {}
|
|
95
|
+
# Most recent event timestamp per run_id — wall-clock fallback
|
|
96
|
+
# for the orphan sweep when pid liveness check is unavailable.
|
|
97
|
+
self._last_event_at: dict[str, datetime] = {}
|
|
98
|
+
# session_id per run_id, so the orphan sweep can resolve a
|
|
99
|
+
# run_id back to its producer pid.
|
|
100
|
+
self._run_session: dict[str, str] = {}
|
|
101
|
+
# Monotonic generation counter — bumped under ``_lock`` on every
|
|
102
|
+
# state change (dispatch / evict). The overlay-sync thread uses it
|
|
103
|
+
# only as a cheap "has anything changed?" wake/idle signal.
|
|
104
|
+
self._generation = 0
|
|
105
|
+
# Per-run delta since the last ``take_delta`` drain. The overlay is
|
|
106
|
+
# maintained incrementally by ONE background sync thread off the
|
|
107
|
+
# read path (no per-query refresh): ``dispatch`` dirties a run,
|
|
108
|
+
# ``evict`` evicts it. The sync thread drains this and rewrites only
|
|
109
|
+
# the affected runs' overlay rows — O(changed runs), not O(pool).
|
|
110
|
+
self._dirty: set[str] = set()
|
|
111
|
+
self._evicted: set[str] = set()
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Write path — fed by the events-daemon subscription
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def dispatch(self, evt_dict: dict[str, Any]) -> None:
|
|
118
|
+
"""Route one event (dict from the events daemon) into the pool.
|
|
119
|
+
|
|
120
|
+
``SessionStarted`` is captured for pid tracking but not
|
|
121
|
+
routed to a per-run accumulator (it has no ``run_id``).
|
|
122
|
+
Run-bearing events route into the accumulator keyed by
|
|
123
|
+
``run_id``, creating it on first sight.
|
|
124
|
+
"""
|
|
125
|
+
et = evt_dict.get("event_type")
|
|
126
|
+
cls = _EVENT_CLASSES.get(str(et) if et else "")
|
|
127
|
+
if cls is None:
|
|
128
|
+
return
|
|
129
|
+
try:
|
|
130
|
+
typed = cls.model_validate(evt_dict)
|
|
131
|
+
except Exception as exc: # noqa: BLE001 — bad event must not kill watcher
|
|
132
|
+
logger.debug("Pool skipping unparseable event %s: %s", et, exc)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if isinstance(typed, SessionStarted):
|
|
136
|
+
session_id = str(typed.session_id) if typed.session_id else None
|
|
137
|
+
if session_id and typed.pid:
|
|
138
|
+
with self._lock:
|
|
139
|
+
self._session_pid[session_id] = typed.pid
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if isinstance(typed, SessionEnded):
|
|
143
|
+
# A cleanly-ended session is no longer open — drop it so the orphan
|
|
144
|
+
# sweep can't self-heal (re-emit SessionEnded for) it.
|
|
145
|
+
session_id = str(typed.session_id) if typed.session_id else None
|
|
146
|
+
if session_id:
|
|
147
|
+
self.mark_session_ended(session_id)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if isinstance(typed, RunMaterialized):
|
|
151
|
+
# Materialization signal — handled by the daemon's _on_event
|
|
152
|
+
# wrapper (it evicts the pool entry). Don't route into an
|
|
153
|
+
# accumulator: there's nothing for the accumulator to absorb,
|
|
154
|
+
# and creating one on first sight would leak state for a run
|
|
155
|
+
# that just finished.
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
run_id = str(getattr(typed, "run_id", None) or "")
|
|
159
|
+
if not run_id:
|
|
160
|
+
return
|
|
161
|
+
session_id = str(getattr(typed, "session_id", None) or "")
|
|
162
|
+
with self._lock:
|
|
163
|
+
acc = self._accs.setdefault(run_id, EventAccumulator())
|
|
164
|
+
acc.on_event(typed)
|
|
165
|
+
self._last_event_at[run_id] = datetime.now(UTC)
|
|
166
|
+
if session_id:
|
|
167
|
+
self._run_session[run_id] = session_id
|
|
168
|
+
# Mark this run dirty + bump generation under the same lock as
|
|
169
|
+
# the state change, so the sync thread's next drain snapshots
|
|
170
|
+
# this event. Atomic with the dispatch.
|
|
171
|
+
self._dirty.add(run_id)
|
|
172
|
+
self._generation += 1
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
# Read path — snapshot for the runs daemon's UNION views
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def snapshot_run_rows(self) -> list[dict[str, Any]]:
|
|
179
|
+
"""Return one run row per active accumulator (None entries dropped)."""
|
|
180
|
+
with self._lock:
|
|
181
|
+
return [r for a in self._accs.values() if (r := a.snapshot_run_row())]
|
|
182
|
+
|
|
183
|
+
def snapshot_step_rows(self) -> list[dict[str, Any]]:
|
|
184
|
+
"""Flatten step rows from every active accumulator."""
|
|
185
|
+
with self._lock:
|
|
186
|
+
return [r for a in self._accs.values() for r in a.snapshot_step_rows()]
|
|
187
|
+
|
|
188
|
+
def snapshot_measurement_rows(self) -> list[dict[str, Any]]:
|
|
189
|
+
"""Flatten measurement rows from every active accumulator."""
|
|
190
|
+
with self._lock:
|
|
191
|
+
return [r for a in self._accs.values() for r in a.snapshot_measurement_rows()]
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------------
|
|
194
|
+
# Orphan sweep + lifecycle
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def open_runs(self) -> list[tuple[str, EventAccumulator, int | None, datetime | None]]:
|
|
198
|
+
"""Return ``(run_id, accumulator, pid_or_None, last_event_or_None)`` for open runs.
|
|
199
|
+
|
|
200
|
+
Open = ``RunStarted`` seen, ``RunEnded`` not seen.
|
|
201
|
+
The orphan sweep iterates this and decides per-entry whether
|
|
202
|
+
to finalize.
|
|
203
|
+
"""
|
|
204
|
+
out: list[tuple[str, EventAccumulator, int | None, datetime | None]] = []
|
|
205
|
+
with self._lock:
|
|
206
|
+
for run_id, acc in self._accs.items():
|
|
207
|
+
if not acc._run_started or acc._run_ended is not None:
|
|
208
|
+
continue
|
|
209
|
+
session_id = self._run_session.get(run_id)
|
|
210
|
+
pid = self._session_pid.get(session_id) if session_id else None
|
|
211
|
+
last = self._last_event_at.get(run_id)
|
|
212
|
+
out.append((run_id, acc, pid, last))
|
|
213
|
+
return out
|
|
214
|
+
|
|
215
|
+
def mark_session_ended(self, session_id: str) -> None:
|
|
216
|
+
"""Forget a session's producer pid (on SessionEnded). Keeps the run
|
|
217
|
+
sweep's pid map from carrying a closed session's producer."""
|
|
218
|
+
with self._lock:
|
|
219
|
+
self._session_pid.pop(session_id, None)
|
|
220
|
+
|
|
221
|
+
def evict(self, run_id: str) -> EventAccumulator | None:
|
|
222
|
+
"""Drop the accumulator for ``run_id`` and return it (or ``None``)."""
|
|
223
|
+
with self._lock:
|
|
224
|
+
self._last_event_at.pop(run_id, None)
|
|
225
|
+
self._run_session.pop(run_id, None)
|
|
226
|
+
acc = self._accs.pop(run_id, None)
|
|
227
|
+
if acc is not None:
|
|
228
|
+
# Evicted runs must have their overlay rows removed; drop
|
|
229
|
+
# any pending dirty mark (the run is gone, not changed).
|
|
230
|
+
self._dirty.discard(run_id)
|
|
231
|
+
self._evicted.add(run_id)
|
|
232
|
+
self._generation += 1
|
|
233
|
+
return acc
|
|
234
|
+
|
|
235
|
+
def take_delta(
|
|
236
|
+
self,
|
|
237
|
+
) -> tuple[set[str], list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]] | None:
|
|
238
|
+
"""Drain the per-run delta since the last call; ``None`` if nothing changed.
|
|
239
|
+
|
|
240
|
+
Returns ``(touched_run_ids, run_rows, step_rows, meas_rows)``:
|
|
241
|
+
|
|
242
|
+
* ``touched_run_ids`` — every run whose overlay rows must be cleared
|
|
243
|
+
(dirty runs to be re-inserted + evicted runs to be removed).
|
|
244
|
+
* ``run_rows`` / ``step_rows`` / ``meas_rows`` — the fresh snapshot
|
|
245
|
+
rows for the still-present dirty runs (flattened, each carries
|
|
246
|
+
``run_id``). Evicted / not-yet-started runs contribute no rows, so
|
|
247
|
+
a clear-then-reinsert leaves them gone.
|
|
248
|
+
|
|
249
|
+
Snapshot + clear happen under the pool lock, so a run that changes
|
|
250
|
+
after the drain is simply re-dirtied for the next drain — never lost.
|
|
251
|
+
The overlay-sync thread is the sole caller; queries never refresh.
|
|
252
|
+
"""
|
|
253
|
+
with self._lock:
|
|
254
|
+
if not self._dirty and not self._evicted:
|
|
255
|
+
return None
|
|
256
|
+
dirty = self._dirty
|
|
257
|
+
evicted = self._evicted
|
|
258
|
+
self._dirty = set()
|
|
259
|
+
self._evicted = set()
|
|
260
|
+
run_rows: list[dict[str, Any]] = []
|
|
261
|
+
step_rows: list[dict[str, Any]] = []
|
|
262
|
+
meas_rows: list[dict[str, Any]] = []
|
|
263
|
+
for rid in dirty:
|
|
264
|
+
acc = self._accs.get(rid)
|
|
265
|
+
if acc is None:
|
|
266
|
+
evicted.add(rid) # vanished between dispatch and drain
|
|
267
|
+
continue
|
|
268
|
+
if rr := acc.snapshot_run_row():
|
|
269
|
+
run_rows.append(rr)
|
|
270
|
+
step_rows.extend(acc.snapshot_step_rows())
|
|
271
|
+
meas_rows.extend(acc.snapshot_measurement_rows())
|
|
272
|
+
return dirty | evicted, run_rows, step_rows, meas_rows
|
|
273
|
+
|
|
274
|
+
def generation(self) -> int:
|
|
275
|
+
"""Current monotonic generation — bumps on every pool state change.
|
|
276
|
+
|
|
277
|
+
A cheap read for the lock-free fast path of the inflight refresh:
|
|
278
|
+
compare against the last-refreshed value and skip the snapshot
|
|
279
|
+
build entirely when nothing has changed.
|
|
280
|
+
"""
|
|
281
|
+
return self._generation
|
|
282
|
+
|
|
283
|
+
def has(self, run_id: str) -> bool:
|
|
284
|
+
"""Whether the pool currently holds an accumulator for ``run_id``."""
|
|
285
|
+
with self._lock:
|
|
286
|
+
return run_id in self._accs
|
|
287
|
+
|
|
288
|
+
def get(self, run_id: str) -> EventAccumulator | None:
|
|
289
|
+
"""Return the accumulator for ``run_id`` without removing it."""
|
|
290
|
+
with self._lock:
|
|
291
|
+
return self._accs.get(run_id)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# Inflight Arrow schemas — mirror ``runs_materialized`` / ``steps_materialized``
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
#
|
|
298
|
+
# Describe the shape of the per-query Arrow snapshots that back the
|
|
299
|
+
# ``inflight_runs`` / ``inflight_steps`` relations the UNION views
|
|
300
|
+
# reference. Registered on the DuckDB connection via ``conn.register()``
|
|
301
|
+
# (read-only metadata — no WAL write, no lock contention with the
|
|
302
|
+
# ingest thread). A pre-query hook that wrote DELETE+INSERT to TEMP
|
|
303
|
+
# tables previously serialized every read behind any concurrent
|
|
304
|
+
# ingest write, wedging the daemon at ~60 % CPU on lock-wait.
|
|
305
|
+
#
|
|
306
|
+
# Outcome here is plain VARCHAR — the materialized side carries the
|
|
307
|
+
# strict ``outcome_kind`` ENUM but DuckDB's ``UNION ALL`` permits
|
|
308
|
+
# string ↔ enum coercion at read time. Pool snapshots only emit
|
|
309
|
+
# strings already in the canonical Outcome enum (validated when
|
|
310
|
+
# the accumulator builds the row).
|
|
311
|
+
INFLIGHT_RUNS_SCHEMA = pa.schema(
|
|
312
|
+
[
|
|
313
|
+
("run_id", pa.string()),
|
|
314
|
+
("file_path", pa.string()),
|
|
315
|
+
("session_id", pa.string()),
|
|
316
|
+
("site_index", pa.int64()),
|
|
317
|
+
("site_name", pa.string()),
|
|
318
|
+
("uut_serial_number", pa.string()),
|
|
319
|
+
("uut_part_number", pa.string()),
|
|
320
|
+
("uut_revision", pa.string()),
|
|
321
|
+
("uut_lot_number", pa.string()),
|
|
322
|
+
("station_id", pa.string()),
|
|
323
|
+
("station_name", pa.string()),
|
|
324
|
+
("station_hostname", pa.string()),
|
|
325
|
+
("station_type", pa.string()),
|
|
326
|
+
("station_location", pa.string()),
|
|
327
|
+
("fixture_id", pa.string()),
|
|
328
|
+
("outcome", pa.string()),
|
|
329
|
+
("started_at", pa.timestamp("us", tz="UTC")),
|
|
330
|
+
("ended_at", pa.timestamp("us", tz="UTC")),
|
|
331
|
+
("num_measurements", pa.int32()),
|
|
332
|
+
("num_steps", pa.int32()),
|
|
333
|
+
("test_phase", pa.string()),
|
|
334
|
+
("part_id", pa.string()),
|
|
335
|
+
("part_name", pa.string()),
|
|
336
|
+
("part_revision", pa.string()),
|
|
337
|
+
("operator_id", pa.string()),
|
|
338
|
+
("operator_name", pa.string()),
|
|
339
|
+
("project_name", pa.string()),
|
|
340
|
+
("git_commit", pa.string()),
|
|
341
|
+
("git_branch", pa.string()),
|
|
342
|
+
("git_remote", pa.string()),
|
|
343
|
+
]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
INFLIGHT_STEPS_SCHEMA = pa.schema(
|
|
347
|
+
[
|
|
348
|
+
("run_id", pa.string()),
|
|
349
|
+
("step_index", pa.int32()),
|
|
350
|
+
("file_path", pa.string()),
|
|
351
|
+
("session_id", pa.string()),
|
|
352
|
+
("site_index", pa.int64()),
|
|
353
|
+
("site_name", pa.string()),
|
|
354
|
+
("step_name", pa.string()),
|
|
355
|
+
("step_path", pa.string()),
|
|
356
|
+
("vector_index", pa.int64()),
|
|
357
|
+
("vector_outer_index", pa.int64()),
|
|
358
|
+
# The vector's OWN 0-based retry (NULL on a logical-step row). Part of
|
|
359
|
+
# the ``vectors_materialized`` grain key (full snowflake, 0.3.1
|
|
360
|
+
# phase 6), so the overlay must carry it too or a live retried vector
|
|
361
|
+
# can't be matched 1:1 against its finalized row.
|
|
362
|
+
("vector_retry", pa.int64()),
|
|
363
|
+
("outcome", pa.string()),
|
|
364
|
+
("started_at", pa.timestamp("us", tz="UTC")),
|
|
365
|
+
("ended_at", pa.timestamp("us", tz="UTC")),
|
|
366
|
+
("duration_s", pa.float64()),
|
|
367
|
+
("step_retry", pa.int64()),
|
|
368
|
+
("measurement_count", pa.int32()),
|
|
369
|
+
("markers", pa.string()),
|
|
370
|
+
("uut_serial_number", pa.string()),
|
|
371
|
+
("station_id", pa.string()),
|
|
372
|
+
# Two unprefixed maps — replaces the old merged, ``in_``/``out_``-
|
|
373
|
+
# prefixed ``dynamic_attrs`` MAP (projection-normalization, 0.3.1).
|
|
374
|
+
# The overlay has no long-EAV table to join against for live rows, so
|
|
375
|
+
# it keeps building these directly from the accumulator's in-memory
|
|
376
|
+
# dicts (cheap, no join) — see ``_event_accumulator._pack_io_maps``.
|
|
377
|
+
("inputs_map", pa.map_(pa.string(), pa.string())),
|
|
378
|
+
("outputs_map", pa.map_(pa.string(), pa.string())),
|
|
379
|
+
]
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
INFLIGHT_MEASUREMENTS_SCHEMA = pa.schema(
|
|
383
|
+
[
|
|
384
|
+
("run_id", pa.string()),
|
|
385
|
+
("session_id", pa.string()),
|
|
386
|
+
("site_index", pa.int64()),
|
|
387
|
+
("site_name", pa.string()),
|
|
388
|
+
("run_started_at", pa.timestamp("us", tz="UTC")),
|
|
389
|
+
("run_ended_at", pa.timestamp("us", tz="UTC")),
|
|
390
|
+
("run_outcome", pa.string()),
|
|
391
|
+
("uut_serial_number", pa.string()),
|
|
392
|
+
("uut_part_number", pa.string()),
|
|
393
|
+
("uut_revision", pa.string()),
|
|
394
|
+
("uut_lot_number", pa.string()),
|
|
395
|
+
("part_id", pa.string()),
|
|
396
|
+
("part_name", pa.string()),
|
|
397
|
+
("part_revision", pa.string()),
|
|
398
|
+
("station_id", pa.string()),
|
|
399
|
+
("station_name", pa.string()),
|
|
400
|
+
("station_hostname", pa.string()),
|
|
401
|
+
("station_type", pa.string()),
|
|
402
|
+
("station_location", pa.string()),
|
|
403
|
+
("fixture_id", pa.string()),
|
|
404
|
+
("test_phase", pa.string()),
|
|
405
|
+
("project_name", pa.string()),
|
|
406
|
+
("operator_id", pa.string()),
|
|
407
|
+
("operator_name", pa.string()),
|
|
408
|
+
("git_commit", pa.string()),
|
|
409
|
+
("git_branch", pa.string()),
|
|
410
|
+
("git_remote", pa.string()),
|
|
411
|
+
("python_version", pa.string()),
|
|
412
|
+
("testerkit_version", pa.string()),
|
|
413
|
+
("env_fingerprint", pa.string()),
|
|
414
|
+
("step_name", pa.string()),
|
|
415
|
+
("step_index", pa.int32()),
|
|
416
|
+
("step_path", pa.string()),
|
|
417
|
+
# Enclosing-step retry — a coordinate on measurements_materialized
|
|
418
|
+
# (full snowflake, 0.3.1); carried so a live measurement matches its
|
|
419
|
+
# finalized row and the ``measurements`` view exposes it on both sides.
|
|
420
|
+
("step_retry", pa.int64()),
|
|
421
|
+
("step_outcome", pa.string()),
|
|
422
|
+
("step_started_at", pa.timestamp("us", tz="UTC")),
|
|
423
|
+
("step_ended_at", pa.timestamp("us", tz="UTC")),
|
|
424
|
+
("vector_index", pa.int64()),
|
|
425
|
+
("vector_outer_index", pa.int64()),
|
|
426
|
+
("vector_retry", pa.int64()),
|
|
427
|
+
("vector_outcome", pa.string()),
|
|
428
|
+
("measurement_name", pa.string()),
|
|
429
|
+
("measurement_value", pa.float64()),
|
|
430
|
+
("measurement_outcome", pa.string()),
|
|
431
|
+
("measurement_unit", pa.string()),
|
|
432
|
+
("measurement_timestamp", pa.timestamp("us", tz="UTC")),
|
|
433
|
+
("limit_low", pa.float64()),
|
|
434
|
+
("limit_high", pa.float64()),
|
|
435
|
+
("limit_nominal", pa.float64()),
|
|
436
|
+
("limit_comparator", pa.string()),
|
|
437
|
+
("characteristic_id", pa.string()),
|
|
438
|
+
("spec_ref", pa.string()),
|
|
439
|
+
("uut_pin", pa.string()),
|
|
440
|
+
("fixture_connection", pa.string()),
|
|
441
|
+
("instrument_name", pa.string()),
|
|
442
|
+
("instrument_resource", pa.string()),
|
|
443
|
+
("instrument_channel", pa.string()),
|
|
444
|
+
# Two unprefixed maps — see INFLIGHT_STEPS_SCHEMA above.
|
|
445
|
+
("inputs_map", pa.map_(pa.string(), pa.string())),
|
|
446
|
+
("outputs_map", pa.map_(pa.string(), pa.string())),
|
|
447
|
+
]
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Allocated once at module load. ``conn.register()`` with these keeps
|
|
451
|
+
# the UNION views resolvable when the pool is empty (the common case
|
|
452
|
+
# at idle).
|
|
453
|
+
EMPTY_INFLIGHT_RUNS = pa.Table.from_pylist([], schema=INFLIGHT_RUNS_SCHEMA)
|
|
454
|
+
EMPTY_INFLIGHT_STEPS = pa.Table.from_pylist([], schema=INFLIGHT_STEPS_SCHEMA)
|
|
455
|
+
EMPTY_INFLIGHT_MEASUREMENTS = pa.Table.from_pylist([], schema=INFLIGHT_MEASUREMENTS_SCHEMA)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Atomic file write helpers.
|
|
2
|
+
|
|
3
|
+
Prevent data corruption from crashes during Parquet/JSON writes by
|
|
4
|
+
writing to a temporary file in the same directory, then atomically
|
|
5
|
+
renaming to the final path via ``os.replace()``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pyarrow as pa
|
|
15
|
+
import pyarrow.parquet as pq
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def atomic_write_table(table: pa.Table, path: Path) -> None:
|
|
19
|
+
"""Write a PyArrow table to Parquet atomically.
|
|
20
|
+
|
|
21
|
+
Writes to a temp file in the same directory, then ``os.replace()``
|
|
22
|
+
to the final path. On failure, the temp file is cleaned up and no
|
|
23
|
+
partial file is left at ``path``.
|
|
24
|
+
"""
|
|
25
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
|
|
27
|
+
os.close(fd)
|
|
28
|
+
try:
|
|
29
|
+
pq.write_table(table, tmp_path)
|
|
30
|
+
os.replace(tmp_path, path)
|
|
31
|
+
except BaseException:
|
|
32
|
+
try:
|
|
33
|
+
os.unlink(tmp_path)
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
raise
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def atomic_write_text(text: str, path: Path) -> None:
|
|
40
|
+
"""Write text to a file atomically.
|
|
41
|
+
|
|
42
|
+
Same pattern as ``atomic_write_table``: temp file + ``os.replace()``.
|
|
43
|
+
"""
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
|
|
46
|
+
closed = False
|
|
47
|
+
try:
|
|
48
|
+
os.write(fd, text.encode("utf-8"))
|
|
49
|
+
os.close(fd)
|
|
50
|
+
closed = True
|
|
51
|
+
os.replace(tmp_path, path)
|
|
52
|
+
except BaseException:
|
|
53
|
+
if not closed:
|
|
54
|
+
os.close(fd)
|
|
55
|
+
try:
|
|
56
|
+
os.unlink(tmp_path)
|
|
57
|
+
except OSError:
|
|
58
|
+
pass
|
|
59
|
+
raise
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Runner-neutral assignment of sequence-relative step + vector indices.
|
|
2
|
+
|
|
3
|
+
The pytest plugin extracts ``(module, class_name, function)`` from each
|
|
4
|
+
collected item; this module turns that list into the ``step_index`` /
|
|
5
|
+
``vector_index`` / ``vector_count_planned`` triples consumers need. The
|
|
6
|
+
algorithm is identical for any runner that produces an ordered list of
|
|
7
|
+
test executions grouped by logical step — only the per-runner attribute
|
|
8
|
+
extraction stays in the runner-specific plugin.
|
|
9
|
+
|
|
10
|
+
* ``step_index`` is **sequence-relative**: it resets per parent (root
|
|
11
|
+
vs. class), and all variants of the same logical step share one value.
|
|
12
|
+
* ``vector_index`` is the 0-based position within the sweep expansion
|
|
13
|
+
for that step.
|
|
14
|
+
* ``vector_count_planned`` records the manifest's intent so consumers
|
|
15
|
+
can detect unrun vectors after the run.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
|
|
22
|
+
# Identity key for a logical step: (module_name, class_name, function_name).
|
|
23
|
+
# Empty strings stand in for missing components so the tuple is hashable
|
|
24
|
+
# and ordered consistently.
|
|
25
|
+
StepKey = tuple[str, str, str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def assign_indices(keys: list[StepKey]) -> list[tuple[int, int, int]]:
|
|
29
|
+
"""Compute ``(step_index, vector_index, vector_count_planned)`` per key.
|
|
30
|
+
|
|
31
|
+
Caller-supplied ``keys`` must already be in execution order — the
|
|
32
|
+
function preserves order and assigns indices accordingly.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
keys: One :data:`StepKey` per item, in collection / execution order.
|
|
36
|
+
The same step running multiple sweep variants appears N times.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A list of triples, parallel to ``keys``. Variants of the same
|
|
40
|
+
logical step share ``step_index`` and differ only in
|
|
41
|
+
``vector_index``.
|
|
42
|
+
"""
|
|
43
|
+
# First pass: count occurrences of each key for vector_count_planned.
|
|
44
|
+
group_count: dict[StepKey, int] = defaultdict(int)
|
|
45
|
+
for key in keys:
|
|
46
|
+
group_count[key] += 1
|
|
47
|
+
|
|
48
|
+
# Second pass: walk in order assigning step_index per parent and
|
|
49
|
+
# vector_index per key. The "parent" partition is by class_name (the
|
|
50
|
+
# second element of the key) — root-level functions share class_name=""
|
|
51
|
+
# so they count together; class methods count within their class.
|
|
52
|
+
parent_step_index: dict[str, dict[StepKey, int]] = defaultdict(dict)
|
|
53
|
+
seen_in_group: dict[StepKey, int] = {}
|
|
54
|
+
|
|
55
|
+
out: list[tuple[int, int, int]] = []
|
|
56
|
+
for key in keys:
|
|
57
|
+
parent = key[1] # class_name (or "" for root)
|
|
58
|
+
bucket = parent_step_index[parent]
|
|
59
|
+
if key not in bucket:
|
|
60
|
+
bucket[key] = len(bucket)
|
|
61
|
+
step_idx = bucket[key]
|
|
62
|
+
|
|
63
|
+
vec_idx = seen_in_group.get(key, 0)
|
|
64
|
+
seen_in_group[key] = vec_idx + 1
|
|
65
|
+
|
|
66
|
+
out.append((step_idx, vec_idx, group_count[key]))
|
|
67
|
+
return out
|