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,75 @@
1
+ """Centralized search path management for TesterKit resources.
2
+
3
+ This module provides a single source of truth for where different
4
+ resource types (parts, stations, instruments, etc.) are located.
5
+
6
+ All paths are relative to the project root (defaults to cwd).
7
+ Run the server from the project directory: `cd myproject && testerkit serve`
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from enum import StrEnum
13
+ from pathlib import Path
14
+
15
+
16
+ class ResourceType(StrEnum):
17
+ """Types of resources in the TesterKit ecosystem."""
18
+
19
+ PARTS = "parts" # Part YAML files
20
+ STATIONS = "stations" # Station configurations
21
+ INSTRUMENTS = "instruments" # Instrument library definitions
22
+ FIXTURES = "fixtures" # Test fixture definitions
23
+ TESTS = "tests" # Test configurations
24
+
25
+
26
+ def _resolve_root(project_root: Path | None = None) -> Path:
27
+ """Resolve project root, defaulting to cwd."""
28
+ return project_root if project_root is not None else Path.cwd()
29
+
30
+
31
+ def get_search_paths(
32
+ resource_type: ResourceType,
33
+ project_root: Path | None = None,
34
+ ) -> list[Path]:
35
+ """Get search paths for a resource type.
36
+
37
+ Args:
38
+ resource_type: Type of resource to find paths for.
39
+ project_root: Project root directory. Defaults to cwd.
40
+
41
+ Returns:
42
+ List containing the single path for this resource type, if it exists.
43
+ """
44
+ path = _resolve_root(project_root) / resource_type.value
45
+ if path.is_dir():
46
+ return [path]
47
+ return []
48
+
49
+
50
+ def get_all_search_paths(
51
+ project_root: Path | None = None,
52
+ ) -> dict[ResourceType, list[Path]]:
53
+ """Get search paths for all resource types."""
54
+ return {rt: get_search_paths(rt, project_root) for rt in ResourceType}
55
+
56
+
57
+ # Convenience aliases
58
+ def get_part_paths(project_root: Path | None = None) -> list[Path]:
59
+ """Get search paths for part folders."""
60
+ return get_search_paths(ResourceType.PARTS, project_root)
61
+
62
+
63
+ def get_station_paths(project_root: Path | None = None) -> list[Path]:
64
+ """Get search paths for station configurations."""
65
+ return get_search_paths(ResourceType.STATIONS, project_root)
66
+
67
+
68
+ def get_instrument_paths(project_root: Path | None = None) -> list[Path]:
69
+ """Get search paths for instrument library."""
70
+ return get_search_paths(ResourceType.INSTRUMENTS, project_root)
71
+
72
+
73
+ def get_fixture_paths(project_root: Path | None = None) -> list[Path]:
74
+ """Get search paths for test fixtures."""
75
+ return get_search_paths(ResourceType.FIXTURES, project_root)
@@ -0,0 +1,257 @@
1
+ """Range expansion utilities for pins, channels, and numeric values.
2
+
3
+ Supports SCPI-style inclusive ranges (hardware industry standard):
4
+ - "1:4" → [1, 2, 3, 4] (inclusive, unlike Python)
5
+ - "GPIO[0:7]" → ["GPIO0", "GPIO1", ..., "GPIO7"]
6
+ - "1,3:5,8" → [1, 3, 4, 5, 8] (non-contiguous)
7
+ - "GPIO[0,2,4:6]" → ["GPIO0", "GPIO2", "GPIO4", "GPIO5", "GPIO6"]
8
+ - "-40:125:55" → [-40, 15, 70, 125] (numeric with step)
9
+ - "0.1:0.5:0.1" → [0.1, 0.2, 0.3, 0.4, 0.5] (float with step)
10
+
11
+ Prior Art:
12
+ - SCPI (IEEE 488.2): (@1:4) → 1,2,3,4 (inclusive)
13
+ - Verilog/VHDL: [7:0] → 8-bit bus (inclusive)
14
+ - NI DAQmx: ai0:3 → 4 channels (inclusive)
15
+ - Python slice: [0:4] → 0,1,2,3 (exclusive - different!)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+
22
+
23
+ def expand_range(spec: str | list | int) -> list[str]:
24
+ """Expand a range specification to a list of string items.
25
+
26
+ Handles three input types:
27
+ 1. List: Pass through, converting items to strings
28
+ 2. String with prefix[range]: Expand prefix to each number (GPIO[0:2] → GPIO0, GPIO1, GPIO2)
29
+ 3. String with numeric range: Expand numbers (1:4 → 1, 2, 3, 4)
30
+ 4. Single value: Return as single-item list
31
+
32
+ Args:
33
+ spec: Range string, list of items, or single value
34
+
35
+ Returns:
36
+ List of expanded items as strings
37
+
38
+ Examples:
39
+ >>> expand_range("1:4")
40
+ ['1', '2', '3', '4']
41
+ >>> expand_range("GPIO[0:2]")
42
+ ['GPIO0', 'GPIO1', 'GPIO2']
43
+ >>> expand_range("GPIO[0,2,4:6]")
44
+ ['GPIO0', 'GPIO2', 'GPIO4', 'GPIO5', 'GPIO6']
45
+ >>> expand_range(["A", "B"]) # List pass-through
46
+ ['A', 'B']
47
+ >>> expand_range("TP_VOUT") # Single item
48
+ ['TP_VOUT']
49
+ >>> expand_range(1) # Single int
50
+ ['1']
51
+ """
52
+ # List: pass through with string conversion
53
+ if isinstance(spec, list):
54
+ return [str(item) for item in spec]
55
+
56
+ # Single int/float: convert to string list
57
+ if isinstance(spec, (int, float)):
58
+ return [str(spec)]
59
+
60
+ # Must be string from here
61
+ if not isinstance(spec, str):
62
+ return [str(spec)]
63
+
64
+ # Check for prefix[range] pattern: GPIO[0:7], CH[1:4], ai[0:15]
65
+ prefix_match = re.match(r"^([A-Za-z_][A-Za-z0-9_.]*)\[(.+)\]$", spec)
66
+ if prefix_match:
67
+ prefix = prefix_match.group(1)
68
+ range_part = prefix_match.group(2)
69
+ numbers = _expand_int_range(range_part)
70
+ return [f"{prefix}{n}" for n in numbers]
71
+
72
+ # Pure numeric range with optional step: 1:4 or 1:10:2 or 1,3:5
73
+ # Must start with digit or negative sign
74
+ if re.match(r"^-?[\d]", spec) and ":" in spec:
75
+ # Check if this looks like a numeric range (no letters except 'e' for scientific notation)
76
+ if re.match(r"^[\d,:\s.\-eE]+$", spec):
77
+ numbers = _expand_int_range(spec)
78
+ return [str(n) for n in numbers]
79
+
80
+ # Comma-separated without colons: 1,3,5
81
+ if "," in spec and ":" not in spec and re.match(r"^[\d,\s.\-]+$", spec):
82
+ numbers = _expand_int_range(spec)
83
+ return [str(n) for n in numbers]
84
+
85
+ # Single item (no range pattern detected)
86
+ return [spec]
87
+
88
+
89
+ def _expand_int_range(spec: str) -> list[int]:
90
+ """Expand comma-separated ranges of integers.
91
+
92
+ Uses inclusive ranges (SCPI-style):
93
+ - "1:4" → [1, 2, 3, 4]
94
+ - "1,3,5" → [1, 3, 5]
95
+ - "1:3,5,7:9" → [1, 2, 3, 5, 7, 8, 9]
96
+
97
+ Args:
98
+ spec: Range specification string
99
+
100
+ Returns:
101
+ List of integers
102
+ """
103
+ result = []
104
+ for part in spec.split(","):
105
+ part = part.strip()
106
+ if ":" in part:
107
+ bounds = part.split(":")
108
+ if len(bounds) == 2:
109
+ # Simple range: 1:4 → 1, 2, 3, 4
110
+ start, end = int(bounds[0]), int(bounds[1])
111
+ step = 1 if start <= end else -1
112
+ result.extend(range(start, end + step, step))
113
+ # Note: start:stop:step format handled by expand_numeric_range for floats
114
+ else:
115
+ result.append(int(part))
116
+ return result
117
+
118
+
119
+ def expand_numeric_range(spec: str | list | int | float) -> list[float]:
120
+ """Expand a numeric range specification to a list of float values.
121
+
122
+ Supports:
123
+ - Explicit list: [0.1, 0.2, 0.3] → pass through
124
+ - Simple range: "1:4" → [1, 2, 3, 4]
125
+ - Range with step: "-40:125:55" → [-40, 15, 70, 125]
126
+ - Float range: "0.1:0.5:0.1" → [0.1, 0.2, 0.3, 0.4, 0.5]
127
+ - Comma-separated: "3.3,5.0,12.0" → [3.3, 5.0, 12.0]
128
+ - Mixed: "0,0.5:2:0.5,5" → [0, 0.5, 1.0, 1.5, 2.0, 5]
129
+
130
+ Args:
131
+ spec: Range string or list of values
132
+
133
+ Returns:
134
+ List of float values
135
+
136
+ Examples:
137
+ >>> expand_numeric_range("-40:125:55")
138
+ [-40.0, 15.0, 70.0, 125.0]
139
+ >>> expand_numeric_range("0.1:0.5:0.1")
140
+ [0.1, 0.2, 0.3, 0.4, 0.5]
141
+ >>> expand_numeric_range([1, 2, 3])
142
+ [1.0, 2.0, 3.0]
143
+ """
144
+ # List: pass through with float conversion
145
+ if isinstance(spec, list):
146
+ return [float(v) for v in spec]
147
+
148
+ # Single numeric value
149
+ if isinstance(spec, (int, float)):
150
+ return [float(spec)]
151
+
152
+ # String parsing
153
+ if not isinstance(spec, str):
154
+ return [float(spec)]
155
+
156
+ result: list[float] = []
157
+
158
+ for part in spec.split(","):
159
+ part = part.strip()
160
+ if ":" in part:
161
+ bounds = part.split(":")
162
+ if len(bounds) == 2:
163
+ # Simple range: 1:4 → 1, 2, 3, 4 (step=1)
164
+ start = float(bounds[0])
165
+ stop = float(bounds[1])
166
+ step = 1.0 if start <= stop else -1.0
167
+ result.extend(_generate_float_range(start, stop, step))
168
+ elif len(bounds) == 3:
169
+ # Range with step: -40:125:55
170
+ start = float(bounds[0])
171
+ stop = float(bounds[1])
172
+ step = float(bounds[2])
173
+ result.extend(_generate_float_range(start, stop, step))
174
+ else:
175
+ result.append(float(part))
176
+
177
+ return result
178
+
179
+
180
+ def generate_numeric_range(
181
+ start: float,
182
+ stop: float,
183
+ step: float | None = None,
184
+ count: int | None = None,
185
+ ) -> list[float]:
186
+ """Generate a range of float values with step or count.
187
+
188
+ Exactly one of step or count must be provided. Range is inclusive of stop.
189
+
190
+ Args:
191
+ start: Starting value (inclusive)
192
+ stop: Ending value (inclusive)
193
+ step: Step size between values
194
+ count: Number of values to generate (evenly spaced)
195
+
196
+ Returns:
197
+ List of float values from start to stop (inclusive)
198
+
199
+ Raises:
200
+ ValueError: If neither step nor count provided, or both provided
201
+
202
+ Examples:
203
+ >>> generate_numeric_range(0, 5, step=1)
204
+ [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
205
+ >>> generate_numeric_range(0, 1, count=3)
206
+ [0.0, 0.5, 1.0]
207
+ """
208
+ if (step is None) == (count is None):
209
+ raise ValueError("Exactly one of 'step' or 'count' must be provided")
210
+
211
+ if step is not None:
212
+ return _generate_float_range(start, stop, step)
213
+ else:
214
+ # count is not None - generate evenly spaced values
215
+ assert count is not None
216
+ if count == 1:
217
+ return [start]
218
+ step_val = (stop - start) / (count - 1)
219
+ return [start + step_val * i for i in range(count)]
220
+
221
+
222
+ def _generate_float_range(start: float, stop: float, step: float) -> list[float]:
223
+ """Generate a range of float values with step, inclusive of stop.
224
+
225
+ Internal helper - use generate_numeric_range() for public API.
226
+
227
+ Args:
228
+ start: Starting value
229
+ stop: Ending value (inclusive)
230
+ step: Step size (must be non-zero)
231
+
232
+ Returns:
233
+ List of float values from start to stop (inclusive)
234
+ """
235
+ if step == 0:
236
+ raise ValueError("Step cannot be zero")
237
+
238
+ result: list[float] = []
239
+ current = start
240
+
241
+ # Handle direction
242
+ if step > 0:
243
+ while current <= stop + abs(step) * 1e-9: # Small tolerance for float comparison
244
+ result.append(current)
245
+ current += step
246
+ else:
247
+ while current >= stop - abs(step) * 1e-9: # Small tolerance for float comparison
248
+ result.append(current)
249
+ current += step
250
+
251
+ # Ensure we don't overshoot due to float accumulation
252
+ if result and step > 0 and result[-1] > stop + abs(step) * 0.1:
253
+ result.pop()
254
+ elif result and step < 0 and result[-1] < stop - abs(step) * 0.1:
255
+ result.pop()
256
+
257
+ return result
@@ -0,0 +1,82 @@
1
+ """YAML file validation for all TesterKit config types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+ from pydantic import ValidationError
10
+
11
+ from testerkit.schema_export import FileType
12
+ from testerkit.store import FILE_LOADERS, detect_file_type
13
+
14
+
15
+ def validate_yaml(
16
+ path: Path,
17
+ *,
18
+ file_type: FileType | None = None,
19
+ catalog_dir: Path | None = None,
20
+ ) -> list[str]:
21
+ """Validate a single YAML file against its TesterKit schema.
22
+
23
+ Args:
24
+ path: Path to the YAML file.
25
+ file_type: Explicit type to validate as. If None, auto-detects
26
+ from top-level keys.
27
+ catalog_dir: Root catalog directory (needed for catalog inheritance).
28
+
29
+ Returns:
30
+ List of error strings (empty means valid).
31
+ """
32
+ resolved_type = file_type or detect_file_type(path)
33
+
34
+ if resolved_type is None:
35
+ if not path.exists():
36
+ return [f"File not found: {path}"]
37
+ return ["Could not determine file type from YAML structure"]
38
+
39
+ if resolved_type == "catalog":
40
+ return _validate_catalog(path, catalog_dir)
41
+
42
+ loader = FILE_LOADERS.get(resolved_type)
43
+ if loader is None:
44
+ return [f"Unknown file type: {resolved_type!r}"]
45
+ return _run_loader(loader, path)
46
+
47
+
48
+ def _run_loader(loader: Callable, path: Path) -> list[str]:
49
+ """Run a loader and return validation errors."""
50
+ try:
51
+ loader(path)
52
+ return []
53
+ except ValidationError as exc:
54
+ return _format_validation_error(exc)
55
+ except (yaml.YAMLError, OSError, ValueError) as exc:
56
+ return [str(exc)]
57
+
58
+
59
+ def _validate_catalog(path: Path, catalog_dir: Path | None) -> list[str]:
60
+ """Validate a catalog entry using the full loader (handles inheritance)."""
61
+ from testerkit.store import load_catalog_entry
62
+
63
+ try:
64
+ load_catalog_entry(path, catalog_dir=catalog_dir)
65
+ return []
66
+ except ValidationError as exc:
67
+ return _format_validation_error(exc)
68
+ except (yaml.YAMLError, OSError, ValueError) as exc:
69
+ return [str(exc)]
70
+
71
+
72
+ def _format_validation_error(exc: ValidationError) -> list[str]:
73
+ """Format Pydantic ValidationError into readable strings."""
74
+ errors = []
75
+ for e in exc.errors():
76
+ loc = ".".join(str(x) for x in e["loc"])
77
+ msg = e["msg"]
78
+ typ = e["type"]
79
+ inp = e.get("input")
80
+ inp_str = f", input_value={inp!r}" if inp is not None else ""
81
+ errors.append(f" {loc}\n {msg} [type={typ}{inp_str}]")
82
+ return errors
testerkit/verbs.py ADDED
@@ -0,0 +1,142 @@
1
+ """Top-level verb functions — ``observe``, ``verify``, ``measure``, ``stream``.
2
+
3
+ The four test-author intent verbs are exposed three ways. Pick the
4
+ one that matches how your code is structured:
5
+
6
+ 1. **Top-level imports** (recommended — works from any code, not
7
+ only tests)::
8
+
9
+ from testerkit import observe, verify, measure, stream
10
+
11
+ def my_step(dmm, psu, voltage):
12
+ psu.set_voltage(voltage)
13
+ observe("psu.voltage", voltage)
14
+ verify("rail_v", dmm.measure_voltage(), Limit(low=4.75, high=5.25))
15
+ measure("rail_ripple", ripple(dmm.read_waveform())) # record-only
16
+
17
+ 2. **Pytest fixtures** (idiomatic when a test signature already
18
+ takes other fixtures)::
19
+
20
+ def test_rail(observe, verify, dmm, psu):
21
+ ...
22
+
23
+ 3. **Context methods** (programmatic / non-pytest paths)::
24
+
25
+ with TestHarness("my-run") as harness:
26
+ with harness.vector(...) as ctx:
27
+ ctx.observe("...", value)
28
+ ctx.verify("...", measured, limit)
29
+
30
+ All three shapes route through the same underlying
31
+ :class:`~testerkit.execution.harness.Context` methods, so behavior is
32
+ identical regardless of which shape your code reaches for.
33
+
34
+ Resolution chain: the top-level verbs read the active context from
35
+ :func:`testerkit.execution._state.get_current_context`. The pytest
36
+ ``context`` fixture pushes a Context onto that ContextVar; calling
37
+ a verb outside a pytest test (notebooks, scripts, custom UIs)
38
+ raises ``RuntimeError``.
39
+
40
+ **Interactive ``connect()`` mode does NOT populate the active
41
+ Context.** ``StationConnection.start`` wires the channel store and
42
+ event store ContextVars so that :func:`testerkit.channels.write` /
43
+ :func:`testerkit.channels.stream` / :func:`testerkit.files.write` /
44
+ :func:`testerkit.files.stream` work — but it deliberately does not
45
+ push a Context, because there is no test vector to anchor
46
+ observations and verifications to in interactive mode. Reach for
47
+ the store-direct surfaces (``channels.*`` / ``files.*``) in
48
+ notebooks, the operator UI, and bringup scripts. The test-author
49
+ verbs are test-author verbs.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ from typing import Any
55
+
56
+ from testerkit.execution._state import get_current_context
57
+
58
+
59
+ def _active_context() -> Any:
60
+ """Return the active :class:`Context`, or raise with a useful hint."""
61
+ ctx = get_current_context()
62
+ if ctx is None:
63
+ raise RuntimeError(
64
+ "No active TesterKit context. The top-level verbs (observe / "
65
+ "verify / measure / stream) are test-author verbs — they require the "
66
+ "pytest ``context`` fixture, which pushes a Context for the "
67
+ "duration of the test. Outside a pytest test (notebooks, "
68
+ "scripts, custom UIs), use the store-direct surfaces instead: "
69
+ "``testerkit.channels.write`` / ``testerkit.channels.stream`` for "
70
+ "channels, ``testerkit.files.write`` / ``testerkit.files.stream`` "
71
+ "for artifacts. Those work inside a ``connect(...)`` block "
72
+ "and don't require an active test context."
73
+ )
74
+ return ctx
75
+
76
+
77
+ def observe(key: str, value: Any, *, namespace: str | None = None, unit: str | None = None) -> None:
78
+ """Record an observation (→ output lane in the EAV store).
79
+
80
+ Thin top-level pass-through to
81
+ :meth:`testerkit.execution.harness.Context.observe`. See that method
82
+ for the full polymorphic dispatch rules (scalar / Waveform /
83
+ numeric_array / blob / URI / sink-handle).
84
+ """
85
+ _active_context().observe(key, value, namespace=namespace, unit=unit)
86
+
87
+
88
+ def verify(
89
+ name: str,
90
+ value: float | int | None,
91
+ limit: Any = None,
92
+ *,
93
+ characteristic: str | None = None,
94
+ namespace: str | None = None,
95
+ unit: str | None = None,
96
+ ) -> Any:
97
+ """Record + judge a measurement (→ measurement row).
98
+
99
+ Thin top-level pass-through to
100
+ :meth:`testerkit.execution.harness.Context.verify`. See that method
101
+ for limit-resolution rules + ``MissingLimitError`` semantics.
102
+ """
103
+ return _active_context().verify(
104
+ name, value, limit, characteristic=characteristic, namespace=namespace, unit=unit
105
+ )
106
+
107
+
108
+ def measure(
109
+ name: str,
110
+ value: float | int | None,
111
+ limit: Any = None,
112
+ *,
113
+ characteristic: str | None = None,
114
+ namespace: str | None = None,
115
+ unit: str | None = None,
116
+ ) -> Any:
117
+ """Record a measurement without judging it (→ measurement row).
118
+
119
+ The record-only sibling of ``verify`` — stamps one measurement row
120
+ with ``Outcome.DONE`` and never raises on a missing limit. Use when
121
+ a value should be captured but not pass/fail judged
122
+ (characterization, diagnostics, logged context). Thin top-level
123
+ pass-through to :meth:`testerkit.execution.harness.Context.measure`.
124
+ """
125
+ return _active_context().measure(
126
+ name, value, limit, characteristic=characteristic, namespace=namespace, unit=unit
127
+ )
128
+
129
+
130
+ def stream(name: str, sample: Any, *, namespace: str | None = None, unit: str | None = None) -> str:
131
+ """Append one sample to a channel (→ ``channel://`` URI).
132
+
133
+ Thin top-level pass-through to
134
+ :meth:`testerkit.execution.harness.Context.stream`. Strictly
135
+ orthogonal to ``observe`` — never writes to the outputs lane on
136
+ the active vector; wire to a vector explicitly via
137
+ ``observe(name, sink)`` if association is wanted.
138
+ """
139
+ return _active_context().stream(name, sample, namespace=namespace, unit=unit)
140
+
141
+
142
+ __all__ = ["observe", "verify", "measure", "stream"]