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,535 @@
1
+ """Ref-counted daemon lifecycle base class.
2
+
3
+ ``DaemonManager`` handles the common pattern shared by ``duckdb_manager``
4
+ and ``flight_manager``:
5
+
6
+ - Acquire / release with PID-list state files and file locks
7
+ - Detached process spawning with readiness polling
8
+ - atexit / signal cleanup
9
+ - Daemon-side ref monitoring with idle timeout
10
+
11
+ Subclasses set class-level file names and override ``_spawn_cmd()``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import atexit
17
+ import json
18
+ import logging
19
+ import os
20
+ import shutil
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ import time
25
+ import warnings
26
+ import zlib
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from filelock import FileLock
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # 300s (5 min) so daemons survive routine UI page switches. The
35
+ # previous 10s was timed to die exactly between operator
36
+ # navigations, causing every page click to wait for a fresh spawn
37
+ # (~1s python startup + parquet rediscovery, observed as ~10s
38
+ # loads with multi-daemon pages). Override via env var if a
39
+ # constrained dev environment needs more aggressive shutdown.
40
+ _IDLE_TIMEOUT = int(os.environ.get("TESTERKIT_DAEMON_IDLE_TIMEOUT", "300"))
41
+ _POLL_INTERVAL = 2 # seconds between daemon ref-count checks
42
+
43
+ # Spawn timeout — how long a client waits for a freshly-spawned
44
+ # daemon to write its ready file. The python interpreter startup
45
+ # alone is ~500ms; loading the testerkit package + DuckDB + Flight
46
+ # adds another second; on slow CI runners or under contention
47
+ # (multiple tests spawning daemons concurrently), this can stretch
48
+ # past the previous 10s ceiling. 30s is a safer ceiling that still
49
+ # surfaces real spawn failures (corrupt index, unbindable port).
50
+ _SPAWN_TIMEOUT = float(os.environ.get("TESTERKIT_DAEMON_SPAWN_TIMEOUT", "30"))
51
+
52
+ # Default number of CPUs a daemon process pins itself to. gRPC's pollers +
53
+ # EventEngine workers, Arrow's thread pool, and DuckDB's executor all size
54
+ # themselves to the visible core count, so an unpinned Flight daemon on a
55
+ # 24-core box spawns ~96 threads. Since every store × directory is its own
56
+ # daemon, the aggregate trips WSL's pids cgroup at ~30 daemons. Query daemons
57
+ # are I/O-bound, so a small core budget costs no meaningful throughput.
58
+ # Override (or disable with <= 0) via ``TESTERKIT_DAEMON_CPU_CAP``.
59
+ _CPU_CAP = int(os.environ.get("TESTERKIT_DAEMON_CPU_CAP", "4"))
60
+
61
+ # Track acquired managers so atexit/signal can release them all
62
+ _acquired: dict[str, DaemonManager] = {}
63
+
64
+
65
+ def _taskset_prefix(dir_key: str) -> list[str]:
66
+ """Return a ``taskset -c <cpus>`` command prefix that pins a spawned daemon
67
+ to a small core set, or ``[]`` when capping is off/unsupported/unneeded.
68
+
69
+ Prepended to the spawn command rather than set from inside the daemon:
70
+ gRPC, Arrow, and DuckDB all read the core count when they are *imported*,
71
+ and importing ``testerkit`` already pulls in pyarrow — so affinity has to be
72
+ set before the Python process starts. ``taskset`` does that and, unlike a
73
+ fork-time ``preexec_fn``, is safe to use from a multithreaded parent. It
74
+ ``exec``s into the command, so the ``-m <module> <dir>`` tail (and thus
75
+ daemon process-matching) is preserved.
76
+
77
+ Each Flight daemon otherwise sizes its gRPC pollers + EventEngine workers,
78
+ Arrow pool, and DuckDB executor to the full core count (~96 threads on a
79
+ 24-core box); since every store × directory is its own daemon, the
80
+ aggregate trips WSL's pids cgroup at ~30 daemons. Query daemons are
81
+ I/O-bound, so a small core budget costs no meaningful throughput.
82
+
83
+ ``dir_key`` (the daemon's directory) offsets the chosen core window by a
84
+ stable hash so different projects' daemons spread across cores instead of
85
+ all piling onto cpus 0..cap-1. ``TESTERKIT_DAEMON_CPU_CAP`` overrides the cap;
86
+ <= 0 disables it.
87
+ """
88
+ if _CPU_CAP <= 0 or not hasattr(os, "sched_getaffinity"):
89
+ return []
90
+ taskset = shutil.which("taskset")
91
+ if taskset is None:
92
+ return []
93
+ try:
94
+ available = sorted(os.sched_getaffinity(0))
95
+ except OSError:
96
+ return []
97
+ n = len(available)
98
+ if _CPU_CAP >= n:
99
+ return [] # already at/under the cap — don't restrict
100
+ offset = zlib.crc32(dir_key.encode()) % n
101
+ chosen = sorted({available[(offset + i) % n] for i in range(_CPU_CAP)})
102
+ return [taskset, "-c", ",".join(str(c) for c in chosen)]
103
+
104
+
105
+ def daemon_duckdb_config() -> dict[str, Any]:
106
+ """DuckDB ``connect(config=...)`` sizing the executor pool to the process's
107
+ *allowed* cores — for a daemon's long-lived index connection.
108
+
109
+ DuckDB defaults its thread pool to ``hardware_concurrency()`` (the whole
110
+ machine), ignoring CPU affinity — so a daemon pinned by :func:`_taskset_prefix`
111
+ to a few cores otherwise spawns ~24 oversubscribed executor threads on those
112
+ cores. Sizing to ``sched_getaffinity`` drops that to the pin with no
113
+ throughput loss (you can't outrun the cores you're pinned to). It also
114
+ auto-scopes: a client that opens the same index unpinned reads its full core
115
+ count, so only daemons shrink. No-op on platforms without ``sched_getaffinity``.
116
+ """
117
+ if not hasattr(os, "sched_getaffinity"):
118
+ return {}
119
+ try:
120
+ return {"threads": max(1, len(os.sched_getaffinity(0)))}
121
+ except OSError:
122
+ return {}
123
+
124
+
125
+ def _daemon_log_tail(log_path: Path, n: int = 25) -> str:
126
+ """Return the last ``n`` significant lines of a daemon log for error context.
127
+
128
+ Drops the high-volume Arrow Acero alignment warnings so a real traceback
129
+ (e.g. a DuckDB BinderException from a schema-drifted index) isn't buried.
130
+ """
131
+ try:
132
+ lines = log_path.read_text(errors="replace").splitlines()
133
+ except OSError:
134
+ return "(no daemon log)"
135
+ significant = [ln for ln in lines if "poorly aligned" not in ln]
136
+ tail = (significant or lines)[-n:]
137
+ return "--- daemon log tail ---\n" + "\n".join(tail)
138
+
139
+
140
+ def _installed_version() -> str:
141
+ """Return the installed testerkit version string."""
142
+ from testerkit import __version__
143
+
144
+ return __version__
145
+
146
+
147
+ def _version_tuple(v: str) -> tuple[int, ...]:
148
+ """Parse a version string into a comparable tuple of ints."""
149
+ parts: list[int] = []
150
+ for segment in v.split("."):
151
+ digits = ""
152
+ for ch in segment:
153
+ if ch.isdigit():
154
+ digits += ch
155
+ else:
156
+ break
157
+ parts.append(int(digits) if digits else 0)
158
+ return tuple(parts)
159
+
160
+
161
+ def _pid_alive(pid: int) -> bool:
162
+ """Check if a process is alive (cross-platform)."""
163
+ try:
164
+ os.kill(pid, 0)
165
+ return True
166
+ except (OSError, PermissionError):
167
+ return False
168
+
169
+
170
+ class DaemonManager:
171
+ """Base class for ref-counted daemon lifecycle management.
172
+
173
+ Subclasses set the four ``_*_name`` class variables. The simple
174
+ case (DuckDB events / runs daemons) sets ``_daemon_module`` and
175
+ ``_port_file`` and inherits the default ``_spawn_cmd`` /
176
+ ``_post_spawn_state`` shown below. Daemons that need extra
177
+ arguments (e.g. ``FlightDaemonManager`` passes host/port) override
178
+ those methods directly.
179
+ """
180
+
181
+ _state_name: str
182
+ _lock_name: str
183
+ _ready_name: str
184
+ _pid_name: str
185
+ # Optional class-level shortcuts for the common DuckDB-style daemons.
186
+ # Set by subclasses; ``None`` means the subclass overrides
187
+ # ``_spawn_cmd`` / ``_post_spawn_state`` directly.
188
+ _daemon_module: str | None = None
189
+ _port_file: str | None = None
190
+
191
+ def __init__(self, daemon_dir: Path) -> None:
192
+ self._dir = daemon_dir
193
+
194
+ # -- Client-side API -----------------------------------------------------
195
+
196
+ def acquire(self) -> None:
197
+ """Acquire a reference to the daemon, starting it if needed."""
198
+ self._dir.mkdir(parents=True, exist_ok=True)
199
+ lock = FileLock(self._dir / self._lock_name, timeout=10)
200
+ state = self._dir / self._state_name
201
+
202
+ with lock:
203
+ if state.exists():
204
+ try:
205
+ data = json.loads(state.read_text())
206
+ if _pid_alive(data["pid"]):
207
+ if self._can_reuse(data):
208
+ refs: list[int] = data.get("refs", [])
209
+ if os.getpid() not in refs:
210
+ refs.append(os.getpid())
211
+ data["refs"] = refs
212
+ state.write_text(json.dumps(data))
213
+ self._register_cleanup()
214
+ return
215
+ else:
216
+ warnings.warn(
217
+ f"Restarting {type(self).__name__} daemon "
218
+ "(running instance is incompatible with this client)",
219
+ stacklevel=2,
220
+ )
221
+ self._kill_daemon(data["pid"])
222
+ state.unlink(missing_ok=True)
223
+ (self._dir / self._ready_name).unlink(missing_ok=True)
224
+ (self._dir / self._pid_name).unlink(missing_ok=True)
225
+ except (json.JSONDecodeError, OSError, KeyError, TypeError) as exc:
226
+ warnings.warn(
227
+ f"Stale daemon state, respawning: {exc}",
228
+ stacklevel=2,
229
+ )
230
+ state.unlink(missing_ok=True)
231
+
232
+ self._spawn()
233
+ pid = self._read_pid()
234
+
235
+ data = {"pid": pid, "refs": [os.getpid()], **self._daemon_identity()}
236
+ data.update(self._post_spawn_state())
237
+ state.write_text(json.dumps(data))
238
+
239
+ self._register_cleanup()
240
+
241
+ def release(self) -> None:
242
+ """No-op. The daemon prunes dead client PIDs itself via
243
+ monitor_refs() every poll cycle — no blocking lock needed on
244
+ the caller's exit path. Ctrl+C on ``testerkit serve`` is instant."""
245
+ return
246
+
247
+ def read_state(self) -> dict:
248
+ """Read the current state file. Returns empty dict if missing."""
249
+ state = self._dir / self._state_name
250
+ if not state.exists():
251
+ return {}
252
+ try:
253
+ return json.loads(state.read_text())
254
+ except (json.JSONDecodeError, OSError):
255
+ return {}
256
+
257
+ @staticmethod
258
+ def _kill_daemon(pid: int) -> None:
259
+ """Kill a daemon process and wait for it to exit."""
260
+ try:
261
+ os.kill(pid, signal.SIGTERM)
262
+ for _ in range(20):
263
+ time.sleep(0.1)
264
+ if not _pid_alive(pid):
265
+ return
266
+ os.kill(pid, signal.SIGKILL)
267
+ except (OSError, PermissionError):
268
+ pass
269
+
270
+ def force_restart(self) -> None:
271
+ """Kill a running daemon unconditionally so it rebuilds on next access."""
272
+ lock = FileLock(self._dir / self._lock_name, timeout=10)
273
+ state = self._dir / self._state_name
274
+
275
+ with lock:
276
+ if state.exists():
277
+ try:
278
+ data = json.loads(state.read_text())
279
+ pid = data.get("pid")
280
+ if pid and _pid_alive(pid):
281
+ self._kill_daemon(pid)
282
+ except (json.JSONDecodeError, OSError, KeyError, TypeError):
283
+ pass
284
+ state.unlink(missing_ok=True)
285
+ (self._dir / self._ready_name).unlink(missing_ok=True)
286
+ (self._dir / self._pid_name).unlink(missing_ok=True)
287
+
288
+ # -- Subclass hooks ------------------------------------------------------
289
+
290
+ def _daemon_identity(self) -> dict[str, Any]:
291
+ """The compatibility identity stamped into the state file on spawn —
292
+ what ``_can_reuse`` later compares against.
293
+
294
+ Default: the testerkit version (the version-ratchet). A subclass with a
295
+ projection fingerprint overrides this to add e.g. ``{"fingerprint":
296
+ ...}`` so reuse can key on it.
297
+ """
298
+ return {"testerkit_version": _installed_version()}
299
+
300
+ def _can_reuse(self, running_state: dict[str, Any]) -> bool:
301
+ """Whether THIS client may reuse the running daemon described by
302
+ ``running_state`` (its state-file dict).
303
+
304
+ Default reproduces the version ratchet exactly: reuse iff the
305
+ running daemon's version is >= ours (an older-or-equal client rides
306
+ a newer daemon; a newer client does not reuse an older one — it
307
+ respawns/upgrades). A subclass keying on a fingerprint overrides
308
+ this to require an exact match instead.
309
+ """
310
+ daemon_version = running_state.get("testerkit_version", "0.0.0")
311
+ return _version_tuple(daemon_version) >= _version_tuple(_installed_version())
312
+
313
+ def _spawn_cmd(self) -> list[str]:
314
+ """Command to spawn the daemon process.
315
+
316
+ Default uses ``_daemon_module`` if set: ``python -m <module>
317
+ <dir>``. Subclasses with extra args override this method.
318
+ """
319
+ if self._daemon_module is None:
320
+ raise NotImplementedError(
321
+ f"{type(self).__name__} must set _daemon_module or override _spawn_cmd"
322
+ )
323
+ return [sys.executable, "-m", self._daemon_module, str(self._dir)]
324
+
325
+ def _post_spawn_state(self) -> dict:
326
+ """Extra fields to store in state file after spawn.
327
+
328
+ Default uses ``_port_file`` if set: read the port file the
329
+ daemon writes before signalling ready, return ``{"location":
330
+ ...}``. Subclasses without a port file override or fall back
331
+ to the empty default.
332
+ """
333
+ if self._port_file is None:
334
+ return {}
335
+ return {"location": (self._dir / self._port_file).read_text().strip()}
336
+
337
+ # -- Daemon-side API -----------------------------------------------------
338
+
339
+ def write_ready(self) -> None:
340
+ """Write PID and ready files, update state. Called from daemon.
341
+
342
+ If the ready file was already written (e.g. Flight writes its port
343
+ file with actual content), this will not overwrite it — subclasses
344
+ that pre-write the ready file should call this after.
345
+ """
346
+ (self._dir / self._pid_name).write_text(str(os.getpid()))
347
+ ready = self._dir / self._ready_name
348
+ if not ready.exists():
349
+ ready.write_text("ok")
350
+
351
+ lock = FileLock(self._dir / self._lock_name, timeout=10)
352
+ state = self._dir / self._state_name
353
+ with lock:
354
+ if state.exists():
355
+ try:
356
+ data = json.loads(state.read_text())
357
+ data["pid"] = os.getpid()
358
+ state.write_text(json.dumps(data))
359
+ except (json.JSONDecodeError, OSError, TypeError) as exc:
360
+ warnings.warn(
361
+ f"Failed to update daemon state: {exc}",
362
+ stacklevel=2,
363
+ )
364
+
365
+ def update_state(self, **extra: object) -> None:
366
+ """Merge extra fields into the state file. Called from daemon."""
367
+ lock = FileLock(self._dir / self._lock_name, timeout=10)
368
+ state = self._dir / self._state_name
369
+ with lock:
370
+ if state.exists():
371
+ try:
372
+ data = json.loads(state.read_text())
373
+ data.update(extra)
374
+ state.write_text(json.dumps(data))
375
+ except (json.JSONDecodeError, OSError, TypeError) as exc:
376
+ warnings.warn(
377
+ f"Failed to update daemon state: {exc}",
378
+ stacklevel=2,
379
+ )
380
+
381
+ def monitor_refs(
382
+ self,
383
+ *,
384
+ idle_timeout: float = _IDLE_TIMEOUT,
385
+ poll_interval: float = _POLL_INTERVAL,
386
+ ) -> None:
387
+ """Block until refs drop to zero and idle timeout expires.
388
+
389
+ Called from daemon processes to know when to shut down.
390
+ """
391
+ state = self._dir / self._state_name
392
+ lock = FileLock(self._dir / self._lock_name, timeout=10)
393
+ idle_since: float | None = None
394
+
395
+ while True:
396
+ time.sleep(poll_interval)
397
+
398
+ with lock:
399
+ if not state.exists():
400
+ break
401
+ try:
402
+ data = json.loads(state.read_text())
403
+ refs: list[int] = data.get("refs", [])
404
+ live = [p for p in refs if _pid_alive(p)]
405
+ if live != refs:
406
+ data["refs"] = live
407
+ state.write_text(json.dumps(data))
408
+ refs = live
409
+ except (json.JSONDecodeError, OSError):
410
+ break
411
+
412
+ if not refs:
413
+ if idle_since is None:
414
+ idle_since = time.monotonic()
415
+ elif time.monotonic() - idle_since >= idle_timeout:
416
+ break
417
+ else:
418
+ idle_since = None
419
+
420
+ def cleanup_state_files(self) -> None:
421
+ """Remove state, ready, and PID files. Called from daemon on shutdown."""
422
+ with FileLock(self._dir / self._lock_name, timeout=5):
423
+ (self._dir / self._state_name).unlink(missing_ok=True)
424
+ (self._dir / self._ready_name).unlink(missing_ok=True)
425
+ (self._dir / self._pid_name).unlink(missing_ok=True)
426
+
427
+ # -- Internal ------------------------------------------------------------
428
+
429
+ def _spawn(self) -> None:
430
+ """Spawn daemon as a detached process, wait for ready file.
431
+
432
+ stdout/stderr are appended to ``_daemon.log`` in the daemon's
433
+ directory. Without a log sink the daemon's warnings (ingest
434
+ failures, schema-drift rebuilds, exceptions) vanish into
435
+ ``/dev/null`` and a misbehaving daemon looks identical to a
436
+ healthy one. With it, ``tail -f`` on the file shows the
437
+ actual reason a query is empty / slow / wrong.
438
+ """
439
+ ready_file = self._dir / self._ready_name
440
+ ready_file.unlink(missing_ok=True)
441
+
442
+ # Prefix with ``taskset`` (when available) so gRPC/Arrow/DuckDB size
443
+ # their thread pools to a small core budget — they read the core count
444
+ # at import, before any daemon code (or even ``import testerkit``) runs.
445
+ cmd = _taskset_prefix(str(self._dir)) + self._spawn_cmd()
446
+ log_path = self._dir / "_daemon.log"
447
+ log_path.parent.mkdir(parents=True, exist_ok=True)
448
+ log_handle = open(log_path, "ab", buffering=0)
449
+ kwargs: dict = {
450
+ "stdout": log_handle,
451
+ "stderr": subprocess.STDOUT,
452
+ "close_fds": True,
453
+ }
454
+ if sys.platform == "win32":
455
+ kwargs["creationflags"] = (
456
+ subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
457
+ )
458
+ else:
459
+ kwargs["start_new_session"] = True
460
+
461
+ try:
462
+ proc = subprocess.Popen(cmd, **kwargs)
463
+ finally:
464
+ # Daemon inherits the file descriptor; we close ours so
465
+ # the parent process doesn't keep the log file open.
466
+ log_handle.close()
467
+
468
+ deadline = time.monotonic() + _SPAWN_TIMEOUT
469
+ while time.monotonic() < deadline:
470
+ if ready_file.exists():
471
+ return
472
+ if proc.poll() is not None:
473
+ raise RuntimeError(
474
+ f"Daemon exited (code {proc.returncode}) before signaling ready. "
475
+ f"dir={self._dir}, cmd={cmd}\n{_daemon_log_tail(log_path)}"
476
+ )
477
+ time.sleep(0.05)
478
+
479
+ proc.kill()
480
+ proc.wait(timeout=2)
481
+ raise RuntimeError(
482
+ f"Daemon failed to start within {_SPAWN_TIMEOUT}s. dir={self._dir}, "
483
+ f"cmd={cmd}\n{_daemon_log_tail(log_path)}"
484
+ )
485
+
486
+ def _read_pid(self) -> int:
487
+ """Read the daemon PID from its PID file."""
488
+ pid_file = self._dir / self._pid_name
489
+ deadline = time.monotonic() + 5
490
+ while time.monotonic() < deadline:
491
+ if pid_file.exists():
492
+ try:
493
+ return int(pid_file.read_text().strip())
494
+ except ValueError:
495
+ pass
496
+ time.sleep(0.05)
497
+ raise RuntimeError("Daemon did not write PID file")
498
+
499
+ def _register_cleanup(self) -> None:
500
+ key = str(self._dir) + ":" + self._state_name
501
+ if key in _acquired:
502
+ return
503
+ _acquired[key] = self
504
+
505
+ def _cleanup() -> None:
506
+ mgr = _acquired.pop(key, None)
507
+ if mgr is not None:
508
+ try:
509
+ mgr.release()
510
+ except Exception: # noqa: BLE001 — deliberately broad for atexit cleanup
511
+ pass
512
+
513
+ atexit.register(_cleanup)
514
+
515
+ # No signal handlers — release() is a no-op so there is nothing
516
+ # to do on SIGINT/SIGTERM. pytest and uvicorn both manage their
517
+ # own signal handling; installing a handler here only interferes.
518
+
519
+
520
+ def wait_for_location(mgr: DaemonManager, store_dir: Path, label: str) -> str:
521
+ """Poll the daemon's state file until it writes its Flight location (up to 5s).
522
+
523
+ Shared by every store's manager (events / runs / files). ``label`` names
524
+ the store in the timeout error.
525
+ """
526
+ deadline = time.monotonic() + 5.0
527
+ while True:
528
+ location = mgr.read_state().get("location")
529
+ if location:
530
+ return location
531
+ if time.monotonic() >= deadline:
532
+ raise RuntimeError(
533
+ f"{label} daemon started but no location in state after 5s: {store_dir}"
534
+ )
535
+ time.sleep(0.05)