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,957 @@
|
|
|
1
|
+
"""Pure in-memory event projection — no I/O, no parquet dependencies.
|
|
2
|
+
|
|
3
|
+
:class:`EventAccumulator` is the single canonical state machine that
|
|
4
|
+
projects the testerkit event stream into row-shaped state. Both the test
|
|
5
|
+
runner's :class:`~testerkit.data.backends.parquet.ParquetSubscriber` (I/O
|
|
6
|
+
destination: disk) and the runs daemon's live overlay (I/O destination:
|
|
7
|
+
in-memory UNION view) use this class so their projections never drift.
|
|
8
|
+
|
|
9
|
+
Lives in its own module so the daemon can import it without pulling in
|
|
10
|
+
the full parquet/subscribers/exporters stack.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from testerkit.data.backends._row_helpers import (
|
|
18
|
+
RunParquetRow,
|
|
19
|
+
_append_not_started,
|
|
20
|
+
_to_datetime,
|
|
21
|
+
run_context_from_run_started,
|
|
22
|
+
step_entry_dict,
|
|
23
|
+
vector_entry_dict,
|
|
24
|
+
)
|
|
25
|
+
from testerkit.data.events import (
|
|
26
|
+
InstrumentConnected,
|
|
27
|
+
InstrumentReserved,
|
|
28
|
+
MeasurementRecorded,
|
|
29
|
+
Observation,
|
|
30
|
+
RunEnded,
|
|
31
|
+
RunStarted,
|
|
32
|
+
StepEnded,
|
|
33
|
+
StepsDiscovered,
|
|
34
|
+
StepStarted,
|
|
35
|
+
VectorEnded,
|
|
36
|
+
VectorStarted,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _safe_str(value: Any) -> str | None:
|
|
41
|
+
"""Return ``str(value)`` or ``None`` if *value* is falsy."""
|
|
42
|
+
return str(value) if value else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _pack_io_maps(
|
|
46
|
+
inputs: dict[str, Any], outputs: dict[str, Any]
|
|
47
|
+
) -> tuple[dict[str, str | None], dict[str, str | None]]:
|
|
48
|
+
"""Render inputs/outputs lane dicts into two unprefixed VARCHAR maps.
|
|
49
|
+
|
|
50
|
+
Replaces the old merged, ``in_``/``out_``-prefixed ``dynamic_attrs`` MAP
|
|
51
|
+
(projection-normalization, 0.3.1) — the dict IS the role, so no prefix is
|
|
52
|
+
needed to tell them apart.
|
|
53
|
+
"""
|
|
54
|
+
return (
|
|
55
|
+
{k: _safe_str(v) for k, v in inputs.items()},
|
|
56
|
+
{k: _safe_str(v) for k, v in outputs.items()},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _step_key(event: Any) -> tuple[str, int, int | None]:
|
|
61
|
+
"""Stable accumulator key for a StepStarted / StepEnded event.
|
|
62
|
+
|
|
63
|
+
Uses ``step_path`` (the canonical hierarchical id) when set;
|
|
64
|
+
falls back to ``step_name`` otherwise so direct-API callers
|
|
65
|
+
(and tests) that emit events without populating step_path
|
|
66
|
+
still get distinct keys per step. ``step_retry`` (the outer item
|
|
67
|
+
attempt) makes each rerun its own execution row instead of
|
|
68
|
+
overwriting the prior attempt (the de-fuse). ``vector_outer_index`` is
|
|
69
|
+
the ENCLOSING class-level iteration this step ran under — NULL at top
|
|
70
|
+
level, 0..N for methods nested under a swept class.
|
|
71
|
+
"""
|
|
72
|
+
path = event.step_path or event.step_name or ""
|
|
73
|
+
return (path, getattr(event, "retry", 0) or 0, getattr(event, "vector_outer_index", None))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _vector_key(event: Any) -> tuple[str, int | None, int, int]:
|
|
77
|
+
"""Stable accumulator key for a VectorStarted / VectorEnded event.
|
|
78
|
+
|
|
79
|
+
Keyed on ``(step_path, vector_outer_index, vector_index, retry)`` — the
|
|
80
|
+
outer index distinguishes class-level sweep iterations; the inner index
|
|
81
|
+
distinguishes in-body loop iterations; retry counts re-executions of
|
|
82
|
+
the same (outer, inner) point.
|
|
83
|
+
"""
|
|
84
|
+
path = event.step_path or event.step_name or ""
|
|
85
|
+
return (
|
|
86
|
+
path,
|
|
87
|
+
getattr(event, "vector_outer_index", None),
|
|
88
|
+
event.vector_index,
|
|
89
|
+
getattr(event, "retry", 0) or 0,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _end_overrides_start(start: Any, end: Any, attr: str) -> dict[str, Any]:
|
|
94
|
+
"""Resolve an inputs-side lane: End overrides Start, Start is the in-flight fallback.
|
|
95
|
+
|
|
96
|
+
A finished block (End event present) carries the post-``configure()`` snapshot
|
|
97
|
+
and wins; while in-flight (no End yet) the overlay reads Start.
|
|
98
|
+
"""
|
|
99
|
+
if end is not None and getattr(end, attr, None):
|
|
100
|
+
return dict(getattr(end, attr))
|
|
101
|
+
if start is not None and getattr(start, attr, None):
|
|
102
|
+
return dict(getattr(start, attr))
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _measurement_event_struct(event: Any) -> dict[str, Any]:
|
|
107
|
+
"""Encode a MeasurementRecorded event into the nested measurement struct.
|
|
108
|
+
|
|
109
|
+
Field order/names match ``_row_helpers.build_measurement_struct`` (and
|
|
110
|
+
``schemas._MEASUREMENT_STRUCT``) so the streaming write and the offline
|
|
111
|
+
write produce identical nested structs.
|
|
112
|
+
"""
|
|
113
|
+
return {
|
|
114
|
+
"name": event.measurement_name,
|
|
115
|
+
"value": event.value,
|
|
116
|
+
"unit": event.unit,
|
|
117
|
+
"outcome": event.outcome,
|
|
118
|
+
"timestamp": event.measurement_timestamp,
|
|
119
|
+
"limit_low": event.limit_low,
|
|
120
|
+
"limit_high": event.limit_high,
|
|
121
|
+
"limit_nominal": event.limit_nominal,
|
|
122
|
+
"limit_comparator": event.limit_comparator,
|
|
123
|
+
"characteristic_id": event.characteristic_id,
|
|
124
|
+
"spec_ref": event.spec_ref,
|
|
125
|
+
"uut_pin": event.uut_pin,
|
|
126
|
+
"fixture_connection": event.fixture_connection,
|
|
127
|
+
"instrument_name": event.instrument_name,
|
|
128
|
+
"instrument_resource": event.instrument_resource,
|
|
129
|
+
"instrument_channel": event.instrument_channel,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class EventAccumulator:
|
|
134
|
+
"""Pure projection of run events into row state — no I/O.
|
|
135
|
+
|
|
136
|
+
The single canonical projection function from the testerkit event
|
|
137
|
+
stream into the parquet row shape. One per in-flight run.
|
|
138
|
+
|
|
139
|
+
Accumulates ``RunStarted`` / ``InstrumentConnected`` /
|
|
140
|
+
``StepsDiscovered`` / ``StepStarted`` / ``MeasurementRecorded`` /
|
|
141
|
+
``StepEnded`` events into in-memory state. ``RunEnded`` is
|
|
142
|
+
handled by subclasses (it's the trigger for the parquet
|
|
143
|
+
write) — the base class doesn't act on it.
|
|
144
|
+
|
|
145
|
+
Snapshot methods (``snapshot_run_row``, ``snapshot_step_rows``,
|
|
146
|
+
``snapshot_measurement_rows``) materialize the current state
|
|
147
|
+
as the same dict shape ``ParquetSubscriber`` writes to disk,
|
|
148
|
+
but without writing anything. Used by the runs daemon to
|
|
149
|
+
surface in-flight runs in queries via an in-memory overlay.
|
|
150
|
+
|
|
151
|
+
The "one applier, multiple destinations" pattern from
|
|
152
|
+
event-sourcing / materialized-view design (Prometheus head
|
|
153
|
+
block, Materialize, RisingWave, CQRS read models): both the
|
|
154
|
+
test runner's parquet writer (``ParquetSubscriber``) and the
|
|
155
|
+
runs daemon's live overlay use this same projection so the
|
|
156
|
+
finalized parquet and the in-flight overlay can never drift.
|
|
157
|
+
|
|
158
|
+
Snapshot return conventions:
|
|
159
|
+
|
|
160
|
+
* ``snapshot_run_row()`` returns ``None`` when no ``RunStarted``
|
|
161
|
+
has been seen (there is no run to project). This is intentionally
|
|
162
|
+
``Optional[dict]`` — a singular row either exists or it doesn't.
|
|
163
|
+
* ``snapshot_step_rows()`` and ``snapshot_measurement_rows()``
|
|
164
|
+
return ``[]`` for the same condition (plural methods follow
|
|
165
|
+
the empty-sequence convention rather than returning ``None``).
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self) -> None:
|
|
169
|
+
self._run_started: Any = None # RunStarted event (run context)
|
|
170
|
+
self._instruments: list[Any] = [] # InstrumentConnected events
|
|
171
|
+
self._reservations: list[Any] = [] # InstrumentReserved events
|
|
172
|
+
self._measurement_events: list[Any] = [] # MeasurementRecorded events
|
|
173
|
+
# ``observe()`` events accumulate here so a vector's observations
|
|
174
|
+
# can ride on its step/vector record's outputs lanes.
|
|
175
|
+
self._observation_events: list[Any] = []
|
|
176
|
+
# Step events keyed by (step_path, step_retry, vector_outer_index) so
|
|
177
|
+
# each sweep variant — each class-container iteration — AND each rerun
|
|
178
|
+
# (step_retry) gets its own entry. ``step_index`` is unique per
|
|
179
|
+
# logical-step within its parent bucket but COLLIDES across parents
|
|
180
|
+
# (a class container at root step_index=0 and its first method at
|
|
181
|
+
# class-bucket step_index=0 would clobber each other under a
|
|
182
|
+
# (step_index, vector_outer_index) key). step_path is unique end-to-end
|
|
183
|
+
# per step; step_retry de-fuses reruns (no overwrite).
|
|
184
|
+
self._step_starts: dict[tuple[str, int, int | None], Any] = {}
|
|
185
|
+
self._step_ends: dict[tuple[str, int, int | None], Any] = {}
|
|
186
|
+
# In-body loop vectors (Mode 2) keyed by (step_path, vector_outer_index,
|
|
187
|
+
# vector_index, retry). Present ONLY when VectorStarted/VectorEnded were
|
|
188
|
+
# emitted; their presence is the Mode-2 signal that produces ``vector`` rows.
|
|
189
|
+
self._vector_starts: dict[tuple[str, int | None, int, int], Any] = {}
|
|
190
|
+
self._vector_ends: dict[tuple[str, int | None, int, int], Any] = {}
|
|
191
|
+
self._run_ended: Any = None # RunEnded event (None for in-flight)
|
|
192
|
+
self._collected_items: list[dict[str, str | int | None]] = []
|
|
193
|
+
# node_id → markers, populated when StepsDiscovered arrives so
|
|
194
|
+
# ``_build_row`` can stamp step_markers on every measurement row
|
|
195
|
+
# without rebuilding the lookup per measurement.
|
|
196
|
+
self._markers_by_node: dict[str, str | None] = {}
|
|
197
|
+
|
|
198
|
+
def on_event(self, event: Any) -> None:
|
|
199
|
+
"""Accumulate one event into in-memory state. No I/O."""
|
|
200
|
+
if isinstance(event, RunStarted):
|
|
201
|
+
self._run_started = event
|
|
202
|
+
elif isinstance(event, InstrumentConnected):
|
|
203
|
+
self._instruments.append(event)
|
|
204
|
+
elif isinstance(event, InstrumentReserved):
|
|
205
|
+
self._reservations.append(event)
|
|
206
|
+
elif isinstance(event, StepsDiscovered):
|
|
207
|
+
self._collected_items = event.items
|
|
208
|
+
markers: dict[str, str | None] = {}
|
|
209
|
+
for ci in event.items:
|
|
210
|
+
nid = ci.get("node_id")
|
|
211
|
+
if isinstance(nid, str) and nid:
|
|
212
|
+
m = ci.get("markers")
|
|
213
|
+
markers[nid] = m if isinstance(m, str) or m is None else str(m)
|
|
214
|
+
self._markers_by_node = markers
|
|
215
|
+
elif isinstance(event, StepStarted):
|
|
216
|
+
self._step_starts[_step_key(event)] = event
|
|
217
|
+
elif isinstance(event, VectorStarted):
|
|
218
|
+
self._vector_starts[_vector_key(event)] = event
|
|
219
|
+
elif isinstance(event, VectorEnded):
|
|
220
|
+
self._vector_ends[_vector_key(event)] = event
|
|
221
|
+
elif isinstance(event, MeasurementRecorded):
|
|
222
|
+
self._measurement_events.append(event)
|
|
223
|
+
elif isinstance(event, Observation):
|
|
224
|
+
self._observation_events.append(event)
|
|
225
|
+
elif isinstance(event, StepEnded):
|
|
226
|
+
self._step_ends[_step_key(event)] = event
|
|
227
|
+
elif isinstance(event, RunEnded):
|
|
228
|
+
self._run_ended = event
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Snapshot — materialize current state as row dicts (no I/O)
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def snapshot_run_row(self) -> dict[str, Any] | None:
|
|
235
|
+
"""Return a single dict matching the runs daemon's ``runs`` row shape.
|
|
236
|
+
|
|
237
|
+
``None`` if no ``RunStarted`` has been seen yet (nothing to
|
|
238
|
+
project). For in-flight runs ``ended_at`` and ``outcome`` are
|
|
239
|
+
``None``; those fields populate when ``RunEnded`` arrives.
|
|
240
|
+
"""
|
|
241
|
+
s = self._run_started
|
|
242
|
+
if not s:
|
|
243
|
+
return None
|
|
244
|
+
ended_at = self._run_ended.occurred_at if self._run_ended else None
|
|
245
|
+
outcome = self._run_ended.outcome if self._run_ended else None
|
|
246
|
+
return {
|
|
247
|
+
"run_id": _safe_str(s.run_id),
|
|
248
|
+
"session_id": _safe_str(s.session_id),
|
|
249
|
+
"site_index": s.site_index,
|
|
250
|
+
"site_name": s.site_name,
|
|
251
|
+
"uut_serial_number": s.uut_serial_number,
|
|
252
|
+
"uut_part_number": s.uut_part_number,
|
|
253
|
+
"uut_revision": s.uut_revision,
|
|
254
|
+
"uut_lot_number": s.uut_lot_number,
|
|
255
|
+
"station_id": s.station_id,
|
|
256
|
+
"station_name": s.station_name,
|
|
257
|
+
"station_hostname": s.station_hostname,
|
|
258
|
+
"station_type": s.station_type,
|
|
259
|
+
"station_location": s.station_location,
|
|
260
|
+
"fixture_id": s.fixture_id,
|
|
261
|
+
"outcome": outcome,
|
|
262
|
+
"started_at": s.occurred_at,
|
|
263
|
+
"ended_at": ended_at,
|
|
264
|
+
"num_measurements": sum(
|
|
265
|
+
1 for e in self._measurement_events if e.measurement_name is not None
|
|
266
|
+
),
|
|
267
|
+
"num_steps": len(set(self._step_starts) | set(self._step_ends)),
|
|
268
|
+
"test_phase": s.test_phase,
|
|
269
|
+
"part_id": s.part_id,
|
|
270
|
+
"part_name": s.part_name,
|
|
271
|
+
"part_revision": s.part_revision,
|
|
272
|
+
"operator_id": s.operator_id,
|
|
273
|
+
"operator_name": s.operator_name,
|
|
274
|
+
"project_name": s.project_name,
|
|
275
|
+
"git_commit": s.git_commit,
|
|
276
|
+
"git_branch": s.git_branch,
|
|
277
|
+
"git_remote": s.git_remote,
|
|
278
|
+
"file_path": None,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
def snapshot_step_rows(self) -> list[dict[str, Any]]:
|
|
282
|
+
"""Return BOTH grains as overlay rows for the daemon's steps tables.
|
|
283
|
+
|
|
284
|
+
Emits the logical-step rows (``vector_index`` NULL) followed by the
|
|
285
|
+
condition-point rows (``vector_index`` 0..N) — mirroring the at-rest
|
|
286
|
+
``steps_materialized`` table, which the daemon exposes via the grain-
|
|
287
|
+
split ``steps`` (NULL) and ``step_vectors`` (0..N) views. Callers that
|
|
288
|
+
want one grain filter on ``vector_index`` (as ``StepsQuery`` does).
|
|
289
|
+
"""
|
|
290
|
+
s = self._run_started
|
|
291
|
+
if not s:
|
|
292
|
+
return []
|
|
293
|
+
ended_at = self._run_ended.occurred_at if self._run_ended else None
|
|
294
|
+
outcome = self._run_ended.outcome if self._run_ended else None
|
|
295
|
+
rows: list[dict[str, Any]] = []
|
|
296
|
+
for entry in self._build_step_results_from_events():
|
|
297
|
+
started_at = _to_datetime(entry.get("started_at"))
|
|
298
|
+
entry_ended_at = _to_datetime(entry.get("ended_at"))
|
|
299
|
+
# Round to microseconds — matches the materialized ROUND(..., 6),
|
|
300
|
+
# so the inflight↔materialized equivalence guard doesn't trip on
|
|
301
|
+
# float64 tails (timestamps are us-resolution anyway).
|
|
302
|
+
duration_s = (
|
|
303
|
+
round((entry_ended_at - started_at).total_seconds(), 6)
|
|
304
|
+
if started_at and entry_ended_at
|
|
305
|
+
else None
|
|
306
|
+
)
|
|
307
|
+
inputs_map, outputs_map = _pack_io_maps(
|
|
308
|
+
entry.get("inputs") or {},
|
|
309
|
+
entry.get("outputs") or {},
|
|
310
|
+
)
|
|
311
|
+
rows.append(
|
|
312
|
+
{
|
|
313
|
+
"run_id": _safe_str(s.run_id),
|
|
314
|
+
"step_index": entry.get("index"),
|
|
315
|
+
"session_id": _safe_str(s.session_id),
|
|
316
|
+
"site_index": s.site_index,
|
|
317
|
+
"site_name": s.site_name,
|
|
318
|
+
"step_name": entry.get("name"),
|
|
319
|
+
"step_path": entry.get("step_path"),
|
|
320
|
+
"vector_index": entry.get("vector_index"),
|
|
321
|
+
"vector_outer_index": entry.get("vector_outer_index"),
|
|
322
|
+
# Logical step: no own vector retry (NULL). Matches
|
|
323
|
+
# steps_materialized, which has no vector_retry column.
|
|
324
|
+
"vector_retry": None,
|
|
325
|
+
"outcome": entry.get("outcome"),
|
|
326
|
+
"started_at": started_at,
|
|
327
|
+
"ended_at": entry_ended_at,
|
|
328
|
+
"duration_s": duration_s,
|
|
329
|
+
"step_retry": entry.get("step_retry", 0),
|
|
330
|
+
"measurement_count": entry.get("measurement_count", 0),
|
|
331
|
+
"markers": entry.get("markers"),
|
|
332
|
+
"uut_serial_number": s.uut_serial_number,
|
|
333
|
+
"station_id": s.station_id,
|
|
334
|
+
"file_path": None,
|
|
335
|
+
"run_outcome": outcome,
|
|
336
|
+
"run_ended_at": ended_at,
|
|
337
|
+
"inputs_map": inputs_map,
|
|
338
|
+
"outputs_map": outputs_map,
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
for entry in self._build_vector_results_from_events():
|
|
342
|
+
v_started = _to_datetime(entry.get("started_at"))
|
|
343
|
+
v_ended = _to_datetime(entry.get("ended_at"))
|
|
344
|
+
v_duration = (
|
|
345
|
+
round((v_ended - v_started).total_seconds(), 6) if v_started and v_ended else None
|
|
346
|
+
)
|
|
347
|
+
inputs_map, outputs_map = _pack_io_maps(
|
|
348
|
+
entry.get("inputs") or {},
|
|
349
|
+
entry.get("outputs") or {},
|
|
350
|
+
)
|
|
351
|
+
rows.append(
|
|
352
|
+
{
|
|
353
|
+
"run_id": _safe_str(s.run_id),
|
|
354
|
+
"step_index": entry.get("index"),
|
|
355
|
+
"session_id": _safe_str(s.session_id),
|
|
356
|
+
"site_index": s.site_index,
|
|
357
|
+
"site_name": s.site_name,
|
|
358
|
+
"step_name": entry.get("name"),
|
|
359
|
+
"step_path": entry.get("step_path"),
|
|
360
|
+
"vector_index": entry.get("vector_index"),
|
|
361
|
+
"vector_outer_index": entry.get("vector_outer_index"),
|
|
362
|
+
# A vector row carries its OWN timing/outcome/retry — the
|
|
363
|
+
# ``vectors_materialized`` grain keys on ``vector_retry``
|
|
364
|
+
# (full snowflake, 0.3.1), so the overlay must carry the
|
|
365
|
+
# vector's own ``retry`` or the inflight/materialized
|
|
366
|
+
# consistency check can't match it 1:1.
|
|
367
|
+
"vector_retry": entry.get("retry", 0),
|
|
368
|
+
"outcome": entry.get("outcome"),
|
|
369
|
+
"started_at": v_started,
|
|
370
|
+
"ended_at": v_ended,
|
|
371
|
+
"duration_s": v_duration,
|
|
372
|
+
"step_retry": entry.get("step_retry", 0),
|
|
373
|
+
"measurement_count": len(entry.get("measurements") or []),
|
|
374
|
+
"markers": None,
|
|
375
|
+
"uut_serial_number": s.uut_serial_number,
|
|
376
|
+
"station_id": s.station_id,
|
|
377
|
+
"file_path": None,
|
|
378
|
+
"run_outcome": outcome,
|
|
379
|
+
"run_ended_at": ended_at,
|
|
380
|
+
"inputs_map": inputs_map,
|
|
381
|
+
"outputs_map": outputs_map,
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
return rows
|
|
385
|
+
|
|
386
|
+
def snapshot_measurement_rows(self) -> list[dict[str, Any]]:
|
|
387
|
+
"""Return flat measurement-fact rows matching the daemon's UNNEST."""
|
|
388
|
+
if not self._run_started:
|
|
389
|
+
return []
|
|
390
|
+
ended_at = self._run_ended.occurred_at if self._run_ended else None
|
|
391
|
+
outcome = self._run_ended.outcome if self._run_ended else None
|
|
392
|
+
vectors_by_key: dict[tuple[str, int | None, int, int], dict[str, Any]] = {}
|
|
393
|
+
for entry in self._build_vector_results_from_events():
|
|
394
|
+
key = (
|
|
395
|
+
entry.get("step_path") or "",
|
|
396
|
+
entry.get("vector_outer_index"),
|
|
397
|
+
entry.get("vector_index", 0),
|
|
398
|
+
entry.get("retry", 0),
|
|
399
|
+
)
|
|
400
|
+
vectors_by_key[key] = entry
|
|
401
|
+
# AT-REST vector_outer_index (may be None) must match the step entry's key.
|
|
402
|
+
steps_by_key: dict[tuple[str, int, int | None], dict[str, Any]] = {}
|
|
403
|
+
for entry in self._build_step_results_from_events():
|
|
404
|
+
key = (
|
|
405
|
+
entry.get("step_path") or "",
|
|
406
|
+
entry.get("step_retry", 0) or 0,
|
|
407
|
+
entry.get("vector_outer_index"),
|
|
408
|
+
)
|
|
409
|
+
steps_by_key[key] = entry
|
|
410
|
+
looped = self._looped_keys()
|
|
411
|
+
rows: list[dict[str, Any]] = []
|
|
412
|
+
for event in self._measurement_events:
|
|
413
|
+
row = self._build_row(event)
|
|
414
|
+
row["run_ended_at"] = ended_at
|
|
415
|
+
row["run_outcome"] = outcome
|
|
416
|
+
# The measurement's enclosing-step retry — a coordinate on
|
|
417
|
+
# measurements_materialized (full snowflake, 0.3.1), so the overlay
|
|
418
|
+
# carries it too for 1:1 matching.
|
|
419
|
+
row["step_retry"] = getattr(event, "step_retry", 0) or 0
|
|
420
|
+
path = event.step_path or event.step_name or ""
|
|
421
|
+
ev_voi = getattr(event, "vector_outer_index", None)
|
|
422
|
+
if (path, ev_voi, event.vector_index) in looped:
|
|
423
|
+
entry = vectors_by_key.get(
|
|
424
|
+
(path, ev_voi, event.vector_index, event.retry or 0)
|
|
425
|
+
) or vectors_by_key.get((path, ev_voi, event.vector_index, 0))
|
|
426
|
+
in_lanes = (entry.get("inputs") if entry else None) or {}
|
|
427
|
+
out_lanes = (entry.get("outputs") if entry else None) or {}
|
|
428
|
+
row["inputs_map"], row["outputs_map"] = _pack_io_maps(in_lanes, out_lanes)
|
|
429
|
+
row["vector_retry"] = entry.get("retry", 0) if entry else 0
|
|
430
|
+
row["vector_outcome"] = entry.get("outcome") if entry else None
|
|
431
|
+
row["step_outcome"] = None
|
|
432
|
+
if entry is not None:
|
|
433
|
+
row["step_started_at"] = _to_datetime(entry.get("step_started_at"))
|
|
434
|
+
row["step_ended_at"] = _to_datetime(entry.get("step_ended_at"))
|
|
435
|
+
else:
|
|
436
|
+
step_entry = steps_by_key.get((path, getattr(event, "step_retry", 0) or 0, ev_voi))
|
|
437
|
+
in_lanes = (step_entry.get("inputs") if step_entry else None) or {}
|
|
438
|
+
out_lanes = (step_entry.get("outputs") if step_entry else None) or {}
|
|
439
|
+
row["inputs_map"], row["outputs_map"] = _pack_io_maps(in_lanes, out_lanes)
|
|
440
|
+
row["vector_index"] = None
|
|
441
|
+
row["vector_retry"] = None
|
|
442
|
+
row["vector_outcome"] = None
|
|
443
|
+
row["step_outcome"] = step_entry.get("outcome") if step_entry else None
|
|
444
|
+
if step_entry is not None:
|
|
445
|
+
row["step_started_at"] = _to_datetime(step_entry.get("started_at"))
|
|
446
|
+
row["step_ended_at"] = _to_datetime(step_entry.get("ended_at"))
|
|
447
|
+
rows.append(row)
|
|
448
|
+
return rows
|
|
449
|
+
|
|
450
|
+
# ------------------------------------------------------------------
|
|
451
|
+
# Pure projection helpers — used by both snapshot and parquet write
|
|
452
|
+
# ------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
def _build_instrument_records(self) -> list[dict[str, Any]]:
|
|
455
|
+
"""Build instrument records from cached InstrumentConnected events."""
|
|
456
|
+
return [
|
|
457
|
+
{
|
|
458
|
+
"name": inst.role,
|
|
459
|
+
"id": inst.instrument_id,
|
|
460
|
+
"driver": inst.driver,
|
|
461
|
+
"resource": inst.resource,
|
|
462
|
+
"protocol": inst.protocol,
|
|
463
|
+
"manufacturer": inst.manufacturer,
|
|
464
|
+
"model": inst.model,
|
|
465
|
+
"serial_number": inst.serial,
|
|
466
|
+
"firmware": inst.firmware,
|
|
467
|
+
"cal_due": inst.cal_due,
|
|
468
|
+
"cal_last": inst.cal_last,
|
|
469
|
+
"cal_certificate": inst.cal_certificate,
|
|
470
|
+
"cal_lab": inst.cal_lab,
|
|
471
|
+
"mocked": inst.mocked,
|
|
472
|
+
}
|
|
473
|
+
for inst in self._instruments
|
|
474
|
+
]
|
|
475
|
+
|
|
476
|
+
def _reserved_instrument_lookups(
|
|
477
|
+
self,
|
|
478
|
+
) -> tuple[dict[tuple[int, int], set[str]], dict[str, dict[str, Any]]]:
|
|
479
|
+
reserved_by_step: dict[tuple[int, int], set[str]] = {}
|
|
480
|
+
structs_by_role: dict[str, dict[str, Any]] = {}
|
|
481
|
+
for ev in self._reservations:
|
|
482
|
+
if ev.step_index is not None:
|
|
483
|
+
reserved_by_step.setdefault(
|
|
484
|
+
(ev.step_index, ev.step_retry if ev.step_retry is not None else 0),
|
|
485
|
+
set(),
|
|
486
|
+
).add(ev.role)
|
|
487
|
+
structs_by_role.setdefault(
|
|
488
|
+
ev.role,
|
|
489
|
+
{
|
|
490
|
+
"name": ev.role,
|
|
491
|
+
"id": ev.instrument_id,
|
|
492
|
+
"driver": None,
|
|
493
|
+
"resource": ev.resource,
|
|
494
|
+
"protocol": None,
|
|
495
|
+
"manufacturer": None,
|
|
496
|
+
"model": None,
|
|
497
|
+
"serial_number": None,
|
|
498
|
+
"firmware": None,
|
|
499
|
+
"cal_due": None,
|
|
500
|
+
"cal_last": None,
|
|
501
|
+
"cal_certificate": None,
|
|
502
|
+
"cal_lab": None,
|
|
503
|
+
"mocked": False,
|
|
504
|
+
},
|
|
505
|
+
)
|
|
506
|
+
for rec in self._build_instrument_records():
|
|
507
|
+
structs_by_role[rec["name"]] = rec
|
|
508
|
+
return reserved_by_step, structs_by_role
|
|
509
|
+
|
|
510
|
+
@staticmethod
|
|
511
|
+
def _min_retry_match(
|
|
512
|
+
cache: dict[tuple[str, int, int | None], Any],
|
|
513
|
+
step_path: str,
|
|
514
|
+
vector_outer_index: int | None,
|
|
515
|
+
) -> Any:
|
|
516
|
+
"""Lowest-retry cached step event for a (step_path, vector_outer_index).
|
|
517
|
+
|
|
518
|
+
Retry-invariant identity lookup: the de-fuse keys steps by
|
|
519
|
+
``(step_path, step_retry, vector_outer_index)``, but a measurement
|
|
520
|
+
event's ``retry`` is the inner vector retry, not ``step_retry``. The
|
|
521
|
+
identity fields a measurement fact reads off the step (node_id, parent,
|
|
522
|
+
module/file/class/function) are the same across reruns, so any matching
|
|
523
|
+
attempt serves; prefer the lowest retry.
|
|
524
|
+
"""
|
|
525
|
+
retries = [r for (p, r, v) in cache if p == step_path and v == vector_outer_index]
|
|
526
|
+
return cache.get((step_path, min(retries), vector_outer_index)) if retries else None
|
|
527
|
+
|
|
528
|
+
@staticmethod
|
|
529
|
+
def _step_event_at(
|
|
530
|
+
cache: dict[tuple[str, int, int | None], Any],
|
|
531
|
+
step_path: str,
|
|
532
|
+
step_retry: int,
|
|
533
|
+
vector_outer_index: int | None,
|
|
534
|
+
) -> Any:
|
|
535
|
+
"""Cached step event for a vector's OWN ``(step_path, step_retry)``.
|
|
536
|
+
|
|
537
|
+
An in-body iteration vector reads its enclosing step's identity and
|
|
538
|
+
timing; the de-fuse keys steps by ``(step_path, step_retry,
|
|
539
|
+
vector_outer_index)``, so a rerun has a distinct StepStarted/StepEnded
|
|
540
|
+
per attempt. Resolving by the vector's own ``step_retry`` makes attempt
|
|
541
|
+
1's iteration vectors read attempt 1's step span — not attempt 0's,
|
|
542
|
+
which the retry-invariant :meth:`_min_retry_match` would return. The
|
|
543
|
+
enclosing leaf step's ``vector_outer_index`` is None for the common
|
|
544
|
+
top-level step, so the ``step_retry``-exact lookup falls through; the
|
|
545
|
+
final retry-invariant match preserves behaviour for parametrized Mode-2
|
|
546
|
+
steps whose StepStarted sits at a nonzero outer index.
|
|
547
|
+
"""
|
|
548
|
+
return (
|
|
549
|
+
cache.get((step_path, step_retry, vector_outer_index))
|
|
550
|
+
or cache.get((step_path, step_retry, None))
|
|
551
|
+
or EventAccumulator._min_retry_match(cache, step_path, vector_outer_index)
|
|
552
|
+
or EventAccumulator._min_retry_match(cache, step_path, None)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
def _step_start_for(self, step_path: str, vector_outer_index: int | None) -> Any:
|
|
556
|
+
return self._min_retry_match(self._step_starts, step_path, vector_outer_index)
|
|
557
|
+
|
|
558
|
+
def _step_end_for(self, step_path: str, vector_outer_index: int | None) -> Any:
|
|
559
|
+
return self._min_retry_match(self._step_ends, step_path, vector_outer_index)
|
|
560
|
+
|
|
561
|
+
def _looped_keys(self) -> set[tuple[str, int | None, int]]:
|
|
562
|
+
"""``(step_path, vector_outer_index, vector_index)`` triples that ran an in-body loop."""
|
|
563
|
+
return {(k[0], k[1], k[2]) for k in (set(self._vector_starts) | set(self._vector_ends))}
|
|
564
|
+
|
|
565
|
+
def _step_start_field(self, step_path: str, vector_outer_index: int | None, attr: str) -> Any:
|
|
566
|
+
"""Get a field from the cached StepStarted event, or None."""
|
|
567
|
+
start = self._step_start_for(step_path, vector_outer_index)
|
|
568
|
+
return getattr(start, attr, None) if start else None
|
|
569
|
+
|
|
570
|
+
def _partition_measurements(
|
|
571
|
+
self,
|
|
572
|
+
) -> dict[tuple[str, int | None, int, int], list[dict[str, Any]]]:
|
|
573
|
+
"""Group measurement structs by enclosing vector, full-identity keyed.
|
|
574
|
+
|
|
575
|
+
Returns ``by_vector`` keyed ``(step_path, vector_outer_index,
|
|
576
|
+
vector_index, vector_retry)`` for measurements that landed inside an
|
|
577
|
+
active vector (Mode-2 in-body or Mode-1/class-outer outer vector).
|
|
578
|
+
Measurements recorded with no own active vector (step-scope) carry the
|
|
579
|
+
outer index on the event but are NOT in ``_vector_starts`` for their
|
|
580
|
+
step_path — the accumulator assigns them to the step row instead (see
|
|
581
|
+
``_build_step_results_from_events``).
|
|
582
|
+
"""
|
|
583
|
+
by_vector: dict[tuple[str, int | None, int, int], list[dict[str, Any]]] = {}
|
|
584
|
+
looped = self._looped_keys()
|
|
585
|
+
for e in self._measurement_events:
|
|
586
|
+
path = e.step_path or e.step_name or ""
|
|
587
|
+
struct = _measurement_event_struct(e)
|
|
588
|
+
if (path, getattr(e, "vector_outer_index", None), e.vector_index) in looped:
|
|
589
|
+
by_vector.setdefault(
|
|
590
|
+
(path, getattr(e, "vector_outer_index", None), e.vector_index, e.retry or 0), []
|
|
591
|
+
).append(struct)
|
|
592
|
+
return by_vector
|
|
593
|
+
|
|
594
|
+
def _build_vector_results_from_events(self) -> list[dict[str, Any]]:
|
|
595
|
+
"""Build vector manifest entries from VectorStarted/VectorEnded.
|
|
596
|
+
|
|
597
|
+
One entry per emitted vector — every sweep point emits these events:
|
|
598
|
+
function ``@parametrize``, class-outer ``testerkit_sweeps``, and in-body
|
|
599
|
+
``vectors`` / ``run_vector`` alike. A non-swept step emits none, so it
|
|
600
|
+
contributes no vector rows (its data rides the ``step`` record).
|
|
601
|
+
|
|
602
|
+
One entry per ``(step_path, vector_index, retry)`` execution. The
|
|
603
|
+
enclosing leaf step's identity (node_id / file / class / function /
|
|
604
|
+
timing) is sourced from its StepStarted when present.
|
|
605
|
+
"""
|
|
606
|
+
by_iteration = self._partition_measurements()
|
|
607
|
+
reserved_by_step, structs_by_role = self._reserved_instrument_lookups()
|
|
608
|
+
entries: list[dict[str, Any]] = []
|
|
609
|
+
keys = sorted(
|
|
610
|
+
set(self._vector_starts) | set(self._vector_ends),
|
|
611
|
+
key=lambda k: (k[2], k[3], k[0]),
|
|
612
|
+
)
|
|
613
|
+
# vector_retry = the 0-based occurrence ordinal of (step_path,
|
|
614
|
+
# vector_outer_index, vector_index) across the whole run — a step rerun
|
|
615
|
+
# AND an in-body retry both re-execute the point and both count (cause is
|
|
616
|
+
# irrelevant). It is sourced at emit (RunScope.next_vector_occurrence
|
|
617
|
+
# stamps it onto VectorStarted.retry) and rides the event as ``key[3]``
|
|
618
|
+
# here, so a step rerun's vectors are DISTINCT keys (not fused) and the
|
|
619
|
+
# inflight overlay and materialized parquet — both reading this one
|
|
620
|
+
# builder — agree.
|
|
621
|
+
for key in keys:
|
|
622
|
+
path, vec_outer, vec, retry = key
|
|
623
|
+
start = self._vector_starts.get(key)
|
|
624
|
+
end = self._vector_ends.get(key)
|
|
625
|
+
ref = start or end
|
|
626
|
+
step_retry_v = getattr(ref, "step_retry", 0) or 0 if ref else 0
|
|
627
|
+
step_start = self._step_event_at(self._step_starts, path, step_retry_v, vec_outer)
|
|
628
|
+
step_end = self._step_event_at(self._step_ends, path, step_retry_v, vec_outer)
|
|
629
|
+
node_id = getattr(ref, "node_id", None) or (step_start.node_id if step_start else None)
|
|
630
|
+
inputs = _end_overrides_start(start, end, "inputs")
|
|
631
|
+
outputs = dict(end.outputs) if end and getattr(end, "outputs", None) else {}
|
|
632
|
+
input_units = _end_overrides_start(start, end, "input_units")
|
|
633
|
+
output_units = (
|
|
634
|
+
dict(end.output_units) if end and getattr(end, "output_units", None) else {}
|
|
635
|
+
)
|
|
636
|
+
output_pins = dict(end.output_pins) if end and getattr(end, "output_pins", None) else {}
|
|
637
|
+
step_idx_v = ref.step_index if ref else 0
|
|
638
|
+
v_roles = reserved_by_step.get((step_idx_v, step_retry_v), set())
|
|
639
|
+
v_instruments = [structs_by_role[r] for r in v_roles if r in structs_by_role]
|
|
640
|
+
entries.append(
|
|
641
|
+
vector_entry_dict(
|
|
642
|
+
index=step_idx_v,
|
|
643
|
+
name=ref.step_name if ref else "",
|
|
644
|
+
node_id=node_id,
|
|
645
|
+
file=step_start.file if step_start else None,
|
|
646
|
+
function=step_start.function if step_start else None,
|
|
647
|
+
class_name=step_start.class_name if step_start else None,
|
|
648
|
+
module=step_start.module if step_start else None,
|
|
649
|
+
step_path=ref.step_path if ref else path,
|
|
650
|
+
markers=self._markers_by_node.get(node_id) if node_id else None,
|
|
651
|
+
step_started_at=step_start.occurred_at if step_start else None,
|
|
652
|
+
step_ended_at=step_end.occurred_at if step_end else None,
|
|
653
|
+
vector_index=vec,
|
|
654
|
+
vector_outer_index=vec_outer,
|
|
655
|
+
retry=retry,
|
|
656
|
+
step_retry=step_retry_v,
|
|
657
|
+
outcome=end.outcome if end else None,
|
|
658
|
+
started_at=start.occurred_at if start else None,
|
|
659
|
+
ended_at=end.occurred_at if end else None,
|
|
660
|
+
inputs=inputs,
|
|
661
|
+
outputs=outputs,
|
|
662
|
+
input_units=input_units,
|
|
663
|
+
output_units=output_units,
|
|
664
|
+
output_pins=output_pins,
|
|
665
|
+
measurements=by_iteration.get(key, []),
|
|
666
|
+
instrument_records=v_instruments,
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
return entries
|
|
670
|
+
|
|
671
|
+
def _build_row(self, event: Any) -> dict[str, Any]:
|
|
672
|
+
"""Denormalize a MeasurementRecorded event into a flat row dict."""
|
|
673
|
+
idx = event.step_index
|
|
674
|
+
vec = event.vector_index
|
|
675
|
+
path = event.step_path or event.step_name or ""
|
|
676
|
+
start = self._step_start_for(path, vec)
|
|
677
|
+
end = self._step_end_for(path, vec)
|
|
678
|
+
node_id = start.node_id if start else None
|
|
679
|
+
# A measurement fact mirrors its carrier's vector_index (NULL for an
|
|
680
|
+
# ambient/step-scope measurement, 0..N for a vector-scope one), so it's
|
|
681
|
+
# built as record_type='measurement' (unconstrained by the grain guard)
|
|
682
|
+
# rather than as a 'vector' row — which would fail the guard when the
|
|
683
|
+
# measurement is ambient (vector_index=NULL).
|
|
684
|
+
row = RunParquetRow(
|
|
685
|
+
record_type="measurement",
|
|
686
|
+
**run_context_from_run_started(self._run_started, event, include_env=True),
|
|
687
|
+
step_name=event.step_name,
|
|
688
|
+
step_index=idx,
|
|
689
|
+
step_path=event.step_path or event.step_name,
|
|
690
|
+
step_started_at=start.occurred_at if start else None,
|
|
691
|
+
step_ended_at=end.occurred_at if end else None,
|
|
692
|
+
step_node_id=node_id,
|
|
693
|
+
step_module=self._step_start_field(path, vec, "module"),
|
|
694
|
+
step_file=self._step_start_field(path, vec, "file"),
|
|
695
|
+
step_class=self._step_start_field(path, vec, "class_name"),
|
|
696
|
+
step_function=self._step_start_field(path, vec, "function"),
|
|
697
|
+
step_markers=self._markers_by_node.get(node_id) if node_id else None,
|
|
698
|
+
step_outcome=end.outcome if end else None,
|
|
699
|
+
vector_index=vec,
|
|
700
|
+
vector_outer_index=getattr(event, "vector_outer_index", None),
|
|
701
|
+
vector_retry=event.retry,
|
|
702
|
+
step_retry=getattr(event, "step_retry", 0) or 0,
|
|
703
|
+
measurement_name=event.measurement_name,
|
|
704
|
+
measurement_timestamp=event.measurement_timestamp,
|
|
705
|
+
measurement_value=event.value,
|
|
706
|
+
measurement_unit=event.unit,
|
|
707
|
+
measurement_outcome=event.outcome,
|
|
708
|
+
limit_low=event.limit_low,
|
|
709
|
+
limit_high=event.limit_high,
|
|
710
|
+
limit_nominal=event.limit_nominal,
|
|
711
|
+
limit_comparator=event.limit_comparator,
|
|
712
|
+
characteristic_id=event.characteristic_id,
|
|
713
|
+
spec_ref=event.spec_ref,
|
|
714
|
+
uut_pin=event.uut_pin,
|
|
715
|
+
fixture_connection=event.fixture_connection,
|
|
716
|
+
instrument_name=event.instrument_name,
|
|
717
|
+
instrument_resource=event.instrument_resource,
|
|
718
|
+
instrument_channel=event.instrument_channel,
|
|
719
|
+
run_outcome=None,
|
|
720
|
+
# v2: a measurement references (does not copy) its vector — the
|
|
721
|
+
# in/out conditions live on the (scope or in-body) vector record;
|
|
722
|
+
# the EAV join resolves them by the shared vector key.
|
|
723
|
+
inputs={},
|
|
724
|
+
outputs={},
|
|
725
|
+
instruments=self._build_instrument_records(),
|
|
726
|
+
)
|
|
727
|
+
flat = row.to_flat_dict()
|
|
728
|
+
# The nested measurements list has no place on the flat fact row. And
|
|
729
|
+
# record_type — set above only to satisfy the at-rest model and bypass
|
|
730
|
+
# the grain guard — is a projection vestige (the measurement fact is
|
|
731
|
+
# all-measurement by construction), so it's dropped from the inflight
|
|
732
|
+
# projection row too.
|
|
733
|
+
flat.pop("measurements", None)
|
|
734
|
+
flat.pop("record_type", None)
|
|
735
|
+
return flat
|
|
736
|
+
|
|
737
|
+
def _build_step_results_from_events(self) -> list[dict[str, Any]]:
|
|
738
|
+
"""Build step manifest from cached StepStarted/StepEnded events.
|
|
739
|
+
|
|
740
|
+
Each entry corresponds to one ``(step_path, step_retry, vector_index)``
|
|
741
|
+
execution. A swept step running 4 vectors produces 4 manifest entries
|
|
742
|
+
with the same ``step_path`` but ``vector_index`` 0..3; a rerun adds a
|
|
743
|
+
fresh entry at ``step_retry`` N+1 (the de-fuse — reruns are distinct
|
|
744
|
+
rows, never fused).
|
|
745
|
+
|
|
746
|
+
Partially-run sweeps (some but not all planned vectors ran)
|
|
747
|
+
also produce manifest entries for the unrun vectors with
|
|
748
|
+
``outcome=None`` — surfaced via ``_append_not_started`` from the
|
|
749
|
+
collected items that never appeared in the executed events.
|
|
750
|
+
"""
|
|
751
|
+
manifest: list[dict[str, Any]] = []
|
|
752
|
+
executed_node_ids: set[str] = set()
|
|
753
|
+
executed_vectors: set[tuple[str, int | None]] = set()
|
|
754
|
+
|
|
755
|
+
looped = self._looped_keys()
|
|
756
|
+
# Step-scope measurements only; swept steps' vector measurements count on vector rows.
|
|
757
|
+
step_scope_meas: dict[tuple[str, int, int | None], list[dict[str, Any]]] = {}
|
|
758
|
+
for e in self._measurement_events:
|
|
759
|
+
path = e.step_path or e.step_name or ""
|
|
760
|
+
key = (path, getattr(e, "step_retry", 0) or 0, getattr(e, "vector_outer_index", None))
|
|
761
|
+
if (path, getattr(e, "vector_outer_index", None), e.vector_index) not in looped:
|
|
762
|
+
step_scope_meas.setdefault(key, []).append(_measurement_event_struct(e))
|
|
763
|
+
|
|
764
|
+
# Observations per vector key — merged into the step entry's outputs
|
|
765
|
+
# so the step record carries the vector's observations on its lanes.
|
|
766
|
+
obs_by_key: dict[tuple[str, int | None], dict[str, Any]] = {}
|
|
767
|
+
obs_units_by_key: dict[tuple[str, int | None], dict[str, str]] = {}
|
|
768
|
+
obs_pins_by_key: dict[tuple[str, int | None], dict[str, str]] = {}
|
|
769
|
+
for ev in self._observation_events:
|
|
770
|
+
if ev.name.startswith("_"):
|
|
771
|
+
continue
|
|
772
|
+
okey = (ev.step_path or ev.step_name or "", ev.vector_index)
|
|
773
|
+
obs_by_key.setdefault(okey, {}).setdefault(ev.name, ev.value)
|
|
774
|
+
if getattr(ev, "unit", None):
|
|
775
|
+
obs_units_by_key.setdefault(okey, {}).setdefault(ev.name, ev.unit)
|
|
776
|
+
if getattr(ev, "uut_pin", None) is not None:
|
|
777
|
+
obs_pins_by_key.setdefault(okey, {}).setdefault(ev.name, ev.uut_pin)
|
|
778
|
+
|
|
779
|
+
# Sort keys by the producer-assigned (step_index, vector_outer_index, retry)
|
|
780
|
+
# so the resulting manifest preserves execution order regardless of the
|
|
781
|
+
# alphabetical position of step_path. Falls back to the key itself
|
|
782
|
+
# for events that didn't set step_index (zero-default).
|
|
783
|
+
def _sort_key(k: tuple[str, int, int | None]) -> tuple[int, int, int, str]:
|
|
784
|
+
ev = self._step_starts.get(k) or self._step_ends.get(k)
|
|
785
|
+
step_index = getattr(ev, "step_index", 0) if ev else 0
|
|
786
|
+
# k = (path, step_retry, vector_outer_index)
|
|
787
|
+
return (step_index, k[2] if k[2] is not None else -1, k[1], k[0])
|
|
788
|
+
|
|
789
|
+
reserved_by_step, structs_by_role = self._reserved_instrument_lookups()
|
|
790
|
+
all_keys = sorted(set(self._step_starts) | set(self._step_ends), key=_sort_key)
|
|
791
|
+
emitted_step_keys: set[tuple[str, int, int | None]] = set()
|
|
792
|
+
for key in all_keys:
|
|
793
|
+
path, step_retry, vec = key
|
|
794
|
+
start = self._step_starts.get(key)
|
|
795
|
+
end = self._step_ends.get(key)
|
|
796
|
+
node_id = start.node_id if start else None
|
|
797
|
+
if node_id:
|
|
798
|
+
executed_node_ids.add(node_id)
|
|
799
|
+
# ``executed_vectors`` is keyed by (step_path, vector_outer_index)
|
|
800
|
+
# so _append_not_started can correctly identify which CIs already
|
|
801
|
+
# ran — pytest parametrize variants share one logical step_path but
|
|
802
|
+
# have distinct node_ids, so keying by node_id misses cross-CI
|
|
803
|
+
# matches. The ``else 0`` is a DEDUP-ONLY placeholder for a NULL
|
|
804
|
+
# outer index (top-level step) — it is not a real vector index and
|
|
805
|
+
# never lands on a row; it only has to agree with
|
|
806
|
+
# _append_not_started's matching ``ci.get("vector_index") or 0``.
|
|
807
|
+
executed_vectors.add((path, vec if vec is not None else 0))
|
|
808
|
+
emitted_step_keys.add(key)
|
|
809
|
+
step_meas = step_scope_meas.get((path, step_retry, vec), [])
|
|
810
|
+
entry = self._build_step_entry(
|
|
811
|
+
key,
|
|
812
|
+
start,
|
|
813
|
+
end,
|
|
814
|
+
len(step_meas),
|
|
815
|
+
# Observations are keyed by their own vector_index (NULL for an
|
|
816
|
+
# ambient/step-scope observation). A top-level step's ``vec``
|
|
817
|
+
# (its enclosing vector_outer_index) is NULL too, so match on
|
|
818
|
+
# ``vec`` directly — coercing to 0 would miss the NULL-keyed
|
|
819
|
+
# ambient observations (now that observe() stamps NULL ambient).
|
|
820
|
+
obs_by_key.get((path, vec), {}),
|
|
821
|
+
obs_units_by_key.get((path, vec), {}),
|
|
822
|
+
obs_pins_by_key.get((path, vec), {}),
|
|
823
|
+
step_measurements=step_meas,
|
|
824
|
+
)
|
|
825
|
+
s_idx = entry.get("index", 0)
|
|
826
|
+
s_retry = entry.get("step_retry", 0) or 0
|
|
827
|
+
s_roles = reserved_by_step.get((s_idx, s_retry), set())
|
|
828
|
+
entry["instrument_records"] = [
|
|
829
|
+
structs_by_role[r] for r in s_roles if r in structs_by_role
|
|
830
|
+
]
|
|
831
|
+
manifest.append(entry)
|
|
832
|
+
|
|
833
|
+
# Orphan: no StepStarted/StepEnded; synthesize minimal so measurements are never dropped.
|
|
834
|
+
for mkey, structs in step_scope_meas.items():
|
|
835
|
+
if mkey in emitted_step_keys:
|
|
836
|
+
continue
|
|
837
|
+
path, step_retry, vec = mkey
|
|
838
|
+
m_event = next(
|
|
839
|
+
(
|
|
840
|
+
e
|
|
841
|
+
for e in self._measurement_events
|
|
842
|
+
if (e.step_path or e.step_name or "") == path
|
|
843
|
+
and getattr(e, "vector_outer_index", None) == vec
|
|
844
|
+
and (getattr(e, "step_retry", 0) or 0) == step_retry
|
|
845
|
+
),
|
|
846
|
+
None,
|
|
847
|
+
)
|
|
848
|
+
executed_vectors.add((path, vec if vec is not None else 0))
|
|
849
|
+
manifest.append(
|
|
850
|
+
step_entry_dict(
|
|
851
|
+
index=m_event.step_index if m_event else 0,
|
|
852
|
+
name=(m_event.step_name if m_event else "") or "",
|
|
853
|
+
node_id=None,
|
|
854
|
+
file=None,
|
|
855
|
+
function=None,
|
|
856
|
+
class_name=None,
|
|
857
|
+
module=None,
|
|
858
|
+
step_path=path,
|
|
859
|
+
description=None,
|
|
860
|
+
markers=None,
|
|
861
|
+
outcome=None,
|
|
862
|
+
started_at=None,
|
|
863
|
+
ended_at=None,
|
|
864
|
+
vector_index=None,
|
|
865
|
+
vector_outer_index=vec,
|
|
866
|
+
measurements=structs,
|
|
867
|
+
measurement_count=len(structs),
|
|
868
|
+
step_retry=step_retry,
|
|
869
|
+
)
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# A @parametrize variant emits its own VectorStarted at (step_path,
|
|
873
|
+
# vector_index), but the step-fusion key collapses all top-level
|
|
874
|
+
# variants onto vector_outer_index (None→0), so the loop above only
|
|
875
|
+
# recorded (step_path, 0) in ``executed_vectors``. Record every vector
|
|
876
|
+
# that actually emitted, keyed by its OWN vector_index — the same axis
|
|
877
|
+
# ``_append_not_started`` matches collected items on — so a ran-but-
|
|
878
|
+
# fused variant (vi=1,2,…) is not mistaken for never-run and ghosted.
|
|
879
|
+
# A genuinely unrun vector emits no event, so a partially-run sweep
|
|
880
|
+
# still leaves its unrun positions absent (their "never ran" rows are
|
|
881
|
+
# preserved). Keyed by (step_path, vector_index), matching the check.
|
|
882
|
+
for vkey in set(self._vector_starts) | set(self._vector_ends):
|
|
883
|
+
# vkey = (step_path, vector_outer_index, vector_index, retry)
|
|
884
|
+
executed_vectors.add((vkey[0], vkey[2]))
|
|
885
|
+
|
|
886
|
+
_append_not_started(
|
|
887
|
+
manifest,
|
|
888
|
+
self._collected_items,
|
|
889
|
+
executed_node_ids,
|
|
890
|
+
executed_vectors=executed_vectors,
|
|
891
|
+
)
|
|
892
|
+
return manifest
|
|
893
|
+
|
|
894
|
+
def _build_step_entry(
|
|
895
|
+
self,
|
|
896
|
+
key: tuple[str, int, int | None],
|
|
897
|
+
start: Any | None,
|
|
898
|
+
end: Any | None,
|
|
899
|
+
meas_count: int,
|
|
900
|
+
observations: dict[str, Any],
|
|
901
|
+
observation_units: dict[str, str] | None = None,
|
|
902
|
+
observation_pins: dict[str, str] | None = None,
|
|
903
|
+
step_measurements: list[dict[str, Any]] | None = None,
|
|
904
|
+
) -> dict[str, Any]:
|
|
905
|
+
"""Build one step manifest entry from cached StepStarted/StepEnded."""
|
|
906
|
+
path, step_retry, vec = key
|
|
907
|
+
# ``step_index`` for the manifest entry comes from the StepStarted
|
|
908
|
+
# event itself — ``step_path`` is the dict key (unique per logical
|
|
909
|
+
# step) and ``step_index`` is the per-bucket index the producer
|
|
910
|
+
# assigned. The two are distinct concepts now that containers and
|
|
911
|
+
# methods can share ``step_index`` across their respective buckets.
|
|
912
|
+
idx = start.step_index if start else (end.step_index if end else 0)
|
|
913
|
+
node_id = start.node_id if start else None
|
|
914
|
+
inputs = _end_overrides_start(start, end, "inputs")
|
|
915
|
+
outputs = dict(end.outputs) if end and getattr(end, "outputs", None) else {}
|
|
916
|
+
input_units = _end_overrides_start(start, end, "input_units")
|
|
917
|
+
output_units = dict(end.output_units) if end and getattr(end, "output_units", None) else {}
|
|
918
|
+
output_pins = dict(end.output_pins) if end and getattr(end, "output_pins", None) else {}
|
|
919
|
+
# Merge accumulated observations for this vector so the in-flight
|
|
920
|
+
# step row carries the same out_* the materialized step aggregates
|
|
921
|
+
# (StepEnded.outputs already holds them once it arrives merged; this
|
|
922
|
+
# covers the pre-merge / direct-event projection path).
|
|
923
|
+
for obs_name, obs_value in observations.items():
|
|
924
|
+
outputs.setdefault(obs_name, obs_value)
|
|
925
|
+
for obs_name, obs_unit in (observation_units or {}).items():
|
|
926
|
+
output_units.setdefault(obs_name, obs_unit)
|
|
927
|
+
for obs_name, obs_pin in (observation_pins or {}).items():
|
|
928
|
+
output_pins.setdefault(obs_name, obs_pin)
|
|
929
|
+
return step_entry_dict(
|
|
930
|
+
index=idx,
|
|
931
|
+
name=start.step_name if start else (end.step_name if end else ""),
|
|
932
|
+
node_id=node_id,
|
|
933
|
+
file=start.file if start else None,
|
|
934
|
+
function=start.function if start else None,
|
|
935
|
+
class_name=start.class_name if start else None,
|
|
936
|
+
module=start.module if start else None,
|
|
937
|
+
step_path=(
|
|
938
|
+
(start.step_path if start else end.step_path if end else "")
|
|
939
|
+
or (start.step_name if start else end.step_name if end else "")
|
|
940
|
+
),
|
|
941
|
+
description=start.description if start else None,
|
|
942
|
+
markers=self._markers_by_node.get(node_id) if node_id else None,
|
|
943
|
+
outcome=end.outcome if end else None,
|
|
944
|
+
started_at=start.occurred_at if start else None,
|
|
945
|
+
ended_at=end.occurred_at if end else None,
|
|
946
|
+
vector_index=None,
|
|
947
|
+
vector_outer_index=vec,
|
|
948
|
+
inputs=inputs,
|
|
949
|
+
outputs=outputs,
|
|
950
|
+
input_units=input_units,
|
|
951
|
+
output_units=output_units,
|
|
952
|
+
output_pins=output_pins,
|
|
953
|
+
measurements=step_measurements or [],
|
|
954
|
+
measurement_count=meas_count,
|
|
955
|
+
step_retry=step_retry,
|
|
956
|
+
instrument_records=[],
|
|
957
|
+
)
|