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