oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,808 @@
1
+ """Automated regression test suite for protocol implementations.
2
+
3
+ This module provides comprehensive regression testing capabilities for detecting
4
+ changes in protocol behavior, tracking metrics over time, and maintaining baseline
5
+ test results (golden outputs).
6
+
7
+ Example:
8
+ >>> from oscura.validation import RegressionTestSuite
9
+ >>> from oscura.analyzers.protocols import UARTDecoder
10
+ >>>
11
+ >>> # Initialize suite and register test
12
+ >>> suite = RegressionTestSuite("my_protocol_tests")
13
+ >>> suite.register_test("uart_decode_hello", UARTDecoder.decode, input_data=b"Hello")
14
+ >>>
15
+ >>> # Capture baseline
16
+ >>> suite.capture_baseline("uart_decode_hello")
17
+ >>>
18
+ >>> # Run regression test
19
+ >>> result = suite.run_test("uart_decode_hello")
20
+ >>> if result.passed:
21
+ ... print("No regression detected")
22
+ >>> else:
23
+ ... print(f"Regression found: {result.differences}")
24
+ >>>
25
+ >>> # Generate report
26
+ >>> report = suite.generate_report()
27
+ >>> report.export_html("regression_report.html")
28
+
29
+ References:
30
+ V0.6.0_COMPLETE_COMPREHENSIVE_PLAN.md: Feature 40 (Regression Testing)
31
+ Software Testing: Regression test automation best practices
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import hashlib
37
+ import json
38
+ import time
39
+ from collections.abc import Callable
40
+ from dataclasses import dataclass, field
41
+ from datetime import datetime
42
+ from enum import Enum
43
+ from pathlib import Path
44
+ from typing import Any
45
+
46
+ import numpy as np
47
+
48
+
49
+ class ComparisonMode(Enum):
50
+ """Comparison mode for different types of protocol outputs.
51
+
52
+ Attributes:
53
+ EXACT: Exact byte-for-byte match (deterministic protocols).
54
+ FUZZY: Fuzzy match with tolerance (timing-dependent protocols).
55
+ STATISTICAL: Statistical comparison for noisy measurements.
56
+ FIELD_BY_FIELD: Field-by-field comparison with per-field tolerance.
57
+ """
58
+
59
+ EXACT = "exact"
60
+ FUZZY = "fuzzy"
61
+ STATISTICAL = "statistical"
62
+ FIELD_BY_FIELD = "field_by_field"
63
+
64
+
65
+ @dataclass
66
+ class RegressionTestResult:
67
+ """Result from a single regression test execution.
68
+
69
+ Attributes:
70
+ test_name: Name of the test.
71
+ baseline: Baseline (golden) output.
72
+ current: Current test output.
73
+ differences: List of detected differences.
74
+ passed: True if test passed (no regressions).
75
+ metrics: Performance metrics (execution_time, memory_usage, etc.).
76
+ timestamp: When test was executed.
77
+ comparison_mode: How outputs were compared.
78
+ confidence: Confidence in the result (0.0-1.0).
79
+
80
+ Example:
81
+ >>> result = RegressionTestResult(
82
+ ... test_name="test_decode",
83
+ ... baseline={"frames": 10},
84
+ ... current={"frames": 10},
85
+ ... differences=[],
86
+ ... passed=True,
87
+ ... metrics={"execution_time": 0.025}
88
+ ... )
89
+ """
90
+
91
+ test_name: str
92
+ baseline: Any
93
+ current: Any
94
+ differences: list[str]
95
+ passed: bool
96
+ metrics: dict[str, float] = field(default_factory=dict)
97
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
98
+ comparison_mode: ComparisonMode = ComparisonMode.EXACT
99
+ confidence: float = 1.0
100
+
101
+
102
+ @dataclass
103
+ class RegressionReport:
104
+ """Comprehensive regression test report.
105
+
106
+ Attributes:
107
+ suite_name: Name of the test suite.
108
+ results: All test results.
109
+ summary: Summary statistics.
110
+ regressions_found: List of test names with regressions.
111
+ timestamp: When report was generated.
112
+ baseline_version: Version of baseline data.
113
+ metadata: Additional report metadata.
114
+
115
+ Example:
116
+ >>> report = RegressionReport(
117
+ ... suite_name="protocol_tests",
118
+ ... results=[result1, result2],
119
+ ... summary={"total": 2, "passed": 2, "failed": 0},
120
+ ... regressions_found=[]
121
+ ... )
122
+ """
123
+
124
+ suite_name: str
125
+ results: list[RegressionTestResult]
126
+ summary: dict[str, int | float]
127
+ regressions_found: list[str]
128
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
129
+ baseline_version: str = "1.0"
130
+ metadata: dict[str, Any] = field(default_factory=dict)
131
+
132
+ def export_json(self, output: Path) -> None:
133
+ """Export report as JSON.
134
+
135
+ Args:
136
+ output: Output JSON file path.
137
+
138
+ Example:
139
+ >>> report.export_json(Path("report.json"))
140
+ """
141
+ data = {
142
+ "suite_name": self.suite_name,
143
+ "timestamp": self.timestamp,
144
+ "baseline_version": self.baseline_version,
145
+ "summary": self.summary,
146
+ "regressions_found": self.regressions_found,
147
+ "results": [
148
+ {
149
+ "test_name": r.test_name,
150
+ "passed": r.passed,
151
+ "differences": r.differences,
152
+ "metrics": r.metrics,
153
+ "timestamp": r.timestamp,
154
+ "comparison_mode": r.comparison_mode.value,
155
+ "confidence": r.confidence,
156
+ "baseline": self._serialize(r.baseline),
157
+ "current": self._serialize(r.current),
158
+ }
159
+ for r in self.results
160
+ ],
161
+ "metadata": self.metadata,
162
+ }
163
+
164
+ output.write_text(json.dumps(data, indent=2))
165
+
166
+ def export_html(self, output: Path) -> None:
167
+ """Generate HTML dashboard with visualizations.
168
+
169
+ Args:
170
+ output: Output HTML file path.
171
+
172
+ Example:
173
+ >>> report.export_html(Path("dashboard.html"))
174
+ """
175
+ passed = sum(1 for r in self.results if r.passed)
176
+ failed = len(self.results) - passed
177
+
178
+ html = [
179
+ "<!DOCTYPE html>",
180
+ "<html>",
181
+ "<head>",
182
+ f"<title>Regression Report: {self.suite_name}</title>",
183
+ "<style>",
184
+ "body { font-family: Arial, sans-serif; margin: 20px; }",
185
+ "h1 { color: #333; }",
186
+ ".summary { background: #f0f0f0; padding: 15px; border-radius: 5px; }",
187
+ ".passed { color: green; font-weight: bold; }",
188
+ ".failed { color: red; font-weight: bold; }",
189
+ ".test { border: 1px solid #ddd; margin: 10px 0; padding: 10px; }",
190
+ ".test-passed { border-left: 4px solid green; }",
191
+ ".test-failed { border-left: 4px solid red; }",
192
+ ".metrics { font-family: monospace; font-size: 0.9em; }",
193
+ "</style>",
194
+ "</head>",
195
+ "<body>",
196
+ f"<h1>Regression Report: {self.suite_name}</h1>",
197
+ f"<p>Generated: {self.timestamp}</p>",
198
+ f"<p>Baseline Version: {self.baseline_version}</p>",
199
+ "<div class='summary'>",
200
+ "<h2>Summary</h2>",
201
+ f"<p>Total Tests: {self.summary['total']}</p>",
202
+ f"<p class='passed'>Passed: {passed}</p>",
203
+ f"<p class='failed'>Failed: {failed}</p>",
204
+ f"<p>Pass Rate: {(passed / max(self.summary['total'], 1)) * 100:.1f}%</p>",
205
+ "</div>",
206
+ "<h2>Test Results</h2>",
207
+ ]
208
+
209
+ for r in self.results:
210
+ test_class = "test-passed" if r.passed else "test-failed"
211
+ status = "PASSED" if r.passed else "FAILED"
212
+ html.extend(
213
+ [
214
+ f"<div class='test {test_class}'>",
215
+ f"<h3>{r.test_name} - {status}</h3>",
216
+ f"<p>Comparison Mode: {r.comparison_mode.value}</p>",
217
+ f"<p>Confidence: {r.confidence:.2f}</p>",
218
+ ]
219
+ )
220
+
221
+ if r.differences:
222
+ html.append("<h4>Differences:</h4><ul>")
223
+ for diff in r.differences:
224
+ html.append(f"<li>{diff}</li>")
225
+ html.append("</ul>")
226
+
227
+ if r.metrics:
228
+ html.append("<h4>Metrics:</h4><div class='metrics'>")
229
+ for key, value in r.metrics.items():
230
+ html.append(f"<p>{key}: {value:.6f}</p>")
231
+ html.append("</div>")
232
+
233
+ html.append("</div>")
234
+
235
+ html.extend(["</body>", "</html>"])
236
+
237
+ output.write_text("\n".join(html))
238
+
239
+ def export_csv(self, output: Path) -> None:
240
+ """Export test results as CSV for historical tracking.
241
+
242
+ Args:
243
+ output: Output CSV file path.
244
+
245
+ Example:
246
+ >>> report.export_csv(Path("history.csv"))
247
+ """
248
+ import csv
249
+
250
+ with open(output, "w", newline="") as f:
251
+ writer = csv.writer(f)
252
+ writer.writerow(
253
+ [
254
+ "Test Name",
255
+ "Passed",
256
+ "Timestamp",
257
+ "Execution Time",
258
+ "Memory Usage",
259
+ "Differences Count",
260
+ ]
261
+ )
262
+ for r in self.results:
263
+ writer.writerow(
264
+ [
265
+ r.test_name,
266
+ r.passed,
267
+ r.timestamp,
268
+ r.metrics.get("execution_time", 0.0),
269
+ r.metrics.get("memory_usage", 0.0),
270
+ len(r.differences),
271
+ ]
272
+ )
273
+
274
+ def _serialize(self, obj: Any) -> Any:
275
+ """Serialize object for JSON export.
276
+
277
+ Args:
278
+ obj: Object to serialize.
279
+
280
+ Returns:
281
+ JSON-serializable representation.
282
+ """
283
+ if isinstance(obj, (str, int, float, bool)) or obj is None:
284
+ return obj
285
+ if isinstance(obj, bytes):
286
+ return obj.hex()
287
+ if isinstance(obj, np.ndarray):
288
+ return obj.tolist()
289
+ if isinstance(obj, dict):
290
+ return {k: self._serialize(v) for k, v in obj.items()}
291
+ if isinstance(obj, (list, tuple)):
292
+ return [self._serialize(item) for item in obj]
293
+ # Fallback: convert to string
294
+ return str(obj)
295
+
296
+
297
+ class RegressionTestSuite:
298
+ """Automated regression test suite for protocol implementations.
299
+
300
+ Manages test baselines, runs regression tests, tracks metrics over time,
301
+ and generates comprehensive reports.
302
+
303
+ Example:
304
+ >>> suite = RegressionTestSuite("uart_protocol")
305
+ >>> suite.register_test("decode_basic", decoder.decode, input_data=raw_bytes)
306
+ >>> suite.capture_baseline("decode_basic")
307
+ >>> result = suite.run_test("decode_basic")
308
+ >>> report = suite.generate_report()
309
+ """
310
+
311
+ def __init__(
312
+ self,
313
+ suite_name: str,
314
+ baseline_dir: Path | str | None = None,
315
+ auto_update_baselines: bool = False,
316
+ ) -> None:
317
+ """Initialize regression test suite.
318
+
319
+ Args:
320
+ suite_name: Name of the test suite.
321
+ baseline_dir: Directory for baseline storage (default: ./baselines/).
322
+ auto_update_baselines: Automatically update baselines on first run.
323
+
324
+ Example:
325
+ >>> suite = RegressionTestSuite("my_tests", baseline_dir="test_baselines")
326
+ """
327
+ self.suite_name = suite_name
328
+ self.baseline_dir = Path(baseline_dir) if baseline_dir else Path("baselines")
329
+ self.baseline_dir.mkdir(parents=True, exist_ok=True)
330
+ self.auto_update_baselines = auto_update_baselines
331
+
332
+ self.tests: dict[str, dict[str, Any]] = {}
333
+ self.baselines: dict[str, Any] = {}
334
+ self.results: list[RegressionTestResult] = []
335
+ self.metrics_history: dict[str, list[dict[str, float]]] = {}
336
+
337
+ def register_test(
338
+ self,
339
+ test_name: str,
340
+ test_function: Callable[..., Any],
341
+ comparison_mode: ComparisonMode = ComparisonMode.EXACT,
342
+ tolerance: float = 0.01,
343
+ **kwargs: Any,
344
+ ) -> None:
345
+ """Register a test for regression tracking.
346
+
347
+ Args:
348
+ test_name: Unique test name.
349
+ test_function: Function to test (must be deterministic or use fixed seed).
350
+ comparison_mode: How to compare outputs.
351
+ tolerance: Tolerance for fuzzy/statistical comparisons.
352
+ **kwargs: Arguments to pass to test_function.
353
+
354
+ Example:
355
+ >>> suite.register_test(
356
+ ... "uart_decode",
357
+ ... decoder.decode,
358
+ ... comparison_mode=ComparisonMode.EXACT,
359
+ ... input_data=test_bytes
360
+ ... )
361
+ """
362
+ self.tests[test_name] = {
363
+ "function": test_function,
364
+ "kwargs": kwargs,
365
+ "comparison_mode": comparison_mode,
366
+ "tolerance": tolerance,
367
+ }
368
+
369
+ def capture_baseline(self, test_name: str) -> None:
370
+ """Capture baseline (golden output) for a test.
371
+
372
+ Args:
373
+ test_name: Name of test to capture baseline for.
374
+
375
+ Raises:
376
+ KeyError: If test not registered.
377
+
378
+ Example:
379
+ >>> suite.capture_baseline("uart_decode")
380
+ """
381
+ if test_name not in self.tests:
382
+ raise KeyError(f"Test '{test_name}' not registered")
383
+
384
+ test = self.tests[test_name]
385
+ output = test["function"](**test["kwargs"])
386
+
387
+ self.baselines[test_name] = output
388
+ self._save_baseline(test_name, output)
389
+
390
+ def run_test(self, test_name: str) -> RegressionTestResult:
391
+ """Run a single regression test.
392
+
393
+ Args:
394
+ test_name: Name of test to run.
395
+
396
+ Returns:
397
+ Test result with comparison details.
398
+
399
+ Raises:
400
+ KeyError: If test not registered or baseline missing.
401
+
402
+ Example:
403
+ >>> result = suite.run_test("uart_decode")
404
+ >>> print(f"Passed: {result.passed}")
405
+ """
406
+ if test_name not in self.tests:
407
+ raise KeyError(f"Test '{test_name}' not registered")
408
+
409
+ test = self.tests[test_name]
410
+
411
+ # Load baseline
412
+ if test_name not in self.baselines:
413
+ self._load_baseline(test_name)
414
+
415
+ if test_name not in self.baselines:
416
+ if self.auto_update_baselines:
417
+ self.capture_baseline(test_name)
418
+ else:
419
+ raise KeyError(f"No baseline for test '{test_name}'")
420
+
421
+ baseline = self.baselines[test_name]
422
+
423
+ # Run test and measure metrics
424
+ start_time = time.perf_counter()
425
+ current = test["function"](**test["kwargs"])
426
+ execution_time = time.perf_counter() - start_time
427
+
428
+ # Compare outputs
429
+ differences, passed, confidence = self._compare_outputs(
430
+ baseline, current, test["comparison_mode"], test["tolerance"]
431
+ )
432
+
433
+ # Track metrics
434
+ metrics = {
435
+ "execution_time": execution_time,
436
+ }
437
+
438
+ result = RegressionTestResult(
439
+ test_name=test_name,
440
+ baseline=baseline,
441
+ current=current,
442
+ differences=differences,
443
+ passed=passed,
444
+ metrics=metrics,
445
+ comparison_mode=test["comparison_mode"],
446
+ confidence=confidence,
447
+ )
448
+
449
+ self.results.append(result)
450
+ self._track_metrics(test_name, metrics)
451
+
452
+ return result
453
+
454
+ def run_all(self) -> list[RegressionTestResult]:
455
+ """Run all registered tests.
456
+
457
+ Returns:
458
+ List of all test results.
459
+
460
+ Example:
461
+ >>> results = suite.run_all()
462
+ >>> failed = [r for r in results if not r.passed]
463
+ """
464
+ results = []
465
+ for test_name in self.tests:
466
+ try:
467
+ result = self.run_test(test_name)
468
+ results.append(result)
469
+ except Exception as e:
470
+ # Create failed result for exceptions
471
+ result = RegressionTestResult(
472
+ test_name=test_name,
473
+ baseline=None,
474
+ current=None,
475
+ differences=[f"Exception: {e}"],
476
+ passed=False,
477
+ confidence=0.0,
478
+ )
479
+ results.append(result)
480
+
481
+ return results
482
+
483
+ def generate_report(self) -> RegressionReport:
484
+ """Generate comprehensive regression report.
485
+
486
+ Returns:
487
+ Report with all results and summary statistics.
488
+
489
+ Example:
490
+ >>> report = suite.generate_report()
491
+ >>> report.export_html("report.html")
492
+ """
493
+ total = len(self.results)
494
+ passed = sum(1 for r in self.results if r.passed)
495
+ failed = total - passed
496
+ regressions = [r.test_name for r in self.results if not r.passed]
497
+
498
+ summary = {
499
+ "total": total,
500
+ "passed": passed,
501
+ "failed": failed,
502
+ "pass_rate": (passed / max(total, 1)) * 100,
503
+ }
504
+
505
+ # Calculate metric trends
506
+ trends = {}
507
+ for test_name, history in self.metrics_history.items():
508
+ if len(history) >= 2:
509
+ recent = history[-1]
510
+ previous = history[-2]
511
+ trends[test_name] = {
512
+ "execution_time_delta": recent.get("execution_time", 0.0)
513
+ - previous.get("execution_time", 0.0),
514
+ }
515
+
516
+ metadata = {
517
+ "test_count": len(self.tests),
518
+ "baseline_dir": str(self.baseline_dir),
519
+ "trends": trends,
520
+ }
521
+
522
+ return RegressionReport(
523
+ suite_name=self.suite_name,
524
+ results=self.results,
525
+ summary=summary,
526
+ regressions_found=regressions,
527
+ metadata=metadata,
528
+ )
529
+
530
+ def update_baseline(self, test_name: str) -> None:
531
+ """Update baseline for a test (when behavior change is intentional).
532
+
533
+ Args:
534
+ test_name: Name of test to update.
535
+
536
+ Example:
537
+ >>> suite.update_baseline("uart_decode") # Accept new behavior
538
+ """
539
+ if test_name not in self.tests:
540
+ raise KeyError(f"Test '{test_name}' not registered")
541
+
542
+ test = self.tests[test_name]
543
+ new_baseline = test["function"](**test["kwargs"])
544
+ self.baselines[test_name] = new_baseline
545
+ self._save_baseline(test_name, new_baseline)
546
+
547
+ def _compare_exact(self, baseline: Any, current: Any) -> tuple[list[str], bool, float]:
548
+ """Compare outputs with exact matching.
549
+
550
+ Args:
551
+ baseline: Baseline output
552
+ current: Current output
553
+
554
+ Returns:
555
+ Tuple of (differences, passed, confidence)
556
+ """
557
+ differences: list[str] = []
558
+ passed = baseline == current
559
+ if not passed:
560
+ differences.append(f"Exact match failed: {baseline} != {current}")
561
+ return differences, passed, 1.0
562
+
563
+ def _compare_fuzzy(
564
+ self, baseline: Any, current: Any, tolerance: float
565
+ ) -> tuple[list[str], bool, float]:
566
+ """Compare outputs with fuzzy tolerance.
567
+
568
+ Args:
569
+ baseline: Baseline output
570
+ current: Current output
571
+ tolerance: Tolerance for numeric differences
572
+
573
+ Returns:
574
+ Tuple of (differences, passed, confidence)
575
+ """
576
+ differences: list[str] = []
577
+ passed = True
578
+ confidence = 1.0
579
+
580
+ if isinstance(baseline, (int, float)) and isinstance(current, (int, float)):
581
+ diff = abs(baseline - current)
582
+ if diff > tolerance:
583
+ differences.append(f"Fuzzy match failed: |{baseline} - {current}| > {tolerance}")
584
+ passed = False
585
+ confidence = 1.0 - min(1.0, diff / tolerance)
586
+ elif isinstance(baseline, (list, tuple)) and isinstance(current, (list, tuple)):
587
+ if len(baseline) != len(current):
588
+ differences.append(f"Length mismatch: {len(baseline)} != {len(current)}")
589
+ passed = False
590
+ else:
591
+ for i, (b, c) in enumerate(zip(baseline, current, strict=True)):
592
+ if isinstance(b, (int, float)) and isinstance(c, (int, float)):
593
+ if abs(b - c) > tolerance:
594
+ differences.append(f"Element {i}: |{b} - {c}| > {tolerance}")
595
+ passed = False
596
+ else:
597
+ # Fallback to exact
598
+ if baseline != current:
599
+ differences.append("Fuzzy comparison not applicable, exact failed")
600
+ passed = False
601
+
602
+ return differences, passed, confidence
603
+
604
+ def _compare_statistical(
605
+ self, baseline: Any, current: Any, tolerance: float
606
+ ) -> tuple[list[str], bool, float]:
607
+ """Compare outputs using statistical measures.
608
+
609
+ Args:
610
+ baseline: Baseline output
611
+ current: Current output
612
+ tolerance: Tolerance for normalized RMSE
613
+
614
+ Returns:
615
+ Tuple of (differences, passed, confidence)
616
+ """
617
+ differences: list[str] = []
618
+ passed = True
619
+ confidence = 1.0
620
+
621
+ if not (isinstance(baseline, np.ndarray) and isinstance(current, np.ndarray)):
622
+ differences.append("Statistical comparison requires numpy arrays")
623
+ return differences, False, 1.0
624
+
625
+ if baseline.shape != current.shape:
626
+ differences.append(f"Shape mismatch: {baseline.shape} != {current.shape}")
627
+ return differences, False, 1.0
628
+
629
+ # Use normalized RMSE
630
+ rmse = np.sqrt(np.mean((baseline - current) ** 2))
631
+ baseline_range = np.ptp(baseline) if np.ptp(baseline) > 0 else 1.0
632
+ normalized_rmse = rmse / baseline_range
633
+
634
+ if normalized_rmse > tolerance:
635
+ differences.append(
636
+ f"Statistical difference: RMSE={rmse:.6f}, "
637
+ f"normalized={normalized_rmse:.6f} > {tolerance}"
638
+ )
639
+ passed = False
640
+ confidence = 1.0 - min(1.0, normalized_rmse / tolerance)
641
+
642
+ return differences, passed, confidence
643
+
644
+ def _compare_field_by_field(
645
+ self, baseline: Any, current: Any, tolerance: float
646
+ ) -> tuple[list[str], bool, float]:
647
+ """Compare dictionary outputs field by field.
648
+
649
+ Args:
650
+ baseline: Baseline output
651
+ current: Current output
652
+ tolerance: Tolerance for numeric field differences
653
+
654
+ Returns:
655
+ Tuple of (differences, passed, confidence)
656
+ """
657
+ differences: list[str] = []
658
+ passed = True
659
+
660
+ if not (isinstance(baseline, dict) and isinstance(current, dict)):
661
+ differences.append("Field-by-field comparison requires dictionaries")
662
+ return differences, False, 1.0
663
+
664
+ all_keys = set(baseline.keys()) | set(current.keys())
665
+ for key in all_keys:
666
+ if key not in baseline:
667
+ differences.append(f"Field '{key}' missing in baseline")
668
+ passed = False
669
+ elif key not in current:
670
+ differences.append(f"Field '{key}' missing in current")
671
+ passed = False
672
+ else:
673
+ b_val = baseline[key]
674
+ c_val = current[key]
675
+ if isinstance(b_val, (int, float)) and isinstance(c_val, (int, float)):
676
+ if abs(b_val - c_val) > tolerance:
677
+ differences.append(f"Field '{key}': |{b_val} - {c_val}| > {tolerance}")
678
+ passed = False
679
+ elif b_val != c_val:
680
+ differences.append(f"Field '{key}': {b_val} != {c_val}")
681
+ passed = False
682
+
683
+ return differences, passed, 1.0
684
+
685
+ def _compare_outputs(
686
+ self, baseline: Any, current: Any, mode: ComparisonMode, tolerance: float
687
+ ) -> tuple[list[str], bool, float]:
688
+ """Compare baseline and current outputs.
689
+
690
+ Args:
691
+ baseline: Baseline output.
692
+ current: Current output.
693
+ mode: Comparison mode.
694
+ tolerance: Tolerance for fuzzy/statistical comparisons.
695
+
696
+ Returns:
697
+ Tuple of (differences, passed, confidence).
698
+ """
699
+ if mode == ComparisonMode.EXACT:
700
+ return self._compare_exact(baseline, current)
701
+ elif mode == ComparisonMode.FUZZY:
702
+ return self._compare_fuzzy(baseline, current, tolerance)
703
+ elif mode == ComparisonMode.STATISTICAL:
704
+ return self._compare_statistical(baseline, current, tolerance)
705
+ else: # FIELD_BY_FIELD - all enum values covered
706
+ return self._compare_field_by_field(baseline, current, tolerance)
707
+
708
+ def _save_baseline(self, test_name: str, output: Any) -> None:
709
+ """Save baseline to disk.
710
+
711
+ Args:
712
+ test_name: Name of the test.
713
+ output: Baseline output to save.
714
+ """
715
+ baseline_file = self.baseline_dir / f"{test_name}.json"
716
+ data = {
717
+ "test_name": test_name,
718
+ "timestamp": datetime.now().isoformat(),
719
+ "output": self._serialize_for_storage(output),
720
+ }
721
+ baseline_file.write_text(json.dumps(data, indent=2))
722
+
723
+ def _load_baseline(self, test_name: str) -> None:
724
+ """Load baseline from disk.
725
+
726
+ Args:
727
+ test_name: Name of the test.
728
+ """
729
+ baseline_file = self.baseline_dir / f"{test_name}.json"
730
+ if baseline_file.exists():
731
+ data = json.loads(baseline_file.read_text())
732
+ self.baselines[test_name] = self._deserialize_from_storage(data["output"])
733
+
734
+ def _serialize_for_storage(self, obj: Any) -> Any:
735
+ """Serialize object for JSON storage.
736
+
737
+ Args:
738
+ obj: Object to serialize.
739
+
740
+ Returns:
741
+ JSON-serializable representation.
742
+ """
743
+ if isinstance(obj, (str, int, float, bool)) or obj is None:
744
+ return obj
745
+ if isinstance(obj, bytes):
746
+ return {"__type__": "bytes", "value": obj.hex()}
747
+ if isinstance(obj, np.ndarray):
748
+ return {"__type__": "ndarray", "value": obj.tolist(), "dtype": str(obj.dtype)}
749
+ if isinstance(obj, dict):
750
+ return {k: self._serialize_for_storage(v) for k, v in obj.items()}
751
+ if isinstance(obj, (list, tuple)):
752
+ return [self._serialize_for_storage(item) for item in obj]
753
+ # Fallback: convert to string
754
+ return str(obj)
755
+
756
+ def _deserialize_from_storage(self, obj: Any) -> Any:
757
+ """Deserialize object from JSON storage.
758
+
759
+ Args:
760
+ obj: Serialized object.
761
+
762
+ Returns:
763
+ Original object type.
764
+ """
765
+ if isinstance(obj, dict):
766
+ if "__type__" in obj:
767
+ if obj["__type__"] == "bytes":
768
+ return bytes.fromhex(obj["value"])
769
+ if obj["__type__"] == "ndarray":
770
+ return np.array(obj["value"], dtype=obj["dtype"])
771
+ return {k: self._deserialize_from_storage(v) for k, v in obj.items()}
772
+ if isinstance(obj, list):
773
+ return [self._deserialize_from_storage(item) for item in obj]
774
+ return obj
775
+
776
+ def _track_metrics(self, test_name: str, metrics: dict[str, float]) -> None:
777
+ """Track metrics over time for trend analysis.
778
+
779
+ Args:
780
+ test_name: Name of the test.
781
+ metrics: Current metrics.
782
+ """
783
+ if test_name not in self.metrics_history:
784
+ self.metrics_history[test_name] = []
785
+ self.metrics_history[test_name].append(metrics)
786
+
787
+ def get_baseline_hash(self, test_name: str) -> str:
788
+ """Get hash of baseline for version tracking.
789
+
790
+ Args:
791
+ test_name: Name of the test.
792
+
793
+ Returns:
794
+ SHA256 hash of baseline.
795
+
796
+ Example:
797
+ >>> baseline_hash = suite.get_baseline_hash("uart_decode")
798
+ """
799
+ if test_name not in self.baselines:
800
+ self._load_baseline(test_name)
801
+
802
+ if test_name not in self.baselines:
803
+ return ""
804
+
805
+ baseline_json = json.dumps(
806
+ self._serialize_for_storage(self.baselines[test_name]), sort_keys=True
807
+ )
808
+ return hashlib.sha256(baseline_json.encode()).hexdigest()