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,413 @@
|
|
|
1
|
+
"""Read-only query client over the runs DuckDB daemon's ``steps`` table.
|
|
2
|
+
|
|
3
|
+
Steps are populated by aggregating the unified per-run parquet on
|
|
4
|
+
ingest (see :mod:`testerkit.data._runs_duckdb_daemon`). Queries hit the
|
|
5
|
+
precomputed table for constant-cost lookups regardless of file count.
|
|
6
|
+
|
|
7
|
+
Pairs with :class:`MeasurementsQuery` (raw measurement view) and
|
|
8
|
+
:class:`RunsQuery` (run-level summaries) — same daemon, same Flight
|
|
9
|
+
client, different storage shape.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from testerkit.analysis.measurement_facets import ColumnSchema, FixedColumnDescriptor
|
|
22
|
+
from testerkit.data import runs_duckdb_manager
|
|
23
|
+
from testerkit.data._flight_query import FlightQueryClient
|
|
24
|
+
from testerkit.data._sql_helpers import multi_filter_clauses, sql_escape
|
|
25
|
+
from testerkit.data.backends._row_helpers import _decode_io_maps
|
|
26
|
+
from testerkit.data.data_dir import resolve_data_dir
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Read-time inputs/outputs map, LEFT JOINed onto ``steps``/``step_vectors``
|
|
31
|
+
# (aliased ``s``) from the ``inputs``/``outputs`` tables (projection-
|
|
32
|
+
# normalization, 0.3.1 — replaces the stored, prefixed ``dynamic_attrs`` MAP).
|
|
33
|
+
# Grouped by the step's own FK coordinates INCLUDING ``step_retry`` (unlike
|
|
34
|
+
# the measurements-side join in ``run_store.get_measurements`` — a step's own
|
|
35
|
+
# grain has ``step_retry`` as part of its PK, so the join must too or two
|
|
36
|
+
# reruns of the same step fan out into each other's values).
|
|
37
|
+
#
|
|
38
|
+
# This join serves FINALIZED rows (whose inputs/outputs are in the tables). A
|
|
39
|
+
# LIVE run's rows aren't in the tables yet — for those the ``steps`` /
|
|
40
|
+
# ``step_vectors`` VIEW carries the inflight ``inputs_map`` / ``outputs_map``
|
|
41
|
+
# inline. ``_STEP_IO_SELECT`` COALESCEs the join (finalized) over the view
|
|
42
|
+
# column (live), so exactly one is non-empty per row.
|
|
43
|
+
_STEP_IO_VALUE_EXPR = """CASE value_type
|
|
44
|
+
WHEN 'scalar:bool' THEN CASE WHEN value_bool THEN 'true' ELSE 'false' END
|
|
45
|
+
WHEN 'scalar:int' THEN CAST(value_int AS VARCHAR)
|
|
46
|
+
WHEN 'scalar:float' THEN CAST(value_double AS VARCHAR)
|
|
47
|
+
WHEN 'scalar:datetime' THEN CAST(value_timestamp AS VARCHAR)
|
|
48
|
+
WHEN 'list' THEN value_json
|
|
49
|
+
WHEN 'dict' THEN value_json
|
|
50
|
+
ELSE value_text
|
|
51
|
+
END"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _step_io_join(alias: str, table: str) -> str:
|
|
55
|
+
"""LEFT JOIN clause aggregating ``table`` (``inputs``/``outputs``) into
|
|
56
|
+
``{alias}.io_map`` per (run_id, step_path, step_retry, vector_index,
|
|
57
|
+
vector_outer_index), joined onto the ``s`` alias."""
|
|
58
|
+
return f"""
|
|
59
|
+
LEFT JOIN (
|
|
60
|
+
SELECT run_id, step_path, step_retry, vector_index, vector_outer_index,
|
|
61
|
+
MAP(list(name), list({_STEP_IO_VALUE_EXPR})) AS io_map
|
|
62
|
+
FROM {table}
|
|
63
|
+
GROUP BY run_id, step_path, step_retry, vector_index, vector_outer_index
|
|
64
|
+
) AS {alias} ON {alias}.run_id = s.run_id
|
|
65
|
+
AND {alias}.step_path = s.step_path
|
|
66
|
+
AND {alias}.step_retry = s.step_retry
|
|
67
|
+
AND {alias}.vector_index IS NOT DISTINCT FROM s.vector_index
|
|
68
|
+
AND {alias}.vector_outer_index IS NOT DISTINCT FROM s.vector_outer_index
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_STEP_IO_JOINS = _step_io_join("inputs_agg", "inputs") + _step_io_join("outputs_agg", "outputs")
|
|
73
|
+
|
|
74
|
+
# SELECT list for step reads: every ``s`` column except the two view-carried
|
|
75
|
+
# maps (live side), replaced by a COALESCE that prefers the finalized join
|
|
76
|
+
# aggregate. The LEFT JOIN's ``io_map`` is NULL when it misses (finalized rows
|
|
77
|
+
# absent → live run) and a non-empty MAP when it hits (a GROUP BY over ≥1 input
|
|
78
|
+
# row always yields ≥1 entry), so COALESCE picks finalized-when-present, else
|
|
79
|
+
# the view's inflight map.
|
|
80
|
+
_STEP_IO_SELECT = (
|
|
81
|
+
"s.* EXCLUDE (inputs_map, outputs_map), "
|
|
82
|
+
"COALESCE(inputs_agg.io_map, s.inputs_map) AS inputs_map, "
|
|
83
|
+
"COALESCE(outputs_agg.io_map, s.outputs_map) AS outputs_map"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class StepRow(BaseModel):
|
|
88
|
+
"""One row from the ``steps`` table — full denormalized run + step context.
|
|
89
|
+
|
|
90
|
+
Mirrors the columns the daemon's ``steps`` table carries (see
|
|
91
|
+
``_rebuild_schema``). Field names match the daemon's column names
|
|
92
|
+
so callers can construct via ``StepRow(**dict_row)`` from
|
|
93
|
+
``_query_dicts`` output.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
file_path: str | None = None
|
|
97
|
+
run_id: str | None = None
|
|
98
|
+
session_id: str | None = None
|
|
99
|
+
# Optional to tolerate the pre-RunStarted-correlation in-flight row
|
|
100
|
+
# (see ``_row_helpers.py``'s placeholder branch); persisted/correlated
|
|
101
|
+
# rows always carry 0+.
|
|
102
|
+
site_index: int | None = None
|
|
103
|
+
site_name: str | None = None
|
|
104
|
+
step_index: int | None = None
|
|
105
|
+
step_name: str | None = None
|
|
106
|
+
step_path: str | None = None
|
|
107
|
+
parent_path: str | None = None
|
|
108
|
+
# vector_index: NULL for step rows; 0..N-1 for vector rows (own position
|
|
109
|
+
# within the sweep). vector_outer_index: NULL at top level; the enclosing
|
|
110
|
+
# outer (class) vector index for method steps nested inside a swept class.
|
|
111
|
+
vector_index: int | None = None
|
|
112
|
+
vector_outer_index: int | None = None
|
|
113
|
+
outcome: str | None = None
|
|
114
|
+
started_at: datetime | None = None
|
|
115
|
+
ended_at: datetime | None = None
|
|
116
|
+
duration_s: float | None = None
|
|
117
|
+
measurement_count: int | None = None
|
|
118
|
+
# 0-based outer (item) retry — pytest-rerunfailures rerun count of this
|
|
119
|
+
# step. ``0`` for the first attempt; ``N`` for the Nth rerun. Each
|
|
120
|
+
# execution is its own row, so ``WHERE step_retry > 0`` finds reruns.
|
|
121
|
+
step_retry: int | None = None
|
|
122
|
+
markers: str | None = None
|
|
123
|
+
uut_serial_number: str | None = None
|
|
124
|
+
station_id: str | None = None
|
|
125
|
+
# Per-vector commanded inputs and recorded outputs — decoded from the
|
|
126
|
+
# query-time inputs_map/outputs_map (see _STEP_IO_JOINS) into a dict per
|
|
127
|
+
# (step, vector). Empty for steps with no inputs/outputs rows.
|
|
128
|
+
inputs: dict[str, Any] = Field(default_factory=dict)
|
|
129
|
+
outputs: dict[str, Any] = Field(default_factory=dict)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class StepNode(BaseModel):
|
|
133
|
+
"""One node in a hierarchical step tree, built from ``step_path``.
|
|
134
|
+
|
|
135
|
+
``step_path`` uses ``/`` as the separator (e.g.
|
|
136
|
+
``power/output/voltage``) so the tree is constructed client-side
|
|
137
|
+
by splitting on it. Roots are the top-level steps; leaves are the
|
|
138
|
+
actual test steps.
|
|
139
|
+
|
|
140
|
+
A node's identity is its step-summary row (``vector_index IS NULL``);
|
|
141
|
+
its ``vectors`` are that step's own vector rows (``vector_index``
|
|
142
|
+
0..N — a swept step's condition points, or a single row for a plain
|
|
143
|
+
step), never siblings/children in the tree.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
step: StepRow
|
|
147
|
+
vectors: list[StepRow] = Field(default_factory=list)
|
|
148
|
+
children: list[StepNode] = Field(default_factory=list)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class StepsQuery:
|
|
152
|
+
"""Read-only client over the runs daemon's ``steps`` table.
|
|
153
|
+
|
|
154
|
+
Construct once and reuse — no explicit close needed::
|
|
155
|
+
|
|
156
|
+
q = StepsQuery()
|
|
157
|
+
rows = q.list_for_run("run-001-abc")
|
|
158
|
+
tree = q.tree_for_run("run-001-abc")
|
|
159
|
+
|
|
160
|
+
Or use as a context manager for deterministic cleanup::
|
|
161
|
+
|
|
162
|
+
with StepsQuery() as q:
|
|
163
|
+
rows = q.list_for_run("run-001-abc")
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self, *, _data_dir: Path | str | None = None) -> None:
|
|
167
|
+
data_dir = resolve_data_dir(_data_dir)
|
|
168
|
+
self._runs_dir = data_dir / "runs"
|
|
169
|
+
self._runs_dir.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
|
|
171
|
+
location = runs_duckdb_manager.acquire(self._runs_dir)
|
|
172
|
+
self._flight = FlightQueryClient(
|
|
173
|
+
location,
|
|
174
|
+
"runs",
|
|
175
|
+
reacquire=lambda: runs_duckdb_manager.acquire(self._runs_dir),
|
|
176
|
+
label="StepsQuery",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _query_dicts(self, sql: str) -> list[dict[str, Any]]:
|
|
180
|
+
return self._flight.query(sql)
|
|
181
|
+
|
|
182
|
+
def close(self) -> None:
|
|
183
|
+
"""Release daemon ref and close Flight client."""
|
|
184
|
+
self._flight.close()
|
|
185
|
+
runs_duckdb_manager.release(self._runs_dir)
|
|
186
|
+
|
|
187
|
+
def __enter__(self) -> StepsQuery:
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
def __exit__(self, *_: object) -> None:
|
|
191
|
+
self.close()
|
|
192
|
+
|
|
193
|
+
def _rows_from(self, sql: str) -> list[StepRow]:
|
|
194
|
+
"""Run ``sql`` and hydrate each dict row into a :class:`StepRow`.
|
|
195
|
+
|
|
196
|
+
Shared by the logical-step (``steps``) and condition-point
|
|
197
|
+
(``step_vectors``) reads — both surfaces share the row shape. ``sql``
|
|
198
|
+
is expected to select from the table aliased ``s`` plus
|
|
199
|
+
``_STEP_IO_JOINS`` (``inputs_map``/``outputs_map``).
|
|
200
|
+
"""
|
|
201
|
+
step_rows: list[StepRow] = []
|
|
202
|
+
for r in self._query_dicts(sql):
|
|
203
|
+
sr = StepRow(**r)
|
|
204
|
+
sr.inputs, sr.outputs = _decode_io_maps(r.get("inputs_map"), r.get("outputs_map"))
|
|
205
|
+
step_rows.append(sr)
|
|
206
|
+
return step_rows
|
|
207
|
+
|
|
208
|
+
def list_for_run(
|
|
209
|
+
self,
|
|
210
|
+
run_id: str,
|
|
211
|
+
*,
|
|
212
|
+
include_incomplete: bool = False,
|
|
213
|
+
) -> list[StepRow]:
|
|
214
|
+
"""Return the LOGICAL step rows for a run, ordered by ``step_index``.
|
|
215
|
+
|
|
216
|
+
Logical steps only (``vector_index IS NULL``) — a swept step is
|
|
217
|
+
ONE row here; its condition points live in ``step_vectors`` (see
|
|
218
|
+
:meth:`list_vectors_for_run`) and are nested onto the step by
|
|
219
|
+
:meth:`tree_for_run`. Matches the run by id-prefix (8-char) so
|
|
220
|
+
callers can pass either the full UUID or its short form. Each row
|
|
221
|
+
carries the step's own ``inputs``/``outputs``.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
run_id: UUID or 8-char prefix.
|
|
225
|
+
include_incomplete: Default ``False`` — only finalized
|
|
226
|
+
steps (``ended_at IS NOT NULL``). UI live views pass
|
|
227
|
+
``True`` to surface in-flight steps.
|
|
228
|
+
"""
|
|
229
|
+
prefix = run_id[:8] if len(run_id) >= 8 else run_id
|
|
230
|
+
ended_clause = "" if include_incomplete else "AND s.ended_at IS NOT NULL"
|
|
231
|
+
return self._rows_from(f"""
|
|
232
|
+
SELECT {_STEP_IO_SELECT}
|
|
233
|
+
FROM steps AS s
|
|
234
|
+
{_STEP_IO_JOINS}
|
|
235
|
+
WHERE s.run_id LIKE '{sql_escape(prefix)}%'
|
|
236
|
+
{ended_clause}
|
|
237
|
+
-- Logical-step grain: no vector dimension to order on (a step's own
|
|
238
|
+
-- vector_index is NULL). Its condition points are ordered in
|
|
239
|
+
-- list_vectors_for_run, which adds vector_outer_index, vector_index.
|
|
240
|
+
ORDER BY s.step_index, s.step_retry
|
|
241
|
+
""")
|
|
242
|
+
|
|
243
|
+
def list_vectors_for_run(
|
|
244
|
+
self,
|
|
245
|
+
run_id: str,
|
|
246
|
+
*,
|
|
247
|
+
include_incomplete: bool = False,
|
|
248
|
+
) -> list[StepRow]:
|
|
249
|
+
"""Return the condition-point (vector) rows for a run.
|
|
250
|
+
|
|
251
|
+
The sub-grain companion to :meth:`list_for_run`: one row per
|
|
252
|
+
sweep variant / in-body iteration (``vector_index`` 0..N), each
|
|
253
|
+
with its own commanded ``inputs`` and recorded ``outputs``.
|
|
254
|
+
Ordered so each step's vectors read in execution order.
|
|
255
|
+
"""
|
|
256
|
+
prefix = run_id[:8] if len(run_id) >= 8 else run_id
|
|
257
|
+
ended_clause = "" if include_incomplete else "AND s.ended_at IS NOT NULL"
|
|
258
|
+
return self._rows_from(f"""
|
|
259
|
+
SELECT {_STEP_IO_SELECT}
|
|
260
|
+
FROM step_vectors AS s
|
|
261
|
+
{_STEP_IO_JOINS}
|
|
262
|
+
WHERE s.run_id LIKE '{sql_escape(prefix)}%'
|
|
263
|
+
{ended_clause}
|
|
264
|
+
ORDER BY s.step_index, s.step_retry, s.vector_outer_index, s.vector_index
|
|
265
|
+
""")
|
|
266
|
+
|
|
267
|
+
def pareto(
|
|
268
|
+
self,
|
|
269
|
+
*,
|
|
270
|
+
top_n: int = 10,
|
|
271
|
+
phase: str | list[str] | None = None,
|
|
272
|
+
part: str | list[str] | None = None,
|
|
273
|
+
station: str | list[str] | None = None,
|
|
274
|
+
since: str | None = None,
|
|
275
|
+
until: str | None = None,
|
|
276
|
+
) -> list[dict[str, Any]]:
|
|
277
|
+
"""Failure pareto of failing steps grouped by ``step_path``.
|
|
278
|
+
|
|
279
|
+
Cross-run aggregate: which test step name has the most
|
|
280
|
+
failures across the matching set of runs. Same semantic as
|
|
281
|
+
:meth:`MeasurementsQuery.pareto` but at step-level instead
|
|
282
|
+
of measurement-level — useful when the operator wants to
|
|
283
|
+
spot a flaky test rather than a flaky measurement within a
|
|
284
|
+
test.
|
|
285
|
+
|
|
286
|
+
``failed_count`` includes ``failed`` + ``errored`` outcomes.
|
|
287
|
+
Optional filters scope to part / phase / station / time
|
|
288
|
+
window — same shape the rest of the metrics tabs use.
|
|
289
|
+
|
|
290
|
+
Note: filters apply to the runs context (joined via
|
|
291
|
+
``run_id``), not to the steps directly, so a "production
|
|
292
|
+
phase / part PN-100" view shows only the failures from
|
|
293
|
+
runs matching those facets.
|
|
294
|
+
"""
|
|
295
|
+
run_filters = ["runs.ended_at IS NOT NULL"]
|
|
296
|
+
# Operator-facing filters — see
|
|
297
|
+
# feedback_operator_facing_identifiers.md (universal rule:
|
|
298
|
+
# match against ``station_hostname``, never ``station_id``).
|
|
299
|
+
run_filters.extend(
|
|
300
|
+
multi_filter_clauses(
|
|
301
|
+
{
|
|
302
|
+
"runs.test_phase": phase,
|
|
303
|
+
"runs.uut_part_number": part,
|
|
304
|
+
"runs.station_hostname": station,
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
if since:
|
|
309
|
+
run_filters.append(f"runs.started_at >= '{sql_escape(since)}'")
|
|
310
|
+
if until:
|
|
311
|
+
run_filters.append(f"runs.started_at <= '{sql_escape(until)}'")
|
|
312
|
+
where = " AND ".join(run_filters)
|
|
313
|
+
return self._query_dicts(f"""
|
|
314
|
+
SELECT
|
|
315
|
+
steps.step_path AS bucket,
|
|
316
|
+
COUNT(*) FILTER (WHERE steps.outcome IN ('failed', 'errored')) AS failed_count,
|
|
317
|
+
COUNT(*) AS total,
|
|
318
|
+
CAST(
|
|
319
|
+
100.0 * COUNT(*) FILTER (WHERE steps.outcome IN ('failed', 'errored'))
|
|
320
|
+
/ COUNT(*)
|
|
321
|
+
AS DOUBLE
|
|
322
|
+
) AS fail_rate_pct
|
|
323
|
+
FROM steps
|
|
324
|
+
JOIN runs USING (run_id)
|
|
325
|
+
WHERE {where} AND steps.step_path IS NOT NULL
|
|
326
|
+
GROUP BY steps.step_path
|
|
327
|
+
HAVING failed_count > 0
|
|
328
|
+
ORDER BY failed_count DESC, total DESC
|
|
329
|
+
LIMIT {int(top_n)}
|
|
330
|
+
""")
|
|
331
|
+
|
|
332
|
+
def list_for_session(
|
|
333
|
+
self,
|
|
334
|
+
session_id: str,
|
|
335
|
+
*,
|
|
336
|
+
include_incomplete: bool = False,
|
|
337
|
+
) -> list[StepRow]:
|
|
338
|
+
"""Return every step row across every run sharing a ``session_id``.
|
|
339
|
+
|
|
340
|
+
Used by multi-site timeline / Gantt views: a session spans N
|
|
341
|
+
sibling runs (one per site), and the timeline needs them all.
|
|
342
|
+
Ordered by ``site_index`` then ``step_index`` so each site's
|
|
343
|
+
lane reads top-to-bottom.
|
|
344
|
+
|
|
345
|
+
Default excludes in-flight rows; pass ``include_incomplete=True``
|
|
346
|
+
for the live timeline view.
|
|
347
|
+
"""
|
|
348
|
+
ended_clause = "" if include_incomplete else "AND s.ended_at IS NOT NULL"
|
|
349
|
+
return self._rows_from(f"""
|
|
350
|
+
SELECT {_STEP_IO_SELECT}
|
|
351
|
+
FROM steps AS s
|
|
352
|
+
{_STEP_IO_JOINS}
|
|
353
|
+
WHERE s.session_id = '{sql_escape(session_id)}'
|
|
354
|
+
{ended_clause}
|
|
355
|
+
ORDER BY s.site_index, s.step_index, s.step_retry, s.vector_index
|
|
356
|
+
""")
|
|
357
|
+
|
|
358
|
+
def tree_for_run(self, run_id: str) -> list[StepNode]:
|
|
359
|
+
"""Return the step tree for a run, built from ``step_path``.
|
|
360
|
+
|
|
361
|
+
Top-level paths (no ``/``) are roots. Children are appended
|
|
362
|
+
under their parent path prefix. Order within each level
|
|
363
|
+
matches ``step_index``.
|
|
364
|
+
|
|
365
|
+
A logical step is one node (from the ``steps`` grain); its
|
|
366
|
+
condition points (from the ``step_vectors`` grain — ``vector_index``
|
|
367
|
+
0..N, sharing the step's ``step_path``) are attached onto
|
|
368
|
+
``node.vectors`` rather than becoming siblings/children (which would
|
|
369
|
+
show a swept step as N+1 same-named nodes). A step record is keyed by
|
|
370
|
+
``(step_path, step_retry, vector_outer_index)`` so a method run under
|
|
371
|
+
each outer sweep iteration keeps its own node and gets its own vectors.
|
|
372
|
+
"""
|
|
373
|
+
parent_anchor: dict[str, StepNode] = {}
|
|
374
|
+
node_by_key: dict[tuple[str, int, int | None], StepNode] = {}
|
|
375
|
+
roots: list[StepNode] = []
|
|
376
|
+
for row in self.list_for_run(run_id):
|
|
377
|
+
path = row.step_path or row.step_name or ""
|
|
378
|
+
key = (path, row.step_retry or 0, row.vector_outer_index)
|
|
379
|
+
node = StepNode(step=row)
|
|
380
|
+
node_by_key[key] = node
|
|
381
|
+
# Anchor parent attachment to the first node at a path; every
|
|
382
|
+
# execution (rerun / sweep variant) keeps its own node, so reruns
|
|
383
|
+
# are never overwritten and lost.
|
|
384
|
+
parent_anchor.setdefault(path, node)
|
|
385
|
+
if "/" not in path:
|
|
386
|
+
roots.append(node)
|
|
387
|
+
continue
|
|
388
|
+
parent_path = path.rsplit("/", 1)[0]
|
|
389
|
+
parent = parent_anchor.get(parent_path)
|
|
390
|
+
if parent is not None:
|
|
391
|
+
parent.children.append(node)
|
|
392
|
+
else:
|
|
393
|
+
# Orphan — parent path didn't appear before child.
|
|
394
|
+
# Treat as a root so it isn't lost.
|
|
395
|
+
roots.append(node)
|
|
396
|
+
# Nest condition points under their step node, matched on the full
|
|
397
|
+
# step-execution key (a vector's ``vector_outer_index`` routes it to
|
|
398
|
+
# the right per-outer method record).
|
|
399
|
+
for vec_row in self.list_vectors_for_run(run_id):
|
|
400
|
+
path = vec_row.step_path or vec_row.step_name or ""
|
|
401
|
+
node = node_by_key.get((path, vec_row.step_retry or 0, vec_row.vector_outer_index))
|
|
402
|
+
if node is not None:
|
|
403
|
+
node.vectors.append(vec_row)
|
|
404
|
+
return roots
|
|
405
|
+
|
|
406
|
+
def describe_columns(self) -> ColumnSchema:
|
|
407
|
+
"""Return the ``steps`` table's column schema."""
|
|
408
|
+
rows = self._query_dicts("DESCRIBE steps")
|
|
409
|
+
fixed = [
|
|
410
|
+
FixedColumnDescriptor(name=str(r["column_name"]), column_type=str(r["column_type"]))
|
|
411
|
+
for r in rows
|
|
412
|
+
]
|
|
413
|
+
return ColumnSchema(fixed=fixed, fields=[])
|
testerkit/api/_mime.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Magic-byte content sniffer for `/api/runs/{id}/ref` responses.
|
|
2
|
+
|
|
3
|
+
When ``load_ref`` materializes a ``.bin`` payload, we get raw bytes
|
|
4
|
+
with no extension to drive ``Content-Type`` selection. ``imghdr`` was
|
|
5
|
+
removed in Python 3.13 and ``python-magic`` is a system dep we don't
|
|
6
|
+
want to require, so we hand-roll a small magic-byte table covering
|
|
7
|
+
the formats a hardware-test workflow realistically captures —
|
|
8
|
+
operator screenshots, scope photos, test recordings, calibration
|
|
9
|
+
certs, log files, exported HTML reports.
|
|
10
|
+
|
|
11
|
+
Anything we can't identify falls back to ``application/octet-stream``
|
|
12
|
+
so the browser offers a download.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sniff_mime(head: bytes) -> str:
|
|
19
|
+
"""Return a MIME type string for *head* (first ~16+ bytes of payload).
|
|
20
|
+
|
|
21
|
+
Falls back to ``application/octet-stream`` for unknown content.
|
|
22
|
+
"""
|
|
23
|
+
if not head:
|
|
24
|
+
return "application/octet-stream"
|
|
25
|
+
|
|
26
|
+
if head.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
27
|
+
return "image/png"
|
|
28
|
+
if head.startswith(b"\xff\xd8\xff"):
|
|
29
|
+
return "image/jpeg"
|
|
30
|
+
if head[:6] in (b"GIF87a", b"GIF89a"):
|
|
31
|
+
return "image/gif"
|
|
32
|
+
if head.startswith(b"RIFF") and len(head) >= 12 and head[8:12] == b"WEBP":
|
|
33
|
+
return "image/webp"
|
|
34
|
+
if head.startswith(b"%PDF-"):
|
|
35
|
+
return "application/pdf"
|
|
36
|
+
if len(head) >= 8 and head[4:8] == b"ftyp":
|
|
37
|
+
return "video/mp4"
|
|
38
|
+
if head.startswith(b"\x1a\x45\xdf\xa3"):
|
|
39
|
+
return "video/webm"
|
|
40
|
+
|
|
41
|
+
# Text-leading formats: strip whitespace before checking, since YAML /
|
|
42
|
+
# XML / HTML often have leading newlines or BOMs.
|
|
43
|
+
stripped = head.lstrip()
|
|
44
|
+
if stripped.startswith(b"<?xml") or stripped[:4].lower() == b"<svg":
|
|
45
|
+
return "image/svg+xml"
|
|
46
|
+
lowered = stripped.lower()
|
|
47
|
+
if lowered.startswith(b"<!doctype html") or lowered.startswith(b"<html"):
|
|
48
|
+
return "text/html"
|
|
49
|
+
|
|
50
|
+
if _looks_like_text(head):
|
|
51
|
+
return "text/plain"
|
|
52
|
+
return "application/octet-stream"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _looks_like_text(head: bytes) -> bool:
|
|
56
|
+
"""Heuristic: decodes as UTF-8 and contains no NUL / control gore.
|
|
57
|
+
|
|
58
|
+
Allows tab / LF / CR / form-feed; rejects other low control bytes
|
|
59
|
+
and non-decodable sequences.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
text = head.decode("utf-8")
|
|
63
|
+
except UnicodeDecodeError:
|
|
64
|
+
return False
|
|
65
|
+
if "\x00" in text:
|
|
66
|
+
return False
|
|
67
|
+
allowed_controls = {"\t", "\n", "\r", "\x0b", "\x0c"}
|
|
68
|
+
for ch in text:
|
|
69
|
+
if ch < " " and ch not in allowed_controls:
|
|
70
|
+
return False
|
|
71
|
+
return True
|