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,113 @@
1
+ # Step 1: Run Something
2
+
3
+ **Goal:** Install TesterKit and run your first test against a mock instrument.
4
+
5
+ ## What You'll Build
6
+
7
+ A tiny project with one mock instrument and a passing measurement test — no hardware, and no station or part YAML yet. This is the **bench-bringup** scaffold: the smallest thing that records a real measurement.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install testerkit
13
+ ```
14
+
15
+ That installs the `testerkit` CLI and the pytest plugin — your tests are ordinary pytest functions; the plugin adds the hardware-test pieces.
16
+
17
+ ## Scaffold a project
18
+
19
+ ```bash
20
+ testerkit init my_project --tier=bringup
21
+ cd my_project
22
+ ```
23
+
24
+ The `bringup` tier is the smallest scaffold: mock instrument fixtures in a `conftest.py`, one smoke test, and one sidecar — no station, catalog, or part YAML. It creates:
25
+
26
+ ```
27
+ my_project/
28
+ ├── testerkit.yaml # project config
29
+ ├── pyproject.toml
30
+ ├── tests/
31
+ │ ├── conftest.py # mock dmm / psu fixtures
32
+ │ ├── test_smoke.py # measurement tests
33
+ │ └── test_smoke.yaml # sidecar limits
34
+ └── reports/
35
+ ```
36
+
37
+ ## Run it
38
+
39
+ ```bash
40
+ pytest -v
41
+ ```
42
+
43
+ Expected output:
44
+
45
+ ```
46
+ tests/test_smoke.py::test_rail_inline PASSED
47
+ tests/test_smoke.py::test_rail_sidecar PASSED
48
+ tests/test_smoke.py::test_current_draw PASSED
49
+ ```
50
+
51
+ Three measurements recorded against mock instruments, each checked against a limit.
52
+
53
+ ## What's in the scaffold
54
+
55
+ The `conftest.py` defines instrument fixtures with `MagicMock` standing in for a real driver:
56
+
57
+ ```python
58
+ # tests/conftest.py
59
+ from unittest.mock import MagicMock
60
+ import pytest
61
+
62
+
63
+ @pytest.fixture
64
+ def dmm() -> MagicMock:
65
+ """Bench DMM. Replace MagicMock with a real driver."""
66
+ inst = MagicMock()
67
+ inst.measure_dc_voltage.return_value = 3.3
68
+ return inst
69
+ ```
70
+
71
+ `test_smoke.py` ships three measurement tests; here's the first — it takes the `dmm` fixture and records a measurement:
72
+
73
+ ```python
74
+ # tests/test_smoke.py
75
+ def test_rail_inline(dmm, verify) -> None:
76
+ verify(
77
+ "v_rail",
78
+ float(dmm.measure_dc_voltage()),
79
+ limit={"low": 3.2, "high": 3.4, "nominal": 3.3, "unit": "V"},
80
+ )
81
+ ```
82
+
83
+ `verify` is a fixture the TesterKit plugin provides (installed with `testerkit`): it records the measurement and checks it against the limit, failing the test if the value is out of band. You'll meet `verify` and limits properly in [Step 3](03-fixtures.md) and [Step 4](04-limits.md) — for now, you've run a test that captures a real measurement. Swap the `MagicMock` for a [PyVISA](https://pyvisa.readthedocs.io/) or [PyMeasure](https://pymeasure.readthedocs.io/) driver when you move to the bench; the test body doesn't change.
84
+
85
+ ## About conftest.py
86
+
87
+ Right now the instruments come from `conftest.py` fixtures — the same pattern you'd use in any pytest project. TesterKit doesn't need its own configuration to get started.
88
+
89
+ Later steps introduce a [station YAML](../concepts/configuration/stations.md) — one file that declares the bench's instruments. When it exists, TesterKit auto-registers an instrument-role fixture for each instrument it declares (`dmm`, `psu`, …), and you delete the matching `conftest.py` fixtures. The test bodies stay the same.
90
+
91
+ ## Results
92
+
93
+ Each measurement is recorded to TesterKit's **run store** — the value `verify` captured, not just a pass/fail. Viewing and querying runs comes in later steps; the point for now is that the test recorded a real measurement.
94
+
95
+ ## Troubleshooting
96
+
97
+ **"pytest: command not found"** — make sure `testerkit` installed into the active environment, and if you use a virtualenv, that it's activated.
98
+
99
+ **"No tests collected"** — check the test file name starts with `test_` and each function starts with `test_`.
100
+
101
+ **"fixture 'dmm' not found"** — the fixture lives in `tests/conftest.py`, which `testerkit init --tier=bringup` creates. Later steps lift the fixture into a station YAML, where the role fixture is auto-registered.
102
+
103
+ ## What You Learned
104
+
105
+ - Install TesterKit with `pip install testerkit`
106
+ - Scaffold the smallest project with `testerkit init --tier=bringup`
107
+ - Run measurement tests against mock instruments with `pytest`
108
+
109
+ ## Continue
110
+
111
+ Next, run the same tests in TesterKit's mock mode and control the returned values from config.
112
+
113
+ ← [Quick Start](quickstart.md) | [Step 2: Mock Instruments →](02-mock-instruments.md)
@@ -0,0 +1,88 @@
1
+ # Step 2: Running Without Hardware
2
+
3
+ **Goal:** Run your tests without real instruments by wrapping your driver classes with TesterKit's `Mock` helper.
4
+
5
+ In step 1 you wrote vanilla pytest tests against `psu` and `dmm` fixtures defined in `conftest.py`. This step shows the smallest change that lets the same tests run on a laptop with no hardware attached.
6
+
7
+ ## The conftest pattern
8
+
9
+ Your `conftest.py` already returns driver instances. Wrap them in `testerkit.instruments.mocks.Mock` when the `--mock-instruments` flag is set:
10
+
11
+ ```python
12
+ # tests/conftest.py
13
+ import pytest
14
+ from drivers import DMM, PSU
15
+ from testerkit import Mock
16
+
17
+
18
+ @pytest.fixture(scope="session")
19
+ def psu(mock_instruments) -> PSU:
20
+ if mock_instruments:
21
+ return Mock(PSU, measure_voltage=5.0, measure_current=0.042)
22
+ return PSU(resource="TCPIP::192.168.1.101::INSTR")
23
+
24
+
25
+ @pytest.fixture(scope="session")
26
+ def dmm(mock_instruments) -> DMM:
27
+ if mock_instruments:
28
+ return Mock(DMM, measure_dc_voltage=3.31)
29
+ return DMM(resource="TCPIP::192.168.1.102::INSTR")
30
+ ```
31
+
32
+ `mock_instruments` is a fixture TesterKit provides — it returns `True` whenever `--mock-instruments` is on the command line or `TESTERKIT_MOCK_INSTRUMENTS=1` is set.
33
+
34
+ `Mock(DMM, measure_dc_voltage=3.31)` returns a stand-in `DMM` — every method call does nothing and returns `None` unless you give it a return value. Pass a literal for a constant reading, a `dict` to map an argument value to a reading, or a function for dynamic behavior.
35
+
36
+ This is exactly what [`examples/01-vanilla`](https://github.com/pragmatest-dev/testerkit/tree/main/examples/01-vanilla) and [`examples/02-verify`](https://github.com/pragmatest-dev/testerkit/tree/main/examples/02-verify) ship.
37
+
38
+ ## Running with mocks
39
+
40
+ ```bash
41
+ pytest tests/ --mock-instruments -v
42
+ ```
43
+
44
+ Same test code as step 1, no hardware required.
45
+
46
+ ```bash
47
+ # Or via env var
48
+ TESTERKIT_MOCK_INSTRUMENTS=1 pytest tests/ -v
49
+ ```
50
+
51
+ ## Mock cheat sheet
52
+
53
+ ```python
54
+ # Constant return
55
+ dmm = Mock(DMM, measure_dc_voltage=3.31)
56
+
57
+ # Map by argument — different return per query string
58
+ dmm = Mock(DMM, query={"MEAS:VOLT:DC?": "3.300", "*IDN?": "Keysight,34461A,..."})
59
+
60
+ # Callable — for noise or sweeps
61
+ import random
62
+ dmm = Mock(DMM, measure_dc_voltage=lambda: 3.3 + random.gauss(0, 0.005))
63
+ ```
64
+
65
+ Every method you don't configure does nothing and returns `None`. Reading an attribute you never configured raises an `AttributeError` instead — so a missing mock value fails loudly rather than silently returning nothing.
66
+
67
+ ## Mocks vs real hardware
68
+
69
+ | You run | `mock_instruments` is | Test code |
70
+ |---|---|---|
71
+ | `pytest tests/` | `False` | identical |
72
+ | `pytest tests/ --mock-instruments` | `True` | identical |
73
+
74
+ The point of the wrap-in-conftest pattern: **the test code is the same on a laptop and on the bench**. Tests don't know which mode they're in.
75
+
76
+ ## What you learned
77
+
78
+ - `--mock-instruments` flag + the `mock_instruments` fixture
79
+ - `Mock(DriverClass, method=return_value, ...)` wraps any driver class
80
+ - The conftest fixture decides real vs mock — tests don't change
81
+
82
+ In later steps you'll move this real-vs-mock choice out of `conftest.py` and into station YAML (step 7), so one setting serves a whole bench of tests. For now, conftest is enough.
83
+
84
+ ## Continue
85
+
86
+ Now let's adopt three of [TesterKit's per-test fixtures](../reference/pytest/fixtures.md) — `context`, `verify`, `measure` — to start recording measurements with limits.
87
+
88
+ ← [Step 1: Run Something](01-first-test.md) | [Step 3: pytest-native tests →](03-fixtures.md)
@@ -0,0 +1,169 @@
1
+ # Step 3: pytest-native tests
2
+
3
+ **Goal:** Adopt TesterKit's per-test fixtures so measurements get recorded with full [traceability](../how-to/execution/traceability.md).
4
+
5
+ In step 2, your tests called driver methods and used `assert` for pass/fail. TesterKit's `measure` and `verify` fixtures slot in alongside that, recording each measurement to the run record (see [data stores](../concepts/data/data-stores.md)) without changing how your test reads.
6
+
7
+ You don't need any new YAML for this step. Keep the `conftest.py` from step 2 — the `psu` / `dmm` fixtures still work.
8
+
9
+ ## The fixtures you add
10
+
11
+ All three are available on every test run — no station, no sidecar, no sweep required. `measure` and `verify` record measurements; `context` exposes what's active for this test — the run, UUT, station, and any sweep values.
12
+
13
+ | Fixture | What it gives the test | Verbs |
14
+ |----------|--------------------------------------------------------|--------------------------------------------------|
15
+ | `measure`| Records a measurement row — no pass/fail check | `measure(name, value, limit=None, characteristic=None)` |
16
+ | `verify` | Records the row, resolves a limit, raises on FAIL | `verify(name, value, limit=..., characteristic=...)` (`characteristic` = a named measurable property on the part spec — covered in step 6 / [concepts/capabilities](../concepts/configuration/capabilities.md)) |
17
+ | `context`| What's active — the run, UUT, station, and (if parametrized) sweep values | `get_param`, `changed`, `last`, `observe`, `.part`, `.station`, `.run` |
18
+
19
+ These are the three you'll reach for most. The TesterKit plugin for pytest provides more — hardware accessors (`pins`, `instruments`, `uut`), config accessors (`part`, `station_config`), and special-purpose fixtures (`vectors`, `sync`) — see the [TesterKit fixtures reference](../reference/pytest/fixtures.md) for the full set.
20
+
21
+ ## From assert to measure
22
+
23
+ Take the test from step 2:
24
+
25
+ ```python
26
+ def test_output_voltage(psu, dmm):
27
+ psu.set_voltage(5.0)
28
+ psu.enable_output()
29
+ v = dmm.measure_dc_voltage()
30
+ assert 3.2 <= v <= 3.4
31
+ ```
32
+
33
+ Add `measure` and record the measurement explicitly:
34
+
35
+ ```python
36
+ def test_output_voltage(psu, dmm, measure):
37
+ psu.set_voltage(5.0)
38
+ psu.enable_output()
39
+ v = dmm.measure_dc_voltage()
40
+ measure("output_voltage", v, limit={"low": 3.2, "high": 3.4, "unit": "V"})
41
+ assert 3.2 <= v <= 3.4
42
+ ```
43
+
44
+ Same control flow, but now there's a measurement recorded in the run record — value, units, limits, and outcome — visible to `testerkit runs`, the operator UI, and any downstream analysis.
45
+
46
+ ## Skip the assert with `verify`
47
+
48
+ `verify` is `measure` + `assert` in one call. Pass / fail is decided by the limit; an out-of-range value raises `AssertionError`:
49
+
50
+ ```python
51
+ def test_output_voltage(psu, dmm, verify):
52
+ psu.set_voltage(5.0)
53
+ psu.enable_output()
54
+ verify("output_voltage", dmm.measure_dc_voltage(),
55
+ limit={"low": 3.2, "high": 3.4, "unit": "V"})
56
+ ```
57
+
58
+ For one-off tests, passing `limit=` inline is fine. The cleaner home for limits is the part spec or the sidecar YAML — both arrive in later steps.
59
+
60
+ ## Classes group related tests
61
+
62
+ Grouping related tests in a plain pytest class is the standard way to structure a TesterKit test:
63
+
64
+ ```python
65
+ class TestPowerUp:
66
+ def test_input_voltage(self, psu, verify):
67
+ psu.set_voltage(5.0)
68
+ psu.enable_output()
69
+ verify("input_voltage", psu.measure_voltage(),
70
+ limit={"low": 4.5, "high": 5.5, "unit": "V"})
71
+
72
+ def test_output_voltage(self, dmm, verify):
73
+ verify("output_voltage", dmm.measure_dc_voltage(),
74
+ limit={"low": 3.2, "high": 3.4, "unit": "V"})
75
+ ```
76
+
77
+ Methods run in source order. Each emits its own [step](../concepts/execution/step-hierarchy.md) events; the class container's [outcome](../reference/data/models.md#enum-outcome) rolls up from the worst child outcome.
78
+
79
+ If a downstream test should skip when an upstream one fails, use `@pytest.mark.dependency(depends=["test_input_voltage"])` from the [`pytest-dependency`](https://pytest-dependency.readthedocs.io/) plugin — pytest's ecosystem, not a TesterKit addition.
80
+
81
+ ## Parametrize is first-class
82
+
83
+ `@pytest.mark.parametrize` works the way it always does. Add the `context` fixture if you want those parametrize values recorded with the measurement:
84
+
85
+ ```python
86
+ import pytest
87
+ @pytest.mark.parametrize("vin", [4.5, 5.0, 5.5])
88
+ def test_output_voltage(vin, psu, dmm, verify):
89
+ psu.set_voltage(vin)
90
+ psu.enable_output()
91
+ verify("output_voltage", dmm.measure_dc_voltage(),
92
+ limit={"low": 3.2, "high": 3.4, "unit": "V"})
93
+ ```
94
+
95
+ Each parametrized `vin` value is recorded as an **input** named `vin` on the measurement (its role is `input` — see [traceability](../how-to/execution/traceability.md)), so you can later query "how did output_voltage track vin?" — inputs are addressable by name and role — without adding extra code to the test. Sweeping from YAML instead of inline arrives in step 5.
96
+
97
+ TesterKit also adds a native sweep marker, `@pytest.mark.testerkit_sweeps`, that records the same inputs and supports range expanders (`linspace`, `arange`, `logspace`):
98
+
99
+ ```python
100
+ import pytest
101
+
102
+ @pytest.mark.testerkit_sweeps([{"vin": [4.5, 5.0, 5.5]}])
103
+ def test_output_voltage(vin, psu, dmm, verify):
104
+ ...
105
+ ```
106
+
107
+ Use `@pytest.mark.parametrize` when you want pytest's per-row `pytest.param(..., id="...")` metadata; use `@pytest.mark.testerkit_sweeps` when you want range expanders (`linspace` / `arange` / `logspace`) or want the sweep to match how you'll define it in YAML (step 5). See [`testerkit_sweeps`](../reference/pytest/markers.md#testerkit_sweeps) and the [TesterKit markers reference](../reference/pytest/markers.md) for all seven `testerkit_*` markers.
108
+
109
+ ## Multiple measurements per test
110
+
111
+ Each `verify` or `measure` call records one measurement. Call them as many times as you need:
112
+
113
+ ```python
114
+ def test_power_analysis(psu, dmm, verify):
115
+ verify("input_voltage", psu.measure_voltage(),
116
+ limit={"low": 4.5, "high": 5.5, "unit": "V"})
117
+ verify("input_current", psu.measure_current(),
118
+ limit={"high": 0.5, "unit": "A"})
119
+ verify("output_voltage", dmm.measure_dc_voltage(),
120
+ limit={"low": 3.2, "high": 3.4, "unit": "V"})
121
+ ```
122
+
123
+ ## Recording many samples
124
+
125
+ `measure` records one row per name within a step. For a stream of samples under a single name — a stability capture or a scope trace — use a channel (`stream`), covered in [Step 10](10-live-monitoring.md) and [Step 11](11-waveforms-and-evidence.md).
126
+
127
+ ## Running the tests
128
+
129
+ Nothing new on the command line — same `pytest` invocation from step 2:
130
+
131
+ ```bash
132
+ pytest tests/ --mock-instruments -v
133
+ ```
134
+
135
+ If you want to see the recorded measurements, list runs from the CLI:
136
+
137
+ ```bash
138
+ testerkit runs
139
+ testerkit show <run_id>
140
+ ```
141
+
142
+ ## What a measurement records
143
+
144
+ Read a run back and each measurement gives you:
145
+
146
+ | Field | Description |
147
+ |--------|-------------|
148
+ | `measurement_name` | name passed to `verify` / `measure` |
149
+ | `measurement_value` | the measured value |
150
+ | `measurement_unit` | unit (from `limit.unit`) |
151
+ | `measurement_outcome` | `passed` / `failed` / `done` / `skipped` / `errored` |
152
+ | `limit_low`, `limit_high`, `limit_nominal`, `limit_comparator` | the active limit |
153
+ | `measurement_timestamp` | when it was recorded |
154
+ | `vector_index` | which sweep variant (NULL for non-parametrized tests) |
155
+
156
+ Read these with `testerkit runs` / the operator UI, or the [Query API](../reference/data/query-api.md).
157
+
158
+ ## What you learned
159
+
160
+ - `measure(name, value, limit={"low": ..., "high": ..., "unit": "V"})` records a measurement explicitly
161
+ - `verify(name, value, limit=...)` does the same plus pass/fail + raise on FAIL
162
+ - Pytest classes group related tests; methods run in source order
163
+ - Parametrize works as it always does; values are recorded as inputs (role `input`)
164
+
165
+ ## Continue
166
+
167
+ So far you've been passing `limit=` inline on every `verify` call. Step 4 separates the limit shape from the test code.
168
+
169
+ ← [Step 2: Mock Instruments](02-mock-instruments.md) | [Step 4: Add Limits →](04-limits.md)
@@ -0,0 +1,139 @@
1
+ # Step 4: Add Limits
2
+
3
+ **Goal:** Decide pass/fail for a measurement.
4
+
5
+ In step 3 your tests called `verify(name, value, limit=...)`. The pass/fail decision happens because a **limit** is attached. This step covers the limit shape and the two ways to attach a limit from code: inline on the call, or via the `testerkit_limits` marker on the test function. Both end up as the same limit.
6
+
7
+ Step 5 moves limits out of code into a YAML file next to the test — keep that destination in mind, but don't reach for YAML yet.
8
+
9
+ ## The limit shape
10
+
11
+ A limit is a plain dict — same shape whether it lives inline, on a marker, or in YAML:
12
+
13
+ ```python
14
+ limit = {
15
+ "low": 3.135, # Minimum acceptable value
16
+ "high": 3.465, # Maximum acceptable value
17
+ "nominal": 3.3, # Expected value (optional)
18
+ "unit": "V", # Unit of measure
19
+ }
20
+ ```
21
+
22
+ Both `verify(name, value, limit=...)` and `measure(name, value, limit=...)` accept this dict directly. It's validated against the [`Limit`](../reference/data/models.md#model-limit) model. If you'd rather construct it explicitly — for IDE type-checking or a shared constant — `Limit` is re-exported from the top-level package:
23
+
24
+ ```python
25
+ from testerkit import Limit
26
+
27
+ V_RAIL = Limit(low=3.135, high=3.465, unit="V")
28
+ ```
29
+
30
+ The dict form is what tutorials and examples use; reach for `Limit(...)` when you want the model object.
31
+
32
+ ## How a measurement is checked
33
+
34
+ `measure(...)` records a [`Measurement`](../reference/data/models.md#model-measurement) row with the value, units, and limit. `verify(...)` does the same plus raises `AssertionError` on FAIL. Either way, the row carries an `Outcome`:
35
+
36
+ | Outcome | String value | Meaning |
37
+ |---------|--------------|---------|
38
+ | `Outcome.PASSED` | `"passed"` | Value within limits |
39
+ | `Outcome.FAILED` | `"failed"` | Value outside limits |
40
+ | `Outcome.SKIPPED` | `"skipped"` | Test was skipped |
41
+ | `Outcome.ERRORED` | `"errored"` | Test encountered an error |
42
+ | `Outcome.ABORTED` | `"aborted"` | Run aborted by operator |
43
+ | `Outcome.TERMINATED` | `"terminated"` | Run terminated (keyboard interrupt, signal) |
44
+ | `Outcome.DONE` | `"done"` | Recorded without a limit, or container with no measurements |
45
+
46
+ A parent step takes the **worst** outcome of its children, ranked `skipped < done < passed < failed < errored < terminated < aborted` (so a parent with one skipped child and one passing child still resolves to `passed`).
47
+
48
+ ## Inline limit on the call
49
+
50
+ The simplest form — pass `limit=` directly to `verify` or `measure`:
51
+
52
+ ```python
53
+ def test_output_voltage(dmm, verify):
54
+ verify(
55
+ "output_voltage",
56
+ dmm.measure_dc_voltage(),
57
+ limit={"low": 3.135, "high": 3.465, "unit": "V"},
58
+ )
59
+ ```
60
+
61
+ Inline limits are fine for one-off tests. They clutter the test body when limits get long or vary per test.
62
+
63
+ ## Limit via marker
64
+
65
+ `testerkit_limits` pulls the limit out of the body and pins it at the top of the test:
66
+
67
+ ```python
68
+ import pytest
69
+
70
+ @pytest.mark.testerkit_limits(
71
+ output_voltage={"low": 3.135, "high": 3.465, "unit": "V"},
72
+ )
73
+ def test_output_voltage(dmm, verify):
74
+ verify("output_voltage", dmm.measure_dc_voltage())
75
+ ```
76
+
77
+ The marker accepts one keyword per measurement name. `verify("output_voltage", ...)` resolves the limit from the marker without an explicit `limit=`. You can apply `@pytest.mark.testerkit_limits` at function, class, or module level — class scope applies to every method on the class.
78
+
79
+ ## Comparators
80
+
81
+ By default, limits use `GELE` (greater-or-equal to low, less-or-equal to high): `low <= value <= high`. Other comparators are available when the test needs a different shape:
82
+
83
+ ```python
84
+ # Upper limit only
85
+ limit = {"high": 1.0, "unit": "A", "comparator": "LE"} # value <= 1.0
86
+
87
+ # Lower limit only
88
+ limit = {"low": 0.0, "unit": "V", "comparator": "GE"} # value >= 0.0
89
+
90
+ # Must equal nominal
91
+ limit = {"nominal": 5.0, "unit": "V", "comparator": "EQ"}
92
+ ```
93
+
94
+ Full list:
95
+
96
+ | Comparator | Pass condition |
97
+ |------------|----------------|
98
+ | `GELE` | `low <= value <= high` (default) |
99
+ | `GELT` | `low <= value < high` |
100
+ | `GTLE` | `low < value <= high` |
101
+ | `GTLT` | `low < value < high` |
102
+ | `EQ` | `value == nominal` |
103
+ | `NE` | `value != nominal` |
104
+ | `GE` | `value >= low` |
105
+ | `GT` | `value > low` |
106
+ | `LE` | `value <= high` |
107
+ | `LT` | `value < high` |
108
+
109
+ ## Recording without judging
110
+
111
+ `measure` records a value without comparing it to a limit — pass no `limit=` and the row carries `Outcome.DONE`:
112
+
113
+ ```python
114
+ def test_voltage(dmm, measure):
115
+ measure("output_voltage", dmm.measure_dc_voltage())
116
+ ```
117
+
118
+ `verify` requires a limit: calling it with none (no inline `limit=`, no marker, no sidecar, no part spec) raises `MissingLimitError`. For a wide characterization sweep where you want the same `verify()` test code to record values without judging, set `verify_requires_limit: false` on a [profile](../how-to/execution/profiles.md) — `verify` then falls back to `measure` semantics for that session.
119
+
120
+ ## What's missing — and what step 5 fixes
121
+
122
+ Inline limits and markers live in the test code. That means a non-developer can't change them, condition-dependent limits get awkward, and limits can't be reused across multiple test files. Step 5 introduces the **[sidecar YAML](05-configuration.md)** — a file next to the test that carries limits (and sweeps, mocks, retries, prompts) without changing the test code.
123
+
124
+ For [condition-indexed bands](../how-to/execution/limits.md#condition-indexed-bands) (different bands at different temperatures or loads) jump to [Test limits](../how-to/execution/limits.md#condition-indexed-bands) when you need it.
125
+
126
+ ## What you learned
127
+
128
+ - The limit dict — `low`, `high`, `nominal`, `unit`, `comparator`
129
+ - Inline limits via `verify(..., limit={...})` or `measure(..., limit={...})`
130
+ - The `testerkit_limits` marker for attaching limits at class or function level
131
+ - The `Outcome` ladder and what each value means
132
+ - The `Comparator` enum for non-`GELE` checks
133
+ - Recording without judging via `measure(no limit)` or a record-only profile
134
+
135
+ ## Continue
136
+
137
+ Move the limits out of code and into a YAML file next to your test.
138
+
139
+ ← [Step 3: pytest-native tests](03-fixtures.md) | [Step 5: Test Configuration →](05-configuration.md)