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