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.
Files changed (521) hide show
  1. testerkit/__init__.py +68 -0
  2. testerkit/_docs/_assets/operator-ui/channels/table.png +0 -0
  3. testerkit/_docs/_assets/operator-ui/dashboard/runs.png +0 -0
  4. testerkit/_docs/_assets/operator-ui/dashboard/stations.png +0 -0
  5. testerkit/_docs/_assets/operator-ui/events/filters.png +0 -0
  6. testerkit/_docs/_assets/operator-ui/events/table.png +0 -0
  7. testerkit/_docs/_assets/operator-ui/explore/chart.png +0 -0
  8. testerkit/_docs/_assets/operator-ui/explore/filters.png +0 -0
  9. testerkit/_docs/_assets/operator-ui/explore/plot-controls.png +0 -0
  10. testerkit/_docs/_assets/operator-ui/files/table.png +0 -0
  11. testerkit/_docs/_assets/operator-ui/launch/form.png +0 -0
  12. testerkit/_docs/_assets/operator-ui/metrics/filters.png +0 -0
  13. testerkit/_docs/_assets/operator-ui/metrics/pareto.png +0 -0
  14. testerkit/_docs/_assets/operator-ui/metrics/ppk.png +0 -0
  15. testerkit/_docs/_assets/operator-ui/metrics/yield.png +0 -0
  16. testerkit/_docs/_assets/operator-ui/results/detail-header.png +0 -0
  17. testerkit/_docs/_assets/operator-ui/results/detail-measurements.png +0 -0
  18. testerkit/_docs/_assets/operator-ui/results/detail-overview.png +0 -0
  19. testerkit/_docs/_assets/operator-ui/results/detail-steps.png +0 -0
  20. testerkit/_docs/_assets/operator-ui/results/stats.png +0 -0
  21. testerkit/_docs/_assets/operator-ui/results/table.png +0 -0
  22. testerkit/_docs/_assets/operator-ui/tour/designer.png +0 -0
  23. testerkit/_docs/_assets/operator-ui/tour/docs.png +0 -0
  24. testerkit/_docs/_assets/operator-ui/tour/fixtures.png +0 -0
  25. testerkit/_docs/_assets/operator-ui/tour/instruments.png +0 -0
  26. testerkit/_docs/_assets/operator-ui/tour/parts.png +0 -0
  27. testerkit/_docs/_assets/operator-ui/tour/profiles.png +0 -0
  28. testerkit/_docs/_assets/operator-ui/tour/stations.png +0 -0
  29. testerkit/_docs/_assets/operator-ui/tour/tests.png +0 -0
  30. testerkit/_docs/_assets/operator-ui/tour/uuts.png +0 -0
  31. testerkit/_docs/concepts/configuration/capabilities.md +447 -0
  32. testerkit/_docs/concepts/configuration/fixtures.md +335 -0
  33. testerkit/_docs/concepts/configuration/index.md +14 -0
  34. testerkit/_docs/concepts/configuration/parts.md +276 -0
  35. testerkit/_docs/concepts/configuration/stations.md +214 -0
  36. testerkit/_docs/concepts/data/data-stores.md +199 -0
  37. testerkit/_docs/concepts/data/event-log.md +185 -0
  38. testerkit/_docs/concepts/data/event-sourcing.md +76 -0
  39. testerkit/_docs/concepts/data/flight-streaming.md +58 -0
  40. testerkit/_docs/concepts/data/index.md +17 -0
  41. testerkit/_docs/concepts/data/sessions.md +78 -0
  42. testerkit/_docs/concepts/data/three-verbs.md +260 -0
  43. testerkit/_docs/concepts/execution/index.md +13 -0
  44. testerkit/_docs/concepts/execution/outcomes.md +199 -0
  45. testerkit/_docs/concepts/execution/step-hierarchy.md +170 -0
  46. testerkit/_docs/concepts/execution/step-manifest.md +115 -0
  47. testerkit/_docs/concepts/index.md +40 -0
  48. testerkit/_docs/concepts/overview/ai-integration.md +57 -0
  49. testerkit/_docs/concepts/overview/architecture.md +357 -0
  50. testerkit/_docs/concepts/overview/index.md +14 -0
  51. testerkit/_docs/concepts/overview/platform-vs-framework.md +147 -0
  52. testerkit/_docs/concepts/overview/pytest.md +53 -0
  53. testerkit/_docs/concepts/overview/tiers.md +46 -0
  54. testerkit/_docs/how-to/catalog/datasheet-to-test.md +169 -0
  55. testerkit/_docs/how-to/catalog/index.md +11 -0
  56. testerkit/_docs/how-to/configuration/configuring-stations.md +364 -0
  57. testerkit/_docs/how-to/configuration/custom-drivers.md +484 -0
  58. testerkit/_docs/how-to/configuration/index.md +13 -0
  59. testerkit/_docs/how-to/configuration/mock-mode.md +342 -0
  60. testerkit/_docs/how-to/data/benchmarking.md +120 -0
  61. testerkit/_docs/how-to/data/capture-an-artifact.md +84 -0
  62. testerkit/_docs/how-to/data/capture-waveform.md +110 -0
  63. testerkit/_docs/how-to/data/choosing-a-channel-verb.md +108 -0
  64. testerkit/_docs/how-to/data/compare-runs.md +120 -0
  65. testerkit/_docs/how-to/data/export-results.md +104 -0
  66. testerkit/_docs/how-to/data/find-flaky-tests.md +108 -0
  67. testerkit/_docs/how-to/data/grafana-dashboards.md +148 -0
  68. testerkit/_docs/how-to/data/index.md +23 -0
  69. testerkit/_docs/how-to/data/mcp-debug-failures.md +125 -0
  70. testerkit/_docs/how-to/data/mcp-query-runs.md +120 -0
  71. testerkit/_docs/how-to/data/querying-channels.md +117 -0
  72. testerkit/_docs/how-to/data/querying-events.md +85 -0
  73. testerkit/_docs/how-to/data/stream-live-channel.md +134 -0
  74. testerkit/_docs/how-to/execution/custom-operator-ui.md +136 -0
  75. testerkit/_docs/how-to/execution/index.md +21 -0
  76. testerkit/_docs/how-to/execution/limits.md +193 -0
  77. testerkit/_docs/how-to/execution/managing-sessions.md +100 -0
  78. testerkit/_docs/how-to/execution/multi-uut-testing.md +211 -0
  79. testerkit/_docs/how-to/execution/operator-prompts.md +151 -0
  80. testerkit/_docs/how-to/execution/profiles.md +304 -0
  81. testerkit/_docs/how-to/execution/spec-driven-testing.md +169 -0
  82. testerkit/_docs/how-to/execution/test-context.md +212 -0
  83. testerkit/_docs/how-to/execution/traceability.md +252 -0
  84. testerkit/_docs/how-to/execution/vector-expansion.md +394 -0
  85. testerkit/_docs/how-to/execution/writing-tests.md +363 -0
  86. testerkit/_docs/how-to/index.md +59 -0
  87. testerkit/_docs/how-to/overview/index.md +11 -0
  88. testerkit/_docs/how-to/overview/mcp-integration.md +168 -0
  89. testerkit/_docs/how-to/overview/operator-ui-tour.md +234 -0
  90. testerkit/_docs/integration/data/grafana.md +162 -0
  91. testerkit/_docs/integration/data/index.md +15 -0
  92. testerkit/_docs/integration/data/lakehouse-import.md +226 -0
  93. testerkit/_docs/integration/data/logging.md +131 -0
  94. testerkit/_docs/integration/data/results-api.md +51 -0
  95. testerkit/_docs/integration/index.md +20 -0
  96. testerkit/_docs/integration/runtime/harness.md +281 -0
  97. testerkit/_docs/integration/runtime/index.md +13 -0
  98. testerkit/_docs/integration/runtime/instruments.md +284 -0
  99. testerkit/_docs/integration/runtime/pytest-existing.md +315 -0
  100. testerkit/_docs/reference/catalog/cookbook.md +389 -0
  101. testerkit/_docs/reference/catalog/index.md +11 -0
  102. testerkit/_docs/reference/catalog/schema.md +375 -0
  103. testerkit/_docs/reference/cli.md +660 -0
  104. testerkit/_docs/reference/configuration.md +406 -0
  105. testerkit/_docs/reference/data/channels-schema.md +145 -0
  106. testerkit/_docs/reference/data/event-types.md +554 -0
  107. testerkit/_docs/reference/data/events-schema.md +120 -0
  108. testerkit/_docs/reference/data/files-schema.md +141 -0
  109. testerkit/_docs/reference/data/index.md +18 -0
  110. testerkit/_docs/reference/data/models.md +2229 -0
  111. testerkit/_docs/reference/data/outputs.md +82 -0
  112. testerkit/_docs/reference/data/parquet-schema.md +529 -0
  113. testerkit/_docs/reference/data/performance-limits.md +224 -0
  114. testerkit/_docs/reference/data/query-api.md +247 -0
  115. testerkit/_docs/reference/index.md +69 -0
  116. testerkit/_docs/reference/operator-ui/channels/detail.md +82 -0
  117. testerkit/_docs/reference/operator-ui/channels/list.md +70 -0
  118. testerkit/_docs/reference/operator-ui/dashboard.md +87 -0
  119. testerkit/_docs/reference/operator-ui/designer.md +167 -0
  120. testerkit/_docs/reference/operator-ui/events.md +89 -0
  121. testerkit/_docs/reference/operator-ui/files.md +74 -0
  122. testerkit/_docs/reference/operator-ui/fixtures.md +100 -0
  123. testerkit/_docs/reference/operator-ui/instruments.md +126 -0
  124. testerkit/_docs/reference/operator-ui/launch.md +79 -0
  125. testerkit/_docs/reference/operator-ui/live.md +102 -0
  126. testerkit/_docs/reference/operator-ui/measurements.md +159 -0
  127. testerkit/_docs/reference/operator-ui/metrics.md +217 -0
  128. testerkit/_docs/reference/operator-ui/parts.md +98 -0
  129. testerkit/_docs/reference/operator-ui/profiles.md +103 -0
  130. testerkit/_docs/reference/operator-ui/results/detail.md +214 -0
  131. testerkit/_docs/reference/operator-ui/results/list.md +106 -0
  132. testerkit/_docs/reference/operator-ui/stations.md +98 -0
  133. testerkit/_docs/reference/operator-ui/tests.md +173 -0
  134. testerkit/_docs/reference/operator-ui/uuts.md +67 -0
  135. testerkit/_docs/reference/overview/index.md +11 -0
  136. testerkit/_docs/reference/overview/pytest-native.md +142 -0
  137. testerkit/_docs/reference/overview/skills.md +177 -0
  138. testerkit/_docs/reference/pytest/fixtures.md +342 -0
  139. testerkit/_docs/reference/pytest/index.md +12 -0
  140. testerkit/_docs/reference/pytest/markers.md +257 -0
  141. testerkit/_docs/reference/runtime/api.md +246 -0
  142. testerkit/_docs/reference/runtime/client.md +280 -0
  143. testerkit/_docs/reference/runtime/connect.md +140 -0
  144. testerkit/_docs/reference/runtime/index.md +13 -0
  145. testerkit/_docs/tutorial/01-first-test.md +113 -0
  146. testerkit/_docs/tutorial/02-mock-instruments.md +88 -0
  147. testerkit/_docs/tutorial/03-fixtures.md +169 -0
  148. testerkit/_docs/tutorial/04-limits.md +139 -0
  149. testerkit/_docs/tutorial/05-configuration.md +194 -0
  150. testerkit/_docs/tutorial/06-specifications.md +273 -0
  151. testerkit/_docs/tutorial/07-real-instruments.md +245 -0
  152. testerkit/_docs/tutorial/08-capabilities.md +223 -0
  153. testerkit/_docs/tutorial/09-production.md +374 -0
  154. testerkit/_docs/tutorial/10-live-monitoring.md +156 -0
  155. testerkit/_docs/tutorial/11-waveforms-and-evidence.md +169 -0
  156. testerkit/_docs/tutorial/12-continuous-monitoring.md +145 -0
  157. testerkit/_docs/tutorial/13-parallel-testing.md +233 -0
  158. testerkit/_docs/tutorial/index.md +61 -0
  159. testerkit/_docs/tutorial/quickstart.md +282 -0
  160. testerkit/analysis/__init__.py +9 -0
  161. testerkit/analysis/_common.py +24 -0
  162. testerkit/analysis/measurement_facets.py +524 -0
  163. testerkit/analysis/measurements_query.py +1458 -0
  164. testerkit/analysis/metrics.py +389 -0
  165. testerkit/analysis/runs_query.py +434 -0
  166. testerkit/analysis/steps_query.py +413 -0
  167. testerkit/api/__init__.py +6 -0
  168. testerkit/api/_mime.py +71 -0
  169. testerkit/api/app.py +1171 -0
  170. testerkit/api/dialogs/__init__.py +36 -0
  171. testerkit/api/dialogs/manager.py +617 -0
  172. testerkit/api/dialogs/models.py +79 -0
  173. testerkit/api/models.py +80 -0
  174. testerkit/api/responses.py +205 -0
  175. testerkit/api/runner.py +222 -0
  176. testerkit/api/schemas.py +311 -0
  177. testerkit/benchmark/__init__.py +34 -0
  178. testerkit/benchmark/concurrency.py +248 -0
  179. testerkit/benchmark/core.py +232 -0
  180. testerkit/benchmark/runner.py +628 -0
  181. testerkit/benchmark/scenario.py +313 -0
  182. testerkit/benchmark/system.py +191 -0
  183. testerkit/benchmark/workloads.py +507 -0
  184. testerkit/catalog/__init__.py +6 -0
  185. testerkit/catalog/generic/generic_dmm.yaml +61 -0
  186. testerkit/catalog/generic/generic_eload.yaml +31 -0
  187. testerkit/catalog/generic/generic_fgen.yaml +56 -0
  188. testerkit/catalog/generic/generic_oscilloscope.yaml +45 -0
  189. testerkit/catalog/generic/generic_psu.yaml +77 -0
  190. testerkit/catalog/generic/generic_smu.yaml +73 -0
  191. testerkit/channels.py +538 -0
  192. testerkit/cli/__init__.py +32 -0
  193. testerkit/cli/_common.py +113 -0
  194. testerkit/cli/_time.py +179 -0
  195. testerkit/cli/benchmark_cmd.py +56 -0
  196. testerkit/cli/catalog_cmd.py +39 -0
  197. testerkit/cli/daemon.py +157 -0
  198. testerkit/cli/data_cmd.py +1040 -0
  199. testerkit/cli/discover_cmd.py +94 -0
  200. testerkit/cli/docs_cmd.py +100 -0
  201. testerkit/cli/instrument.py +172 -0
  202. testerkit/cli/mcp_cmd.py +37 -0
  203. testerkit/cli/metrics.py +366 -0
  204. testerkit/cli/project.py +312 -0
  205. testerkit/cli/root.py +14 -0
  206. testerkit/cli/runs.py +412 -0
  207. testerkit/cli/schema_cmd.py +87 -0
  208. testerkit/cli/serve_cmd.py +50 -0
  209. testerkit/cli/setup_cmd.py +604 -0
  210. testerkit/cli/station.py +294 -0
  211. testerkit/cli/validate.py +112 -0
  212. testerkit/client.py +463 -0
  213. testerkit/connect.py +635 -0
  214. testerkit/data/__init__.py +8 -0
  215. testerkit/data/_accumulator_pool.py +455 -0
  216. testerkit/data/_atomic.py +59 -0
  217. testerkit/data/_collection_indices.py +67 -0
  218. testerkit/data/_daemon_lifecycle.py +535 -0
  219. testerkit/data/_duckdb_daemon.py +695 -0
  220. testerkit/data/_duckdb_flight_server.py +813 -0
  221. testerkit/data/_event_filters.py +15 -0
  222. testerkit/data/_event_reader.py +101 -0
  223. testerkit/data/_flight_errors.py +199 -0
  224. testerkit/data/_flight_query.py +202 -0
  225. testerkit/data/_flight_retry.py +148 -0
  226. testerkit/data/_flight_subscribe.py +67 -0
  227. testerkit/data/_index_epoch.py +342 -0
  228. testerkit/data/_ipc_writer.py +185 -0
  229. testerkit/data/_json_safe.py +63 -0
  230. testerkit/data/_process.py +18 -0
  231. testerkit/data/_push_relay.py +124 -0
  232. testerkit/data/_runs_duckdb_daemon.py +2704 -0
  233. testerkit/data/_session_reaper.py +110 -0
  234. testerkit/data/_sql_helpers.py +30 -0
  235. testerkit/data/_store.py +24 -0
  236. testerkit/data/backends/__init__.py +7 -0
  237. testerkit/data/backends/_event_accumulator.py +957 -0
  238. testerkit/data/backends/_row_helpers.py +1172 -0
  239. testerkit/data/backends/parquet.py +1143 -0
  240. testerkit/data/backends/protocol.py +35 -0
  241. testerkit/data/channels/__init__.py +8 -0
  242. testerkit/data/channels/_flight_daemon.py +75 -0
  243. testerkit/data/channels/client.py +242 -0
  244. testerkit/data/channels/flight_manager.py +88 -0
  245. testerkit/data/channels/index.py +664 -0
  246. testerkit/data/channels/models.py +418 -0
  247. testerkit/data/channels/server.py +162 -0
  248. testerkit/data/channels/store.py +1481 -0
  249. testerkit/data/data_dir.py +61 -0
  250. testerkit/data/duckdb_manager.py +79 -0
  251. testerkit/data/event_log.py +384 -0
  252. testerkit/data/event_store.py +768 -0
  253. testerkit/data/events.py +1025 -0
  254. testerkit/data/exporters/__init__.py +6 -0
  255. testerkit/data/exporters/_helpers.py +40 -0
  256. testerkit/data/exporters/csv_exporter.py +168 -0
  257. testerkit/data/exporters/hdf5.py +234 -0
  258. testerkit/data/exporters/json_exporter.py +229 -0
  259. testerkit/data/exporters/mdf4.py +201 -0
  260. testerkit/data/exporters/stdf.py +255 -0
  261. testerkit/data/exporters/tdms.py +292 -0
  262. testerkit/data/files/__init__.py +71 -0
  263. testerkit/data/files/_backend.py +207 -0
  264. testerkit/data/files/_catalog_daemon.py +84 -0
  265. testerkit/data/files/catalog.py +218 -0
  266. testerkit/data/files/catalog_manager.py +325 -0
  267. testerkit/data/files/models.py +60 -0
  268. testerkit/data/files/serializers.py +414 -0
  269. testerkit/data/files/store.py +463 -0
  270. testerkit/data/files/streaming.py +783 -0
  271. testerkit/data/models.py +604 -0
  272. testerkit/data/ref.py +152 -0
  273. testerkit/data/retention.py +297 -0
  274. testerkit/data/run_store.py +367 -0
  275. testerkit/data/runs_duckdb_manager.py +103 -0
  276. testerkit/data/schema_dispatch.py +191 -0
  277. testerkit/data/schema_migrate.py +76 -0
  278. testerkit/data/schema_versions.py +85 -0
  279. testerkit/data/schemas.py +288 -0
  280. testerkit/data/subscribers/__init__.py +48 -0
  281. testerkit/data/subscribers/_base.py +20 -0
  282. testerkit/data/subscribers/_output_file.py +25 -0
  283. testerkit/data/subscribers/replay.py +55 -0
  284. testerkit/environment.py +84 -0
  285. testerkit/execution/__init__.py +11 -0
  286. testerkit/execution/_git.py +190 -0
  287. testerkit/execution/_state.py +740 -0
  288. testerkit/execution/accessors.py +54 -0
  289. testerkit/execution/audit.py +51 -0
  290. testerkit/execution/cascade.py +85 -0
  291. testerkit/execution/connections.py +491 -0
  292. testerkit/execution/harness.py +1931 -0
  293. testerkit/execution/instrument_events.py +42 -0
  294. testerkit/execution/limits.py +154 -0
  295. testerkit/execution/metadata.py +102 -0
  296. testerkit/execution/mocks.py +63 -0
  297. testerkit/execution/profiles.py +616 -0
  298. testerkit/execution/run_scope.py +1331 -0
  299. testerkit/execution/session_scope.py +196 -0
  300. testerkit/execution/sidecar.py +280 -0
  301. testerkit/execution/site_runner.py +705 -0
  302. testerkit/execution/sites.py +183 -0
  303. testerkit/execution/sync.py +294 -0
  304. testerkit/execution/uut_provider.py +231 -0
  305. testerkit/execution/vectors.py +205 -0
  306. testerkit/execution/verify.py +396 -0
  307. testerkit/expand.py +75 -0
  308. testerkit/files.py +235 -0
  309. testerkit/fixtures/__init__.py +6 -0
  310. testerkit/fixtures/manager.py +357 -0
  311. testerkit/grafana/__init__.py +1 -0
  312. testerkit/grafana/bootstrap.py +61 -0
  313. testerkit/grafana/cli.py +314 -0
  314. testerkit/grafana/dashboards/asset_utilization.json +92 -0
  315. testerkit/grafana/dashboards/channel_explorer.json +77 -0
  316. testerkit/grafana/dashboards/event_log.json +92 -0
  317. testerkit/grafana/dashboards/failure_pareto.json +104 -0
  318. testerkit/grafana/dashboards/measurement_distribution.json +97 -0
  319. testerkit/grafana/dashboards/measurement_trend.json +161 -0
  320. testerkit/grafana/dashboards/station_comparison.json +95 -0
  321. testerkit/grafana/dashboards/test_duration.json +104 -0
  322. testerkit/grafana/dashboards/unit_traceability.json +73 -0
  323. testerkit/grafana/dashboards/yield_overview.json +189 -0
  324. testerkit/grafana/provisioning/dashboards.yaml.j2 +12 -0
  325. testerkit/grafana/provisioning/datasources.yaml.j2 +17 -0
  326. testerkit/grafana/server.py +282 -0
  327. testerkit/init.py +724 -0
  328. testerkit/instruments/__init__.py +10 -0
  329. testerkit/instruments/base.py +82 -0
  330. testerkit/instruments/discovery/__init__.py +62 -0
  331. testerkit/instruments/discovery/_base.py +174 -0
  332. testerkit/instruments/discovery/lxi.py +152 -0
  333. testerkit/instruments/discovery/ni.py +95 -0
  334. testerkit/instruments/discovery/serial.py +73 -0
  335. testerkit/instruments/discovery/visa.py +98 -0
  336. testerkit/instruments/lifecycle.py +213 -0
  337. testerkit/instruments/loader.py +79 -0
  338. testerkit/instruments/locks.py +178 -0
  339. testerkit/instruments/mocks.py +230 -0
  340. testerkit/instruments/observer.py +270 -0
  341. testerkit/instruments/observers/__init__.py +36 -0
  342. testerkit/instruments/observers/_base.py +68 -0
  343. testerkit/instruments/observers/daqmx.py +70 -0
  344. testerkit/instruments/observers/descriptor.py +69 -0
  345. testerkit/instruments/observers/generic.py +92 -0
  346. testerkit/instruments/observers/lantz.py +31 -0
  347. testerkit/instruments/observers/modbus.py +96 -0
  348. testerkit/instruments/observers/motion.py +68 -0
  349. testerkit/instruments/observers/ni_modular.py +73 -0
  350. testerkit/instruments/observers/ophyd.py +80 -0
  351. testerkit/instruments/observers/pymeasure.py +120 -0
  352. testerkit/instruments/observers/qcodes.py +66 -0
  353. testerkit/instruments/observers/scpi.py +88 -0
  354. testerkit/instruments/observers/tektronix.py +57 -0
  355. testerkit/instruments/observers/visa.py +103 -0
  356. testerkit/instruments/pool.py +459 -0
  357. testerkit/instruments/proxy.py +59 -0
  358. testerkit/instruments/route_manager.py +329 -0
  359. testerkit/instruments/routed_proxy.py +88 -0
  360. testerkit/instruments/server.py +508 -0
  361. testerkit/instruments/switch.py +43 -0
  362. testerkit/instruments/visa.py +294 -0
  363. testerkit/matching/__init__.py +6 -0
  364. testerkit/matching/service.py +969 -0
  365. testerkit/mcp/__init__.py +6 -0
  366. testerkit/mcp/server.py +681 -0
  367. testerkit/mcp/tools.py +1868 -0
  368. testerkit/models/__init__.py +18 -0
  369. testerkit/models/capability.py +598 -0
  370. testerkit/models/catalog.py +76 -0
  371. testerkit/models/data_options.py +153 -0
  372. testerkit/models/enums.py +334 -0
  373. testerkit/models/instrument.py +152 -0
  374. testerkit/models/instrument_asset.py +25 -0
  375. testerkit/models/part.py +271 -0
  376. testerkit/models/part_manifest.py +96 -0
  377. testerkit/models/project.py +114 -0
  378. testerkit/models/station.py +140 -0
  379. testerkit/models/test_config.py +748 -0
  380. testerkit/ontology/__init__.py +39 -0
  381. testerkit/ontology/schema.py +188 -0
  382. testerkit/ontology/testerkit.yaml +1420 -0
  383. testerkit/parts/__init__.py +8 -0
  384. testerkit/parts/context.py +181 -0
  385. testerkit/parts/folder.py +251 -0
  386. testerkit/parts/loader.py +27 -0
  387. testerkit/prompts/__init__.py +17 -0
  388. testerkit/prompts/core.py +133 -0
  389. testerkit/pytest_plugin/__init__.py +1287 -0
  390. testerkit/pytest_plugin/autouse.py +369 -0
  391. testerkit/pytest_plugin/helpers.py +354 -0
  392. testerkit/pytest_plugin/hooks.py +1982 -0
  393. testerkit/pytest_plugin/markers.py +203 -0
  394. testerkit/pytest_plugin/retry.py +37 -0
  395. testerkit/pytest_plugin/sweeps.py +117 -0
  396. testerkit/queries.py +78 -0
  397. testerkit/reports/__init__.py +6 -0
  398. testerkit/reports/core.py +492 -0
  399. testerkit/reports/datasheet.py +831 -0
  400. testerkit/reports/templates/datasheet.html +383 -0
  401. testerkit/reports/templates/default.html +153 -0
  402. testerkit/sbom.py +142 -0
  403. testerkit/schema_export.py +101 -0
  404. testerkit/signals.py +63 -0
  405. testerkit/skills/README.md +58 -0
  406. testerkit/skills/templates/project-instructions.md +125 -0
  407. testerkit/skills/testerkit-analysis/SKILL.md +87 -0
  408. testerkit/skills/testerkit-capture/SKILL.md +114 -0
  409. testerkit/skills/testerkit-data/SKILL.md +138 -0
  410. testerkit/skills/testerkit-datasheets/SKILL.md +100 -0
  411. testerkit/skills/testerkit-datasheets/agents/scaffold-writer.md +115 -0
  412. testerkit/skills/testerkit-datasheets/agents/section-extractor.md +98 -0
  413. testerkit/skills/testerkit-datasheets/agents/section-reviewer.md +153 -0
  414. testerkit/skills/testerkit-datasheets/agents/section-splitter.md +89 -0
  415. testerkit/skills/testerkit-datasheets/agents/section-writer.md +226 -0
  416. testerkit/skills/testerkit-datasheets/references/catalog-pipeline.md +271 -0
  417. testerkit/skills/testerkit-datasheets/references/process-queue.md +42 -0
  418. testerkit/skills/testerkit-datasheets/references/scaffold.md +156 -0
  419. testerkit/skills/testerkit-datasheets/references/test-pipeline.md +312 -0
  420. testerkit/skills/testerkit-debug/SKILL.md +127 -0
  421. testerkit/skills/testerkit-interactive/SKILL.md +188 -0
  422. testerkit/skills/testerkit-interactive/references/live-ui-patterns.md +102 -0
  423. testerkit/skills/testerkit-mocks/SKILL.md +128 -0
  424. testerkit/skills/testerkit-parts/SKILL.md +126 -0
  425. testerkit/skills/testerkit-profiles/SKILL.md +139 -0
  426. testerkit/skills/testerkit-sites/SKILL.md +140 -0
  427. testerkit/skills/testerkit-stations/SKILL.md +146 -0
  428. testerkit/skills/testerkit-tests/SKILL.md +136 -0
  429. testerkit/store.py +1639 -0
  430. testerkit/ui/__init__.py +71 -0
  431. testerkit/ui/_asgi.py +162 -0
  432. testerkit/ui/app.py +32 -0
  433. testerkit/ui/components/__init__.py +13 -0
  434. testerkit/ui/components/artifact_viewer.py +250 -0
  435. testerkit/ui/components/channel_values.py +159 -0
  436. testerkit/ui/components/event_timeline.py +222 -0
  437. testerkit/ui/components/execution_gantt.py +210 -0
  438. testerkit/ui/components/file_streams.py +147 -0
  439. testerkit/ui/components/instrument_activity.py +146 -0
  440. testerkit/ui/components/session_table.py +118 -0
  441. testerkit/ui/pages/__init__.py +25 -0
  442. testerkit/ui/pages/channels/__init__.py +4 -0
  443. testerkit/ui/pages/channels/detail.py +932 -0
  444. testerkit/ui/pages/channels/list.py +515 -0
  445. testerkit/ui/pages/dashboard.py +236 -0
  446. testerkit/ui/pages/designer/__init__.py +5 -0
  447. testerkit/ui/pages/designer/graph.py +635 -0
  448. testerkit/ui/pages/designer/matching.py +663 -0
  449. testerkit/ui/pages/designer/page.py +593 -0
  450. testerkit/ui/pages/designer/properties.py +591 -0
  451. testerkit/ui/pages/designer/state.py +468 -0
  452. testerkit/ui/pages/docs/__init__.py +6 -0
  453. testerkit/ui/pages/docs/index.py +95 -0
  454. testerkit/ui/pages/docs/page.py +698 -0
  455. testerkit/ui/pages/events/__init__.py +3 -0
  456. testerkit/ui/pages/events/list.py +263 -0
  457. testerkit/ui/pages/explore.py +1108 -0
  458. testerkit/ui/pages/files/__init__.py +4 -0
  459. testerkit/ui/pages/files/detail.py +426 -0
  460. testerkit/ui/pages/files/list.py +447 -0
  461. testerkit/ui/pages/fixtures/__init__.py +8 -0
  462. testerkit/ui/pages/fixtures/detail.py +280 -0
  463. testerkit/ui/pages/fixtures/edit.py +370 -0
  464. testerkit/ui/pages/fixtures/list.py +180 -0
  465. testerkit/ui/pages/fixtures/new.py +195 -0
  466. testerkit/ui/pages/instruments/__init__.py +4 -0
  467. testerkit/ui/pages/instruments/detail.py +320 -0
  468. testerkit/ui/pages/instruments/edit.py +482 -0
  469. testerkit/ui/pages/instruments/list.py +249 -0
  470. testerkit/ui/pages/instruments/new.py +200 -0
  471. testerkit/ui/pages/launch.py +261 -0
  472. testerkit/ui/pages/live.py +124 -0
  473. testerkit/ui/pages/metrics_page.py +1229 -0
  474. testerkit/ui/pages/parts/__init__.py +11 -0
  475. testerkit/ui/pages/parts/detail.py +202 -0
  476. testerkit/ui/pages/parts/edit.py +310 -0
  477. testerkit/ui/pages/parts/list.py +160 -0
  478. testerkit/ui/pages/parts/new.py +170 -0
  479. testerkit/ui/pages/parts/requirements.py +153 -0
  480. testerkit/ui/pages/parts/stations.py +199 -0
  481. testerkit/ui/pages/profiles/__init__.py +4 -0
  482. testerkit/ui/pages/profiles/detail.py +113 -0
  483. testerkit/ui/pages/profiles/list.py +86 -0
  484. testerkit/ui/pages/results/__init__.py +4 -0
  485. testerkit/ui/pages/results/detail.py +631 -0
  486. testerkit/ui/pages/results/list.py +230 -0
  487. testerkit/ui/pages/stations/__init__.py +4 -0
  488. testerkit/ui/pages/stations/detail.py +256 -0
  489. testerkit/ui/pages/stations/edit.py +273 -0
  490. testerkit/ui/pages/stations/list.py +180 -0
  491. testerkit/ui/pages/stations/new.py +311 -0
  492. testerkit/ui/pages/tests/__init__.py +4 -0
  493. testerkit/ui/pages/tests/detail.py +152 -0
  494. testerkit/ui/pages/tests/list.py +194 -0
  495. testerkit/ui/pages/uuts/__init__.py +4 -0
  496. testerkit/ui/pages/uuts/list.py +97 -0
  497. testerkit/ui/shared/__init__.py +8 -0
  498. testerkit/ui/shared/components.py +1826 -0
  499. testerkit/ui/shared/dialogs.py +105 -0
  500. testerkit/ui/shared/event_binding.py +194 -0
  501. testerkit/ui/shared/layout.py +314 -0
  502. testerkit/ui/shared/services.py +1719 -0
  503. testerkit/ui/shared/timestamps.py +21 -0
  504. testerkit/ui/static/branding/favicon-16.png +0 -0
  505. testerkit/ui/static/branding/favicon-32.png +0 -0
  506. testerkit/ui/static/branding/favicon-48.png +0 -0
  507. testerkit/ui/static/branding/testerkit-icon-256.png +0 -0
  508. testerkit/ui/static/branding/testerkit-mark.svg +1 -0
  509. testerkit/ui/static/branding/testerkit-wordmark.svg +1 -0
  510. testerkit/ui/static/global.css +465 -0
  511. testerkit/utils/__init__.py +17 -0
  512. testerkit/utils/enum_meta.py +800 -0
  513. testerkit/utils/paths.py +75 -0
  514. testerkit/utils/ranges.py +257 -0
  515. testerkit/validation.py +82 -0
  516. testerkit/verbs.py +142 -0
  517. testerkit-0.4.0.dist-info/METADATA +250 -0
  518. testerkit-0.4.0.dist-info/RECORD +521 -0
  519. testerkit-0.4.0.dist-info/WHEEL +4 -0
  520. testerkit-0.4.0.dist-info/entry_points.txt +5 -0
  521. testerkit-0.4.0.dist-info/licenses/LICENSE +191 -0
@@ -0,0 +1,604 @@
1
+ """Data models for test results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from datetime import UTC, datetime, timedelta
7
+ from enum import StrEnum
8
+ from typing import Any
9
+ from uuid import UUID, uuid4
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ from testerkit.models.test_config import Limit
14
+
15
+
16
+ def _utcnow() -> datetime:
17
+ """Return current UTC datetime (timezone-aware)."""
18
+ return datetime.now(UTC)
19
+
20
+
21
+ def ensure_utc(dt: datetime) -> datetime:
22
+ """Normalize a datetime to UTC-aware.
23
+
24
+ Naive inputs are assumed UTC (the server's storage convention) and
25
+ stamped without shifting the wall-clock value; aware inputs are
26
+ converted to UTC. The server only ever reasons in UTC, so every
27
+ datetime crossing a receive/parse boundary passes through here.
28
+ """
29
+ if dt.tzinfo is None:
30
+ return dt.replace(tzinfo=UTC)
31
+ return dt.astimezone(UTC)
32
+
33
+
34
+ class StimulusRecord(BaseModel):
35
+ """Record of a stimulus applied during test execution.
36
+
37
+ Captures the full signal path when an instrument sets an input condition:
38
+ - param: The parameter name (e.g., "vin", "load", "temp")
39
+ - value: The commanded value
40
+ - unit: Unit of the value
41
+ - instrument: Station config name (e.g., "psu_main")
42
+ - resource: VISA address or connection string
43
+ - channel: Channel on instrument (e.g., "CH1")
44
+ - uut_pin: UUT pin being driven
45
+ - fixture_connection: Named fixture connection
46
+ """
47
+
48
+ param: str
49
+ value: float | None = None
50
+ unit: str | None = None
51
+ instrument: str | None = None # Station config name (e.g., "psu_main")
52
+ resource: str | None = None # VISA address or connection string
53
+ channel: str | None = None # Channel on instrument (e.g., "CH1")
54
+ uut_pin: str | None = None # UUT pin being driven
55
+ fixture_connection: str | None = None # Named fixture connection
56
+
57
+
58
+ class Outcome(StrEnum):
59
+ """Canonical terminal outcome of a measurement / step / run.
60
+
61
+ All values are past participles — outcomes are retrospective by
62
+ construction (the row reflects what happened). Each runner adapter
63
+ translates its native signals into these. Pytest's own
64
+ ``passed`` / ``failed`` / ``skipped`` / ``error`` mostly map by
65
+ casing; bare-assert failures and ``pytest.skip()`` flow through
66
+ ``pytest_runtest_makereport``.
67
+
68
+ Producer story:
69
+
70
+ * ``PASSED`` — measurement value met its limit. Producer:
71
+ ``execution.verify._compute_outcome`` when ``value in limit``;
72
+ pytest ``passed`` propagates when no measurement-level outcome
73
+ contradicts.
74
+ * ``FAILED`` — measurement violated its limit OR the test failed an
75
+ assertion. Producer: ``_compute_outcome`` when ``value not in
76
+ limit``; ``pytest_runtest_makereport`` escalates ``step.outcome``
77
+ to ``FAILED`` when pytest reports ``failed`` and no measurement
78
+ already failed.
79
+ * ``ERRORED`` — exception during execution (not an assertion).
80
+ Producer: pytest ``error`` (setup/teardown failures); uncaught
81
+ non-AssertionError during call; ``_compute_outcome`` when value
82
+ is ``None`` (couldn't measure → can't judge).
83
+ * ``SKIPPED`` — explicit skip by operator/marker/condition.
84
+ Producer: pytest ``skipped`` (``pytest.skip()`` /
85
+ ``@pytest.mark.skip`` / skipif); ``VectorBuilder.skip()`` on the
86
+ catch-all client API.
87
+ * ``DONE`` — recorded value, no judgment evaluated. Producer:
88
+ plain ``logger.measure(name, value)`` (no limit, no verify) —
89
+ the recorder semantic. Setup/action / characterization-mode
90
+ measurements that explicitly aren't being judged. Test engineers
91
+ reject ``PASSED`` for un-judged actions ("don't call my setup a
92
+ pass") — this is the answer.
93
+ * ``TERMINATED`` — operator stopped the run and cleanup ran (safe
94
+ states reached, fixtures torn down, parquet finalized). The
95
+ "expected, mid-flight, but graceful" stop. TestStand calls this
96
+ Terminated; we follow that convention so test engineers
97
+ crossing over from TestStand/WATS recognize the semantics.
98
+ Producer: ``pytest_keyboard_interrupt`` once we've confirmed
99
+ teardown ran; ``connect.__exit__`` on KeyboardInterrupt /
100
+ SystemExit; SIGTERM handler when the cleanup chain completed.
101
+ * ``ABORTED`` — terminated WITHOUT cleanup. Reserved for the
102
+ no-safe-state case: the close()-time fallback when no
103
+ ``RunEnded`` was ever emitted, partial signal-handler exits,
104
+ etc. When you see Aborted in a report it means the rig may
105
+ not be in a known state — operator should physically check.
106
+ Producer: ``the materializer`` fallback;
107
+ ``RunBuilder.abort()`` on the catch-all client.
108
+
109
+ Note on the "never ran" case: there is no ``Planned`` value.
110
+ A step that pytest collected but never executed is signaled by
111
+ ``outcome is None`` at finalize time — the field-missingness IS
112
+ the receipt. The display layer derives "Never Ran" from
113
+ ``outcome is None`` plus the run's finalized state.
114
+ """
115
+
116
+ PASSED = "passed"
117
+ FAILED = "failed"
118
+ SKIPPED = "skipped"
119
+ ERRORED = "errored"
120
+ TERMINATED = "terminated"
121
+ ABORTED = "aborted"
122
+ DONE = "done"
123
+
124
+ @property
125
+ def severity(self) -> int:
126
+ """Severity rank for cascade ordering. Worst wins.
127
+
128
+ ``ABORTED`` (7) > ``TERMINATED`` (6) > ``ERRORED`` (5) >
129
+ ``FAILED`` (4) > ``PASSED`` (3) > ``DONE`` (2) > ``SKIPPED``
130
+ (1). See :func:`escalate_outcome` for the rationale.
131
+
132
+ ``None`` (no outcome stamped yet) is treated as
133
+ severity ``-1`` by :func:`escalate_outcome` — anything
134
+ wins over an unjudged row.
135
+ """
136
+ return _OUTCOME_SEVERITY[self]
137
+
138
+
139
+ _OUTCOME_SEVERITY: dict[Outcome, int] = {
140
+ Outcome.ABORTED: 7,
141
+ Outcome.TERMINATED: 6,
142
+ Outcome.ERRORED: 5,
143
+ Outcome.FAILED: 4,
144
+ Outcome.PASSED: 3,
145
+ Outcome.DONE: 2,
146
+ Outcome.SKIPPED: 1,
147
+ }
148
+
149
+ # Drift guard: every Outcome variant must have a severity rank. Catches
150
+ # the common bug where a new variant is added but the dict isn't updated.
151
+ assert set(Outcome) == set(_OUTCOME_SEVERITY), (
152
+ f"_OUTCOME_SEVERITY is missing ranks for: {set(Outcome) - set(_OUTCOME_SEVERITY)}"
153
+ )
154
+
155
+
156
+ def escalate_outcome(
157
+ current: Outcome | None,
158
+ incoming: Outcome | None,
159
+ ) -> Outcome | None:
160
+ """Return the worse (higher-severity) of two outcomes.
161
+
162
+ Severity, worst first:
163
+ ``ABORTED > TERMINATED > ERRORED > FAILED > PASSED > DONE > SKIPPED``,
164
+ with ``None`` ranked below everything ("never judged").
165
+
166
+ Use this everywhere outcome cascading is needed (vector → step → run)
167
+ to keep severity logic in one place. Reading the ladder:
168
+
169
+ * ``ABORTED`` preempts everything — when cleanup didn't run, the
170
+ operator needs to know the rig isn't in a known state.
171
+ * ``TERMINATED`` beats ``ERRORED`` — operator-initiated stop with
172
+ cleanup is "louder" than a test-code blow-up because the run
173
+ didn't complete normally.
174
+ * ``ERRORED`` beats ``FAILED`` — an unexpected blow-up is worse than
175
+ a judged-bad value.
176
+ * ``PASSED`` beats ``DONE`` — an actual verdict outranks a recorded-
177
+ but-unjudged value.
178
+ * Any concrete outcome beats ``None`` — the row was unjudged and
179
+ now has a verdict.
180
+ """
181
+ cur_sev = current.severity if current is not None else -1
182
+ inc_sev = incoming.severity if incoming is not None else -1
183
+ return current if cur_sev >= inc_sev else incoming
184
+
185
+
186
+ def retry_aware_rollup(steps: Iterable[TestStep]) -> Outcome | None:
187
+ """Roll up step outcomes for a parent container (class step / run).
188
+
189
+ Groups steps by ``node_id`` so multiple attempts of the same test
190
+ (``testerkit_retry`` / ``pytest-rerunfailures``) collapse to a single
191
+ contribution. The LAST attempt's outcome wins per group — matching
192
+ pytest-rerunfailures' final-attempt-is-the-outcome semantics and
193
+ the STDF retest convention (retest count is metadata; the final
194
+ disposition is the disposition). After per-group reduction the
195
+ final attempts are escalated via the severity ladder.
196
+
197
+ Steps without a ``node_id`` (container steps, autouse-wrapped
198
+ cleanup) are not collapsed — each gets its own group keyed by
199
+ object identity. Steps with ``outcome is None`` are skipped (they
200
+ haven't recorded a verdict yet).
201
+ """
202
+ # Modern Python dicts preserve insertion order; a single dict is
203
+ # enough to bucket-by-node_id AND remember which bucket came first.
204
+ groups: dict[Any, list[TestStep]] = {}
205
+ for step in steps:
206
+ if step.outcome is None:
207
+ continue
208
+ key: Any = step.node_id if step.node_id is not None else id(step)
209
+ groups.setdefault(key, []).append(step)
210
+
211
+ result: Outcome | None = None
212
+ for bucket in groups.values():
213
+ # Last attempt's outcome is THE outcome for this node_id.
214
+ result = escalate_outcome(result, bucket[-1].outcome)
215
+ return result
216
+
217
+
218
+ class Measurement(BaseModel):
219
+ """A single measurement with optional limit checking."""
220
+
221
+ name: str
222
+ step_path: str = ""
223
+ value: float | None
224
+ unit: str | None = None
225
+ limit_low: float | None = None
226
+ limit_high: float | None = None
227
+ limit_nominal: float | None = None
228
+ outcome: Outcome | None = None
229
+ characteristic_id: str | None = None # Characteristic ID for structured traceability
230
+ spec_ref: str | None = None # Human-readable spec reference with conditions
231
+ limit_comparator: str | None = None # ATML comparator: EQ, NE, GE, LE, GELE, etc.
232
+ timestamp: datetime = Field(default_factory=_utcnow)
233
+
234
+ # Traceability (ATML: signal routing)
235
+ uut_pin: str | None = None # Which UUT pin was measured
236
+ instrument_name: str | None = None # Station config name (e.g., "dmm_main")
237
+ instrument_resource: str | None = None # VISA address or connection string
238
+ instrument_channel: str | None = None # Channel on instrument (e.g., "CH1")
239
+ fixture_connection: str | None = None # Fixture connection name (e.g., "VOUT")
240
+
241
+ def check_limit(self) -> Outcome:
242
+ """Evaluate value against the row's limit fields; set + return outcome.
243
+
244
+ Single judgment path: rebuilds a :class:`Limit` from the
245
+ stamped row fields via :meth:`Limit.from_row` and delegates
246
+ the comparator-aware check to ``Limit.__contains__``. The
247
+ same path :func:`testerkit.execution.verify._compute_outcome`
248
+ uses, just starting from a row instead of a Limit object.
249
+
250
+ Outcomes:
251
+
252
+ * ``ERRORED`` — value is None (couldn't measure → can't judge).
253
+ * ``DONE`` — no limit fields stamped (recorder semantic).
254
+ * ``PASSED`` / ``FAILED`` — value evaluated against the
255
+ reconstructed limit per its comparator.
256
+ """
257
+ if self.value is None:
258
+ self.outcome = Outcome.ERRORED
259
+ return self.outcome
260
+ limit = Limit.from_row(
261
+ low=self.limit_low,
262
+ high=self.limit_high,
263
+ nominal=self.limit_nominal,
264
+ unit=self.unit,
265
+ comparator=self.limit_comparator,
266
+ characteristic_id=self.characteristic_id,
267
+ spec_ref=self.spec_ref,
268
+ )
269
+ if limit is None:
270
+ self.outcome = Outcome.DONE
271
+ return self.outcome
272
+ self.outcome = Outcome.PASSED if self.value in limit else Outcome.FAILED
273
+ return self.outcome
274
+
275
+
276
+ class TestVector(BaseModel):
277
+ """A test vector execution with its input parameters and observations.
278
+
279
+ Represents a single execution of a test with specific input values.
280
+ Parameters are stored once here, not duplicated on each measurement.
281
+
282
+ This is the primary unit of test execution: the framework expands
283
+ vectors from config (product, zip, range, nested loops) and iterates
284
+ over them, calling the test function for each.
285
+
286
+ Hierarchy:
287
+ TestRun
288
+ └── TestStep (one per pytest test function)
289
+ └── TestVector[] (one per param set, expanded from config)
290
+ └── Measurement[] (values captured in that vector)
291
+
292
+ Data categories:
293
+ - params (in_*): Configuration - commanded values, setpoints, settings
294
+ - observations (out_*): Measured context, environmental readings, raw data
295
+ - measurements: The actual test results (always scalars)
296
+ """
297
+
298
+ __test__ = False # Prevent pytest collection
299
+
300
+ id: UUID = Field(default_factory=uuid4)
301
+ test_step_id: UUID | None = None # FK to parent TestStep
302
+ index: int = 0 # 0-based index in the parameter expansion
303
+ params: dict[str, Any] = Field(default_factory=dict) # Input parameter values (→ in_*)
304
+ observations: dict[str, Any] = Field(default_factory=dict) # Observed context (→ out_*)
305
+ # Optional engineering unit per param / observation name → the lane unit field.
306
+ param_units: dict[str, str] = Field(default_factory=dict)
307
+ observation_units: dict[str, str] = Field(default_factory=dict)
308
+ observation_pins: dict[str, str] = Field(default_factory=dict)
309
+ stimulus: list[StimulusRecord] = Field(default_factory=list) # Stimulus signal paths
310
+ retry: int = 0 # Retry counter — 0 for the first execution, N for the Nth retry
311
+ max_retries: int = 0 # Maximum retries allowed (0 = no retries, single execution)
312
+ outcome: Outcome | None = None
313
+ measurements: list[Measurement] = Field(default_factory=list)
314
+ started_at: datetime = Field(default_factory=_utcnow)
315
+ ended_at: datetime | None = None
316
+ error_message: str | None = None
317
+
318
+
319
+ class TestStep(BaseModel):
320
+ """A test step containing test vectors.
321
+
322
+ A step corresponds to a pytest test function. It may contain multiple
323
+ test vectors if the test is parametrized or uses vector expansion.
324
+
325
+ Hierarchy:
326
+ TestRun
327
+ └── TestStep (one per pytest test function)
328
+ └── TestVector[] (one per param set, expanded from config)
329
+ └── Measurement[] (values captured in that vector)
330
+ """
331
+
332
+ __test__ = False # Prevent pytest collection
333
+
334
+ id: UUID = Field(default_factory=uuid4)
335
+ name: str
336
+ step_path: str = ""
337
+ description: str | None = None
338
+
339
+ # Code identity (populated from pytest.Item when available)
340
+ node_id: str | None = None
341
+ file: str | None = None
342
+ module: str | None = None
343
+ class_name: str | None = None
344
+ function: str | None = None
345
+ markers: str | None = None
346
+ started_at: datetime = Field(default_factory=_utcnow)
347
+ ended_at: datetime | None = None
348
+ outcome: Outcome | None = None
349
+ # 0-based outer (item) retry — 0 for the first attempt, N for the Nth
350
+ # pytest-rerunfailures rerun of this step. Distinct from a vector's own
351
+ # inner retry; the de-fuse keys each execution by both axes.
352
+ retry: int = 0
353
+ vectors: list[TestVector] = Field(default_factory=list)
354
+ # Step-scope data: measurements/inputs/outputs latched when no vector is active.
355
+ measurements: list[Measurement] = Field(default_factory=list)
356
+ inputs: dict[str, Any] = Field(default_factory=dict)
357
+ outputs: dict[str, Any] = Field(default_factory=dict)
358
+ error_message: str | None = None
359
+ instrument_records: list[dict[str, Any]] | None = None
360
+
361
+ @property
362
+ def total_vectors(self) -> int:
363
+ """Total number of test vectors."""
364
+ return len(self.vectors) if self.vectors else 1
365
+
366
+ @property
367
+ def passed_vectors(self) -> int:
368
+ """Number of passed test vectors."""
369
+ return sum(1 for v in self.vectors if v.outcome == Outcome.PASSED)
370
+
371
+ @property
372
+ def failed_vectors(self) -> int:
373
+ """Number of failed test vectors."""
374
+ return sum(1 for v in self.vectors if v.outcome == Outcome.FAILED)
375
+
376
+
377
+ class CollectedItem(BaseModel):
378
+ """A pytest item collected for execution (before any run).
379
+
380
+ ``step_index`` and ``vector_index`` are computed at collection time,
381
+ pre-reorder for class-level sweeps:
382
+
383
+ * ``step_index`` = position within the parent sequence (root for classless
384
+ methods, or within the test class). All sweep variants of the same
385
+ function share one ``step_index``.
386
+ * ``vector_index`` = 0-based position within the sweep expansion.
387
+ * ``vector_count_planned`` = number of items collected for this logical
388
+ step. Lets the manifest detect unrun vectors after the run.
389
+ """
390
+
391
+ node_id: str
392
+ file: str | None = None
393
+ module: str | None = None
394
+ class_name: str | None = None
395
+ function: str | None = None
396
+ markers: str | None = None
397
+ # step_path: computed at collection time so that unrun items
398
+ # carry the same hierarchical identifier as executed step events.
399
+ step_path: str = ""
400
+ step_index: int = 0
401
+ vector_index: int = 0
402
+ vector_count_planned: int = 1
403
+
404
+
405
+ class UUT(BaseModel):
406
+ """Device under test identification."""
407
+
408
+ serial: str
409
+ part_number: str | None = None
410
+ revision: str | None = None
411
+ lot_number: str | None = None
412
+
413
+
414
+ class RunSummary(BaseModel):
415
+ """Lightweight run header read from parquet index (no steps/measurements)."""
416
+
417
+ test_run_id: str
418
+ session_id: str | None = None
419
+ # Optional to tolerate the pre-RunStarted-correlation in-flight row
420
+ # (see ``_row_helpers.py``'s placeholder branch); persisted/correlated
421
+ # rows always carry 0+.
422
+ site_index: int | None = None
423
+ site_name: str | None = None
424
+ started_at: datetime | None = None
425
+ ended_at: datetime | None = None
426
+ uut_serial_number: str | None = None
427
+ uut_part_number: str | None = None
428
+ part_id: str | None = None
429
+ station_id: str | None = None
430
+ station_name: str | None = None
431
+ station_type: str | None = None
432
+ station_hostname: str | None = None
433
+ fixture_id: str | None = None
434
+ test_phase: str | None = None
435
+ operator: str | None = None
436
+ outcome: str | None = None
437
+ total_measurements: int = 0
438
+ total_steps: int = 0
439
+ project_name: str | None = None
440
+ file_path: str | None = None # internal: parquet file location for fast measurement lookup
441
+
442
+
443
+ class TestRun(BaseModel):
444
+ """A complete test run with steps and measurements."""
445
+
446
+ __test__ = False # Prevent pytest collection
447
+
448
+ id: UUID = Field(default_factory=uuid4)
449
+ session_id: UUID = Field(default_factory=uuid4) # Cross-store join key; set by logger
450
+ started_at: datetime = Field(default_factory=_utcnow)
451
+ ended_at: datetime | None = None
452
+
453
+ # Execution lane — always present, 0-based, default 0. Stamped in
454
+ # RunScope.__init__ from the resolved site ContextVars (see
455
+ # execution._state.get_current_site_index/get_current_site_name)
456
+ # so every identity-field reader (build_run_metadata, RunStarted)
457
+ # sources it uniformly from TestRun instead of reaching into the
458
+ # ContextVar directly.
459
+ site_index: int = 0
460
+ site_name: str | None = None
461
+
462
+ # UUT identification
463
+ uut: UUT
464
+
465
+ # Part traceability
466
+ part_id: str | None = None
467
+ part_name: str | None = None
468
+ part_revision: str | None = None
469
+
470
+ # Station traceability — ``station_id`` is None for bringup tier
471
+ # runs (no station YAML on disk; tests use conftest-defined
472
+ # instrument fixtures). ``station_hostname`` always populates from
473
+ # ``socket.gethostname()`` so the run is traceable to a machine
474
+ # even without a station id.
475
+ station_id: str | None = None
476
+ station_name: str | None = None
477
+ station_type: str | None = None
478
+ station_location: str | None = None
479
+ station_hostname: str | None = None
480
+
481
+ # Fixture traceability
482
+ fixture_id: str | None = None
483
+
484
+ # Phase / profile traceability
485
+ test_phase: str | None = None
486
+ profile: str | None = None # active --test-profile name, if any
487
+ # Raw CLI facet values used to select the profile; combined with git SHA
488
+ # this is the minimum reproducibility payload for the run.
489
+ profile_facets: dict[str, str] = Field(default_factory=dict)
490
+ # Resolved required_inputs at session start (serial_number, operator, etc.).
491
+ # Each project declares the keys; the values come from CLI flags, env
492
+ # vars, or operator prompts at the start of the run.
493
+ session_inputs: dict[str, str] = Field(default_factory=dict)
494
+
495
+ # Operator
496
+ operator_id: str | None = None # from --operator
497
+ operator_name: str | None = None # human-readable name
498
+
499
+ # Code traceability
500
+ git_commit: str | None = None
501
+ git_branch: str | None = None
502
+ git_remote: str | None = None
503
+ project_name: str | None = None
504
+
505
+ # Results
506
+ outcome: Outcome | None = None
507
+ steps: list[TestStep] = Field(default_factory=list)
508
+
509
+ # Collected items (full list from pytest collection, before execution)
510
+ collected_items: list[CollectedItem] = Field(default_factory=list)
511
+
512
+ # Custom metadata (user-defined fields)
513
+ custom_metadata: dict[str, Any] = Field(default_factory=dict)
514
+
515
+ # Environment snapshot (stored in Parquet file-level metadata)
516
+ environment_json: str | None = None
517
+
518
+
519
+ class Waveform(BaseModel):
520
+ """Time-series waveform data with metadata.
521
+
522
+ Uses compressed representation where the time axis is reconstructed
523
+ from ``t0 + i * dt`` instead of storing paired timestamps.
524
+
525
+ Attributes:
526
+ t0: Absolute UTC timestamp of the first sample. ``None`` when
527
+ the producer doesn't know the wall-clock time (e.g. a
528
+ synthesized or hardware-trigger-relative capture). For
529
+ scope captures where samples are relative to a trigger,
530
+ store the trigger offset in ``attributes`` and set ``t0``
531
+ to the trigger's absolute time.
532
+ dt: Sample interval (seconds).
533
+ Y: Sample values (voltage, current, etc.).
534
+ attributes: Metadata (unit, channel, coupling, trigger
535
+ offset, etc.). Renamed from ``attrs`` in build item 17 for
536
+ cross-schema vocabulary consistency (matches
537
+ FileArtifactMetadata.attributes and
538
+ ChannelDescriptor.attributes).
539
+ """
540
+
541
+ t0: datetime | None = None
542
+ dt: float
543
+ Y: list[float] # Sample values
544
+ attributes: dict[str, Any] = Field(default_factory=dict)
545
+
546
+ @property
547
+ def num_samples(self) -> int:
548
+ """Number of samples in the waveform."""
549
+ return len(self.Y)
550
+
551
+ @property
552
+ def duration(self) -> float:
553
+ """Total duration in seconds."""
554
+ return self.num_samples * self.dt
555
+
556
+ def time_axis(self) -> list[datetime]:
557
+ """Reconstruct the absolute time axis: ``t0 + i * dt`` for each sample.
558
+
559
+ Raises ``ValueError`` if ``t0`` is None — without an anchor,
560
+ the absolute axis can't be reconstructed. Callers that only
561
+ need relative time should compute ``[i * dt for i in
562
+ range(num_samples)]`` directly.
563
+ """
564
+ if self.t0 is None:
565
+ raise ValueError(
566
+ "Waveform.time_axis() requires t0 to be set. "
567
+ "For relative-only time, use [i * dt for i in range(num_samples)]."
568
+ )
569
+ return [self.t0 + timedelta(seconds=i * self.dt) for i in range(self.num_samples)]
570
+
571
+
572
+ class XYData(BaseModel):
573
+ """Paired x/y arrays for related-but-non-time-series data.
574
+
575
+ For data the test author thinks of as one artifact rather than two
576
+ parallel channels: IV curves, eye diagrams, S-parameter sweeps,
577
+ optical spectra. Per the §4 manifestation rules, this is "Pattern
578
+ B" — one discrete artifact per vector that routes to FileStore.
579
+
580
+ ``observe(name, XYData(...))`` registers via the serializer
581
+ registry (build item 12) and lands on disk as a single ``.npz``
582
+ holding ``x``, ``y``, and any of the optional unit/name keys
583
+ that were set. The MIME convention (build item 13) is
584
+ ``application/x-numpy-npz``.
585
+
586
+ Use parallel channels (`stream`) instead when the data is
587
+ continuous over time and you want it live-subscribable — see §4
588
+ Pattern A.
589
+
590
+ Attributes:
591
+ x: Independent-axis values.
592
+ y: Dependent-axis values. Must have the same length as ``x``.
593
+ x_unit: Optional unit for the x axis ("V", "Hz", "dBm").
594
+ y_unit: Optional unit for the y axis.
595
+ x_name: Optional human label for the x axis ("Bias voltage").
596
+ y_name: Optional human label for the y axis.
597
+ """
598
+
599
+ x: list[float]
600
+ y: list[float]
601
+ x_unit: str | None = None
602
+ y_unit: str | None = None
603
+ x_name: str | None = None
604
+ y_name: str | None = None