oscura 0.5.1__py3-none-any.whl → 0.7.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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.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()
|