feat: CI test suite phases A+B, WaveformConfig separation, dead golden code cleanup
- Phase A: Remove self-blessing golden test from FPGA regression, wire MF co-sim (4 scenarios) into run_regression.sh, add opcode count guards to cross-layer tests (+3 tests) - Phase B: Add radar_params.vh parser and architectural param consistency tests (+7 tests), add banned stale-value pattern scanner (+1 test) - Separate WaveformConfig.range_resolution_m (physical, bandwidth-dependent) from bin_spacing_m (sample-rate dependent); rename all callers - Remove 151 lines of dead golden generate/compare code from tb_radar_receiver_final.v; testbench now runs structural + bounds only - Untrack generated MF co-sim CSV files, gitignore tb/golden/ directory CI: 256 tests total (168 python + 40 cross-layer + 27 FPGA + 21 MCU), all green
This commit is contained in:
+3
-1
@@ -33,9 +33,11 @@
|
||||
9_Firmware/9_2_FPGA/tb/cosim/compare_doppler_*.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/rtl_multiseg_*.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/rx_final_doppler_out.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/rtl_mf_*.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/compare_mf_*.csv
|
||||
|
||||
# Golden reference outputs (regenerated by testbenches)
|
||||
9_Firmware/9_2_FPGA/tb/golden/golden_doppler.mem
|
||||
9_Firmware/9_2_FPGA/tb/golden/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -253,6 +253,68 @@ run_lint_static() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: compile, run, and compare a matched-filter co-sim scenario
|
||||
# run_mf_cosim <scenario_name> <define_flag>
|
||||
# ---------------------------------------------------------------------------
|
||||
run_mf_cosim() {
|
||||
local name="$1"
|
||||
local define="$2"
|
||||
local vvp="tb/tb_mf_cosim_${name}.vvp"
|
||||
local scenario_lower="$name"
|
||||
|
||||
printf " %-45s " "MF Co-Sim ($name)"
|
||||
|
||||
# Compile — build command as string to handle optional define
|
||||
local cmd="iverilog -g2001 -DSIMULATION"
|
||||
if [[ -n "$define" ]]; then
|
||||
cmd="$cmd $define"
|
||||
fi
|
||||
cmd="$cmd -o $vvp tb/tb_mf_cosim.v matched_filter_processing_chain.v fft_engine.v chirp_memory_loader_param.v"
|
||||
|
||||
if ! eval "$cmd" 2>/tmp/iverilog_err_$$; then
|
||||
echo -e "${RED}COMPILE FAIL${NC}"
|
||||
ERRORS="$ERRORS\n MF Co-Sim ($name): compile error ($(head -1 /tmp/iverilog_err_$$))"
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
# Run TB
|
||||
local output
|
||||
output=$(timeout 120 vvp "$vvp" 2>&1) || true
|
||||
rm -f "$vvp"
|
||||
|
||||
# Check TB internal pass/fail
|
||||
local tb_fail
|
||||
tb_fail=$(echo "$output" | grep -Ec '^\[FAIL' || true)
|
||||
if [[ "$tb_fail" -gt 0 ]]; then
|
||||
echo -e "${RED}FAIL${NC} (TB internal failure)"
|
||||
ERRORS="$ERRORS\n MF Co-Sim ($name): TB internal failure"
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
# Run Python compare
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
local compare_out
|
||||
local compare_rc=0
|
||||
compare_out=$(python3 tb/cosim/compare_mf.py "$scenario_lower" 2>&1) || compare_rc=$?
|
||||
if [[ "$compare_rc" -ne 0 ]]; then
|
||||
echo -e "${RED}FAIL${NC} (compare_mf.py mismatch)"
|
||||
ERRORS="$ERRORS\n MF Co-Sim ($name): Python compare failed"
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}SKIP${NC} (RTL passed, python3 not found — compare skipped)"
|
||||
SKIP=$((SKIP + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}PASS${NC} (RTL + Python compare)"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: compile and run a single testbench
|
||||
# run_test <name> <vvp_path> <iverilog_args...>
|
||||
@@ -416,30 +478,14 @@ run_test "Full-Chain Real-Data (decim→Doppler, exact match)" \
|
||||
doppler_processor.v xfft_16.v fft_engine.v
|
||||
|
||||
if [[ "$QUICK" -eq 0 ]]; then
|
||||
# Golden generate
|
||||
run_test "Receiver (golden generate)" \
|
||||
tb/tb_rx_golden_reg.vvp \
|
||||
-DGOLDEN_GENERATE \
|
||||
tb/tb_radar_receiver_final.v radar_receiver_final.v \
|
||||
radar_mode_controller.v tb/ad9484_interface_400m_stub.v \
|
||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
||||
chirp_memory_loader_param.v latency_buffer.v \
|
||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
||||
rx_gain_control.v mti_canceller.v
|
||||
|
||||
# Golden compare
|
||||
run_test "Receiver (golden compare)" \
|
||||
tb/tb_rx_compare_reg.vvp \
|
||||
tb/tb_radar_receiver_final.v radar_receiver_final.v \
|
||||
radar_mode_controller.v tb/ad9484_interface_400m_stub.v \
|
||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
||||
chirp_memory_loader_param.v latency_buffer.v \
|
||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
||||
rx_gain_control.v mti_canceller.v
|
||||
# NOTE: The "Receiver golden generate/compare" pair was REMOVED because
|
||||
# it was self-blessing: both passes ran the same RTL with the same
|
||||
# deterministic stimulus, so the test always passed regardless of bugs.
|
||||
# Real co-sim coverage is provided by:
|
||||
# - tb_doppler_realdata.v (committed Python golden hex, exact match)
|
||||
# - tb_fullchain_realdata.v (committed Python golden hex, exact match)
|
||||
# A proper full-pipeline co-sim (DDC→MF→Decim→Doppler vs Python) is
|
||||
# planned as a replacement (Phase C of CI test plan).
|
||||
|
||||
# Full system top (monitoring-only, legacy)
|
||||
run_test "System Top (radar_system_tb)" \
|
||||
@@ -469,12 +515,28 @@ if [[ "$QUICK" -eq 0 ]]; then
|
||||
usb_data_interface.v edge_detector.v radar_mode_controller.v \
|
||||
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
|
||||
else
|
||||
echo " (skipped receiver golden + system top + E2E — use without --quick)"
|
||||
SKIP=$((SKIP + 4))
|
||||
echo " (skipped system top + E2E — use without --quick)"
|
||||
SKIP=$((SKIP + 2))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ===========================================================================
|
||||
# PHASE 2b: MATCHED FILTER CO-SIMULATION (RTL vs Python golden reference)
|
||||
# Runs tb_mf_cosim.v for 4 scenarios, then compare_mf.py validates output
|
||||
# against committed Python golden CSV files. In SIMULATION mode, thresholds
|
||||
# are generous (behavioral vs fixed-point twiddles differ) — validates
|
||||
# state machine mechanics, output count, and energy sanity.
|
||||
# ===========================================================================
|
||||
echo "--- PHASE 2b: Matched Filter Co-Sim ---"
|
||||
|
||||
run_mf_cosim "chirp" ""
|
||||
run_mf_cosim "dc" "-DSCENARIO_DC"
|
||||
run_mf_cosim "impulse" "-DSCENARIO_IMPULSE"
|
||||
run_mf_cosim "tone5" "-DSCENARIO_TONE5"
|
||||
|
||||
echo ""
|
||||
|
||||
# ===========================================================================
|
||||
# PHASE 3: UNIT TESTS — Signal Processing
|
||||
# ===========================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,43 +7,21 @@
|
||||
// -> matched_filter_multi_segment -> range_bin_decimator
|
||||
// -> doppler_processor_optimized -> doppler_output
|
||||
//
|
||||
// ============================================================================
|
||||
// TWO MODES (compile-time define):
|
||||
//
|
||||
// 1. GOLDEN_GENERATE mode (-DGOLDEN_GENERATE):
|
||||
// Dumps all Doppler output samples to golden reference files.
|
||||
// Run once on known-good RTL:
|
||||
// iverilog -g2001 -DSIMULATION -DGOLDEN_GENERATE -o tb_golden_gen.vvp \
|
||||
// <src files> tb/tb_radar_receiver_final.v
|
||||
// mkdir -p tb/golden
|
||||
// vvp tb_golden_gen.vvp
|
||||
//
|
||||
// 2. Default mode (no GOLDEN_GENERATE):
|
||||
// Loads golden files, compares each Doppler output against reference,
|
||||
// and runs physics-based bounds checks.
|
||||
// iverilog -g2001 -DSIMULATION -o tb_radar_receiver_final.vvp \
|
||||
// <src files> tb/tb_radar_receiver_final.v
|
||||
// vvp tb_radar_receiver_final.vvp
|
||||
//
|
||||
// PREREQUISITES:
|
||||
// - The directory tb/golden/ must exist before running either mode.
|
||||
// Create it with: mkdir -p tb/golden
|
||||
//
|
||||
// TAP POINTS:
|
||||
// Tap 1 (DDC output) - bounds checking only (CDC jitter -> non-deterministic)
|
||||
// Signals: dut.ddc_out_i [17:0], dut.ddc_out_q [17:0], dut.ddc_valid_i
|
||||
// Tap 2 (Doppler output) - golden compared (deterministic after MF buffering)
|
||||
// Tap 2 (Doppler output) - structural + bounds checks (deterministic after MF)
|
||||
// Signals: doppler_output[31:0], doppler_valid, doppler_bin[4:0],
|
||||
// range_bin_out[5:0]
|
||||
//
|
||||
// Golden file: tb/golden/golden_doppler.mem
|
||||
// 2048 entries of 32-bit hex, indexed by range_bin*32 + doppler_bin
|
||||
//
|
||||
// Strategy:
|
||||
// - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives)
|
||||
// - Overrides radar_mode_controller timing params for fast simulation
|
||||
// - Feeds 120 MHz tone at ADC input (IF frequency -> DDC passband)
|
||||
// - Verifies structural correctness + golden comparison + bounds checks
|
||||
// - Verifies structural correctness (S1-S10) + physics bounds checks (B1-B5)
|
||||
// - Bit-accurate golden comparison is done by the MF co-sim tests
|
||||
// (tb_mf_cosim.v + compare_mf.py) and full-chain co-sim tests
|
||||
// (tb_doppler_realdata.v, tb_fullchain_realdata.v), not here.
|
||||
//
|
||||
// Convention: check task, VCD dump, CSV output, pass/fail summary
|
||||
// ============================================================================
|
||||
@@ -194,46 +172,6 @@ task check;
|
||||
end
|
||||
endtask
|
||||
|
||||
// ============================================================================
|
||||
// GOLDEN MEMORY DECLARATIONS AND LOAD/STORE LOGIC
|
||||
// ============================================================================
|
||||
localparam GOLDEN_ENTRIES = 2048; // 64 range bins * 32 Doppler bins
|
||||
localparam GOLDEN_TOLERANCE = 2; // +/- 2 LSB tolerance for comparison
|
||||
|
||||
reg [31:0] golden_doppler [0:2047];
|
||||
|
||||
// -- Golden comparison tracking --
|
||||
integer golden_match_count;
|
||||
integer golden_mismatch_count;
|
||||
integer golden_max_err_i;
|
||||
integer golden_max_err_q;
|
||||
integer golden_compare_count;
|
||||
|
||||
`ifdef GOLDEN_GENERATE
|
||||
// In generate mode, we just initialize the array to X/0
|
||||
// and fill it as outputs arrive
|
||||
integer gi;
|
||||
initial begin
|
||||
for (gi = 0; gi < GOLDEN_ENTRIES; gi = gi + 1)
|
||||
golden_doppler[gi] = 32'd0;
|
||||
golden_match_count = 0;
|
||||
golden_mismatch_count = 0;
|
||||
golden_max_err_i = 0;
|
||||
golden_max_err_q = 0;
|
||||
golden_compare_count = 0;
|
||||
end
|
||||
`else
|
||||
// In comparison mode, load the golden reference
|
||||
initial begin
|
||||
$readmemh("tb/golden/golden_doppler.mem", golden_doppler);
|
||||
golden_match_count = 0;
|
||||
golden_mismatch_count = 0;
|
||||
golden_max_err_i = 0;
|
||||
golden_max_err_q = 0;
|
||||
golden_compare_count = 0;
|
||||
end
|
||||
`endif
|
||||
|
||||
// ============================================================================
|
||||
// DDC ENERGY ACCUMULATOR (Bounds Check B1)
|
||||
// ============================================================================
|
||||
@@ -257,7 +195,7 @@ always @(posedge clk_100m) begin
|
||||
end
|
||||
|
||||
// ============================================================================
|
||||
// DOPPLER OUTPUT CAPTURE, GOLDEN COMPARISON, AND DUPLICATE DETECTION
|
||||
// DOPPLER OUTPUT CAPTURE AND DUPLICATE DETECTION
|
||||
// ============================================================================
|
||||
integer doppler_output_count;
|
||||
integer doppler_frame_count;
|
||||
@@ -311,13 +249,6 @@ end
|
||||
// Monitor doppler outputs -- only after reset released
|
||||
always @(posedge clk_100m) begin
|
||||
if (reset_n && doppler_valid) begin : doppler_capture_block
|
||||
// ---- Signed intermediates for golden comparison ----
|
||||
reg signed [16:0] actual_i, actual_q;
|
||||
reg signed [16:0] expected_i, expected_q;
|
||||
reg signed [16:0] err_i_signed, err_q_signed;
|
||||
integer abs_err_i, abs_err_q;
|
||||
integer gidx;
|
||||
reg [31:0] expected_val;
|
||||
// ---- Magnitude intermediates for B2 ----
|
||||
reg signed [16:0] mag_i_signed, mag_q_signed;
|
||||
integer mag_i, mag_q, mag_sum;
|
||||
@@ -350,9 +281,6 @@ always @(posedge clk_100m) begin
|
||||
if ((doppler_output_count % 256) == 0)
|
||||
$display("[INFO] %0d doppler outputs so far (t=%0t)", doppler_output_count, $time);
|
||||
|
||||
// ---- Golden index computation ----
|
||||
gidx = range_bin_out * 32 + doppler_bin;
|
||||
|
||||
// ---- Duplicate detection (B5) ----
|
||||
if (range_bin_out < 64 && doppler_bin < 32) begin
|
||||
if (index_seen[range_bin_out][doppler_bin]) begin
|
||||
@@ -376,44 +304,6 @@ always @(posedge clk_100m) begin
|
||||
if (mag_sum > peak_dbin_mag[range_bin_out])
|
||||
peak_dbin_mag[range_bin_out] = mag_sum;
|
||||
end
|
||||
|
||||
`ifdef GOLDEN_GENERATE
|
||||
// ---- GOLDEN GENERATE: store output ----
|
||||
if (gidx < GOLDEN_ENTRIES)
|
||||
golden_doppler[gidx] = doppler_output;
|
||||
`else
|
||||
// ---- GOLDEN COMPARE: check against reference ----
|
||||
if (gidx < GOLDEN_ENTRIES) begin
|
||||
expected_val = golden_doppler[gidx];
|
||||
|
||||
actual_i = $signed(doppler_output[15:0]);
|
||||
actual_q = $signed(doppler_output[31:16]);
|
||||
expected_i = $signed(expected_val[15:0]);
|
||||
expected_q = $signed(expected_val[31:16]);
|
||||
|
||||
err_i_signed = actual_i - expected_i;
|
||||
err_q_signed = actual_q - expected_q;
|
||||
|
||||
abs_err_i = (err_i_signed < 0) ? -err_i_signed : err_i_signed;
|
||||
abs_err_q = (err_q_signed < 0) ? -err_q_signed : err_q_signed;
|
||||
|
||||
golden_compare_count = golden_compare_count + 1;
|
||||
|
||||
if (abs_err_i > golden_max_err_i) golden_max_err_i = abs_err_i;
|
||||
if (abs_err_q > golden_max_err_q) golden_max_err_q = abs_err_q;
|
||||
|
||||
if (abs_err_i <= GOLDEN_TOLERANCE && abs_err_q <= GOLDEN_TOLERANCE) begin
|
||||
golden_match_count = golden_match_count + 1;
|
||||
end else begin
|
||||
golden_mismatch_count = golden_mismatch_count + 1;
|
||||
if (golden_mismatch_count <= 20)
|
||||
$display("[MISMATCH] idx=%0d rbin=%0d dbin=%0d actual=%08h expected=%08h err_i=%0d err_q=%0d",
|
||||
gidx, range_bin_out, doppler_bin,
|
||||
doppler_output, expected_val,
|
||||
abs_err_i, abs_err_q);
|
||||
end
|
||||
end
|
||||
`endif
|
||||
end
|
||||
|
||||
// Track frame completions via doppler_proc -- only after reset
|
||||
@@ -556,13 +446,6 @@ initial begin
|
||||
end
|
||||
end
|
||||
|
||||
// ---- DUMP GOLDEN FILE (generate mode only) ----
|
||||
`ifdef GOLDEN_GENERATE
|
||||
$writememh("tb/golden/golden_doppler.mem", golden_doppler);
|
||||
$display("[GOLDEN_GENERATE] Wrote tb/golden/golden_doppler.mem (%0d entries captured)",
|
||||
doppler_output_count);
|
||||
`endif
|
||||
|
||||
// ================================================================
|
||||
// RUN CHECKS
|
||||
// ================================================================
|
||||
@@ -649,33 +532,7 @@ initial begin
|
||||
"S10: DDC produced substantial output (>100 valid samples)");
|
||||
|
||||
// ================================================================
|
||||
// GOLDEN COMPARISON REPORT
|
||||
// ================================================================
|
||||
`ifdef GOLDEN_GENERATE
|
||||
$display("");
|
||||
$display("Golden comparison: SKIPPED (GOLDEN_GENERATE mode)");
|
||||
$display(" Wrote golden reference with %0d Doppler samples", doppler_output_count);
|
||||
`else
|
||||
$display("");
|
||||
$display("------------------------------------------------------------");
|
||||
$display("GOLDEN COMPARISON (tolerance=%0d LSB)", GOLDEN_TOLERANCE);
|
||||
$display("------------------------------------------------------------");
|
||||
$display("Golden comparison: %0d/%0d match (tolerance=%0d LSB)",
|
||||
golden_match_count, golden_compare_count, GOLDEN_TOLERANCE);
|
||||
$display(" Mismatches: %0d (I-ch max_err=%0d, Q-ch max_err=%0d)",
|
||||
golden_mismatch_count, golden_max_err_i, golden_max_err_q);
|
||||
|
||||
// CHECK G1: All golden comparisons match
|
||||
if (golden_compare_count > 0) begin
|
||||
check(golden_mismatch_count == 0,
|
||||
"G1: All Doppler outputs match golden reference within tolerance");
|
||||
end else begin
|
||||
check(0, "G1: All Doppler outputs match golden reference (NO COMPARISONS)");
|
||||
end
|
||||
`endif
|
||||
|
||||
// ================================================================
|
||||
// BOUNDS CHECKS (active in both modes)
|
||||
// BOUNDS CHECKS
|
||||
// ================================================================
|
||||
$display("");
|
||||
$display("------------------------------------------------------------");
|
||||
@@ -748,16 +605,8 @@ initial begin
|
||||
// ================================================================
|
||||
$display("");
|
||||
$display("============================================================");
|
||||
$display("INTEGRATION TEST -- GOLDEN COMPARISON + BOUNDS");
|
||||
$display("INTEGRATION TEST -- STRUCTURAL + BOUNDS");
|
||||
$display("============================================================");
|
||||
`ifdef GOLDEN_GENERATE
|
||||
$display("Mode: GOLDEN_GENERATE (reference dump, comparison skipped)");
|
||||
`else
|
||||
$display("Golden comparison: %0d/%0d match (tolerance=%0d LSB)",
|
||||
golden_match_count, golden_compare_count, GOLDEN_TOLERANCE);
|
||||
$display(" Mismatches: %0d (I-ch max_err=%0d, Q-ch max_err=%0d)",
|
||||
golden_mismatch_count, golden_max_err_i, golden_max_err_q);
|
||||
`endif
|
||||
$display("Bounds checks:");
|
||||
$display(" B1: DDC RMS energy in range [%0d, %0d]",
|
||||
(ddc_energy_acc > 0) ? 1 : 0, DDC_MAX_ENERGY);
|
||||
|
||||
@@ -58,9 +58,9 @@ class TestRadarSettings(unittest.TestCase):
|
||||
|
||||
def test_has_physical_conversion_fields(self):
|
||||
s = _models().RadarSettings()
|
||||
self.assertIsInstance(s.range_resolution, float)
|
||||
self.assertIsInstance(s.range_bin_spacing, float)
|
||||
self.assertIsInstance(s.velocity_resolution, float)
|
||||
self.assertGreater(s.range_resolution, 0)
|
||||
self.assertGreater(s.range_bin_spacing, 0)
|
||||
self.assertGreater(s.velocity_resolution, 0)
|
||||
|
||||
def test_defaults(self):
|
||||
@@ -436,10 +436,19 @@ class TestWaveformConfig(unittest.TestCase):
|
||||
self.assertEqual(wc.decimation_factor, 16)
|
||||
|
||||
def test_range_resolution(self):
|
||||
"""range_resolution_m should be ~24.0 m/bin with PLFM defaults."""
|
||||
"""bin_spacing_m should be ~24.0 m/bin with PLFM defaults."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.range_resolution_m, 23.98, places=1)
|
||||
self.assertAlmostEqual(wc.bin_spacing_m, 23.98, places=1)
|
||||
|
||||
def test_range_resolution_physical(self):
|
||||
"""range_resolution_m = c/(2*BW), ~7.5 m at 20 MHz BW."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.range_resolution_m, 7.49, places=1)
|
||||
# 30 MHz BW → 5.0 m resolution
|
||||
wc30 = WaveformConfig(bandwidth_hz=30e6)
|
||||
self.assertAlmostEqual(wc30.range_resolution_m, 4.996, places=1)
|
||||
|
||||
def test_velocity_resolution(self):
|
||||
"""velocity_resolution_mps should be ~2.67 m/s/bin."""
|
||||
@@ -448,10 +457,10 @@ class TestWaveformConfig(unittest.TestCase):
|
||||
self.assertAlmostEqual(wc.velocity_resolution_mps, 2.67, places=1)
|
||||
|
||||
def test_max_range(self):
|
||||
"""max_range_m = range_resolution * n_range_bins."""
|
||||
"""max_range_m = bin_spacing * n_range_bins."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 64, places=1)
|
||||
self.assertAlmostEqual(wc.max_range_m, wc.bin_spacing_m * 64, places=1)
|
||||
|
||||
def test_max_velocity(self):
|
||||
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
|
||||
@@ -467,9 +476,9 @@ class TestWaveformConfig(unittest.TestCase):
|
||||
"""Non-default parameters correctly change derived values."""
|
||||
from v7.models import WaveformConfig
|
||||
wc1 = WaveformConfig()
|
||||
# Matched-filter: range_per_bin = c/(2*fs)*dec — proportional to 1/fs
|
||||
wc2 = WaveformConfig(sample_rate_hz=200e6) # double fs → halve range res
|
||||
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
|
||||
# Matched-filter: bin_spacing = c/(2*fs)*dec — proportional to 1/fs
|
||||
wc2 = WaveformConfig(sample_rate_hz=200e6) # double fs → halve bin spacing
|
||||
self.assertAlmostEqual(wc2.bin_spacing_m, wc1.bin_spacing_m / 2, places=2)
|
||||
|
||||
def test_zero_center_freq_velocity(self):
|
||||
"""Zero center freq should cause ZeroDivisionError in velocity calc."""
|
||||
@@ -927,7 +936,7 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
||||
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
||||
from v7.processing import extract_targets_from_frame
|
||||
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
||||
targets = extract_targets_from_frame(frame, range_resolution=23.98)
|
||||
targets = extract_targets_from_frame(frame, bin_spacing=23.98)
|
||||
self.assertEqual(len(targets), 1)
|
||||
self.assertAlmostEqual(targets[0].range, 10 * 23.98, places=1)
|
||||
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
||||
@@ -956,7 +965,7 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
||||
pitch=0.0, heading=90.0)
|
||||
frame = self._make_frame(det_cells=[(10, 16)])
|
||||
targets = extract_targets_from_frame(
|
||||
frame, range_resolution=100.0, gps=gps)
|
||||
frame, bin_spacing=100.0, gps=gps)
|
||||
# Should be roughly east of radar position
|
||||
self.assertAlmostEqual(targets[0].latitude, 41.9, places=2)
|
||||
self.assertGreater(targets[0].longitude, 12.5)
|
||||
|
||||
@@ -105,11 +105,11 @@ class RadarSettings:
|
||||
tab and Opcode enum in radar_protocol.py. This dataclass holds only
|
||||
host-side display/map settings and physical-unit conversion factors.
|
||||
|
||||
range_resolution and velocity_resolution should be calibrated to
|
||||
range_bin_spacing and velocity_resolution should be calibrated to
|
||||
the actual waveform parameters.
|
||||
"""
|
||||
system_frequency: float = 10.5e9 # Hz (PLFM TX LO, verified from ADF4382 config)
|
||||
range_resolution: float = 24.0 # Meters per decimated range bin (c/(2*100MSPS)*16)
|
||||
range_bin_spacing: float = 24.0 # Meters per decimated range bin (c/(2*100MSPS)*16)
|
||||
velocity_resolution: float = 2.67 # m/s per Doppler bin (lam/(2*32*167us))
|
||||
max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m (3 km mode)
|
||||
map_size: float = 1536 # Map display size (m)
|
||||
@@ -216,18 +216,30 @@ class WaveformConfig:
|
||||
decimation_factor: int = 16 # 1024 → 64
|
||||
|
||||
@property
|
||||
def range_resolution_m(self) -> float:
|
||||
def bin_spacing_m(self) -> float:
|
||||
"""Meters per decimated range bin (matched-filter receiver).
|
||||
|
||||
For matched-filter pulse compression: bin spacing = c / (2 * fs).
|
||||
After decimation the bin spacing grows by *decimation_factor*.
|
||||
This is independent of chirp bandwidth (BW affects resolution, not
|
||||
bin spacing).
|
||||
This is independent of chirp bandwidth (BW affects physical
|
||||
resolution, not bin spacing).
|
||||
"""
|
||||
c = 299_792_458.0
|
||||
raw_bin = c / (2.0 * self.sample_rate_hz)
|
||||
return raw_bin * self.decimation_factor
|
||||
|
||||
@property
|
||||
def range_resolution_m(self) -> float:
|
||||
"""Physical range resolution in meters, set by chirp bandwidth.
|
||||
|
||||
range_resolution = c / (2 * BW).
|
||||
At 20 MHz BW → 7.5 m; at 30 MHz BW → 5.0 m.
|
||||
This is distinct from bin_spacing_m (which depends on sample rate
|
||||
and decimation factor, not bandwidth).
|
||||
"""
|
||||
c = 299_792_458.0
|
||||
return c / (2.0 * self.bandwidth_hz)
|
||||
|
||||
@property
|
||||
def velocity_resolution_mps(self) -> float:
|
||||
"""m/s per Doppler bin. lambda / (2 * n_doppler * PRI)."""
|
||||
@@ -238,7 +250,7 @@ class WaveformConfig:
|
||||
@property
|
||||
def max_range_m(self) -> float:
|
||||
"""Maximum unambiguous range in meters."""
|
||||
return self.range_resolution_m * self.n_range_bins
|
||||
return self.bin_spacing_m * self.n_range_bins
|
||||
|
||||
@property
|
||||
def max_velocity_mps(self) -> float:
|
||||
|
||||
@@ -490,7 +490,7 @@ def polar_to_geographic(
|
||||
|
||||
def extract_targets_from_frame(
|
||||
frame,
|
||||
range_resolution: float = 1.0,
|
||||
bin_spacing: float = 1.0,
|
||||
velocity_resolution: float = 1.0,
|
||||
gps: GPSData | None = None,
|
||||
) -> list[RadarTarget]:
|
||||
@@ -503,8 +503,8 @@ def extract_targets_from_frame(
|
||||
----------
|
||||
frame : RadarFrame
|
||||
Frame with populated ``detections``, ``magnitude``, ``range_doppler_i/q``.
|
||||
range_resolution : float
|
||||
Meters per range bin.
|
||||
bin_spacing : float
|
||||
Meters per range bin (bin spacing, NOT bandwidth-limited resolution).
|
||||
velocity_resolution : float
|
||||
m/s per Doppler bin.
|
||||
gps : GPSData | None
|
||||
@@ -525,7 +525,7 @@ def extract_targets_from_frame(
|
||||
mag = float(frame.magnitude[rbin, dbin])
|
||||
snr = 10.0 * math.log10(max(mag, 1.0)) if mag > 0 else 0.0
|
||||
|
||||
range_m = float(rbin) * range_resolution
|
||||
range_m = float(rbin) * bin_spacing
|
||||
velocity_ms = float(dbin - doppler_center) * velocity_resolution
|
||||
|
||||
lat, lon, azimuth, elevation = 0.0, 0.0, 0.0, 0.0
|
||||
|
||||
@@ -169,7 +169,7 @@ class RadarDataWorker(QThread):
|
||||
The FPGA already does: FFT, MTI, CFAR, DC notch.
|
||||
Host-side DSP adds: clustering, tracking, geo-coordinate mapping.
|
||||
|
||||
Bin-to-physical conversion uses RadarSettings.range_resolution
|
||||
Bin-to-physical conversion uses RadarSettings.range_bin_spacing
|
||||
and velocity_resolution (should be calibrated to actual waveform).
|
||||
"""
|
||||
targets: list[RadarTarget] = []
|
||||
@@ -180,7 +180,7 @@ class RadarDataWorker(QThread):
|
||||
|
||||
# Extract detections from FPGA CFAR flags
|
||||
det_indices = np.argwhere(frame.detections > 0)
|
||||
r_res = self._settings.range_resolution
|
||||
r_res = self._settings.range_bin_spacing
|
||||
v_res = self._settings.velocity_resolution
|
||||
|
||||
for idx in det_indices:
|
||||
@@ -559,7 +559,7 @@ class ReplayWorker(QThread):
|
||||
# Target extraction
|
||||
targets = self._extract_targets(
|
||||
frame,
|
||||
range_resolution=self._waveform.range_resolution_m,
|
||||
bin_spacing=self._waveform.bin_spacing_m,
|
||||
velocity_resolution=self._waveform.velocity_resolution_mps,
|
||||
gps=self._gps,
|
||||
)
|
||||
|
||||
@@ -793,3 +793,51 @@ def parse_stm32_gpio_init(filepath: Path | None = None) -> list[GpioPin]:
|
||||
))
|
||||
|
||||
return pins
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# FPGA radar_params.vh parser
|
||||
# ===================================================================
|
||||
|
||||
def parse_radar_params_vh() -> dict[str, int]:
|
||||
"""
|
||||
Parse `define values from radar_params.vh.
|
||||
|
||||
Returns dict like {"RP_FFT_SIZE": 1024, "RP_DECIMATION_FACTOR": 16, ...}.
|
||||
Only parses defines with simple integer or Verilog literal values.
|
||||
Skips bit-width prefixed literals (e.g. 2'b00) — returns the numeric value.
|
||||
"""
|
||||
path = FPGA_DIR / "radar_params.vh"
|
||||
text = path.read_text()
|
||||
params: dict[str, int] = {}
|
||||
|
||||
for m in re.finditer(
|
||||
r'`define\s+(RP_\w+)\s+(\S+)', text
|
||||
):
|
||||
name = m.group(1)
|
||||
val_str = m.group(2).rstrip()
|
||||
|
||||
# Skip non-numeric defines (like RADAR_PARAMS_VH guard)
|
||||
if name == "RADAR_PARAMS_VH":
|
||||
continue
|
||||
|
||||
# Handle Verilog bit-width literals: 2'b00, 8'h30, etc.
|
||||
verilog_lit = re.match(r"\d+'([bhd])(\w+)", val_str)
|
||||
if verilog_lit:
|
||||
base_char = verilog_lit.group(1)
|
||||
digits = verilog_lit.group(2)
|
||||
base = {"b": 2, "h": 16, "d": 10}[base_char]
|
||||
params[name] = int(digits, base)
|
||||
continue
|
||||
|
||||
# Handle parenthesized expressions like (`RP_X * `RP_Y)
|
||||
if "(" in val_str or "`" in val_str:
|
||||
continue # Skip computed defines
|
||||
|
||||
# Plain integer
|
||||
try:
|
||||
params[name] = int(val_str)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return params
|
||||
|
||||
@@ -27,10 +27,12 @@ layers agree (because both could be wrong).
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -202,6 +204,58 @@ class TestTier1OpcodeContract:
|
||||
f"but ground truth says '{expected_reg}'"
|
||||
)
|
||||
|
||||
def test_opcode_count_exact_match(self):
|
||||
"""
|
||||
Total opcode count must match ground truth exactly.
|
||||
|
||||
This is a CANARY test: if an LLM agent or developer adds/removes
|
||||
an opcode in one layer without updating ground truth, this fails
|
||||
immediately. Update GROUND_TRUTH_OPCODES when intentionally
|
||||
changing the register map.
|
||||
"""
|
||||
expected_count = len(GROUND_TRUTH_OPCODES) # 25
|
||||
py_count = len(cp.parse_python_opcodes())
|
||||
v_count = len(cp.parse_verilog_opcodes())
|
||||
|
||||
assert py_count == expected_count, (
|
||||
f"Python has {py_count} opcodes but ground truth has {expected_count}. "
|
||||
f"If intentional, update GROUND_TRUTH_OPCODES in this test file."
|
||||
)
|
||||
assert v_count == expected_count, (
|
||||
f"Verilog has {v_count} opcodes but ground truth has {expected_count}. "
|
||||
f"If intentional, update GROUND_TRUTH_OPCODES in this test file."
|
||||
)
|
||||
|
||||
def test_no_extra_verilog_opcodes(self):
|
||||
"""
|
||||
Verilog must not have opcodes absent from ground truth.
|
||||
|
||||
Catches the case where someone adds a new case entry in
|
||||
radar_system_top.v but forgets to update the contract.
|
||||
"""
|
||||
v_opcodes = cp.parse_verilog_opcodes()
|
||||
extra = set(v_opcodes.keys()) - set(GROUND_TRUTH_OPCODES.keys())
|
||||
assert not extra, (
|
||||
f"Verilog has opcodes not in ground truth: "
|
||||
f"{[f'0x{x:02X} ({v_opcodes[x].register})' for x in extra]}. "
|
||||
f"Add them to GROUND_TRUTH_OPCODES if intentional."
|
||||
)
|
||||
|
||||
def test_no_extra_python_opcodes(self):
|
||||
"""
|
||||
Python must not have opcodes absent from ground truth.
|
||||
|
||||
Catches phantom opcodes (like the 0x06 incident) added by
|
||||
LLM agents that assume without verifying.
|
||||
"""
|
||||
py_opcodes = cp.parse_python_opcodes()
|
||||
extra = set(py_opcodes.keys()) - set(GROUND_TRUTH_OPCODES.keys())
|
||||
assert not extra, (
|
||||
f"Python has opcodes not in ground truth: "
|
||||
f"{[f'0x{x:02X} ({py_opcodes[x].name})' for x in extra]}. "
|
||||
f"Remove phantom opcodes or add to GROUND_TRUTH_OPCODES."
|
||||
)
|
||||
|
||||
|
||||
class TestTier1BitWidths:
|
||||
"""Verify register widths and opcode bit slices match ground truth."""
|
||||
@@ -300,6 +354,122 @@ class TestTier1StatusFieldPositions:
|
||||
)
|
||||
|
||||
|
||||
class TestTier1ArchitecturalParams:
|
||||
"""
|
||||
Verify radar_params.vh (FPGA single source of truth) matches Python
|
||||
WaveformConfig defaults and frozen architectural constants.
|
||||
|
||||
These tests catch:
|
||||
- LLM agents changing FFT size, bin counts, or sample rates in one
|
||||
layer without updating the other
|
||||
- Accidental parameter drift between FPGA and GUI
|
||||
- Unauthorized changes to critical architectural constants
|
||||
|
||||
When intentionally changing a parameter (e.g. FFT 1024→2048),
|
||||
update BOTH radar_params.vh AND WaveformConfig, then update the
|
||||
FROZEN_PARAMS dict below.
|
||||
"""
|
||||
|
||||
# Frozen architectural constants — update deliberately when changing arch
|
||||
FROZEN_PARAMS: ClassVar[dict[str, int]] = {
|
||||
"RP_FFT_SIZE": 1024,
|
||||
"RP_DECIMATION_FACTOR": 16,
|
||||
"RP_BINS_PER_SEGMENT": 64,
|
||||
"RP_OUTPUT_RANGE_BINS_3KM": 64,
|
||||
"RP_DOPPLER_FFT_SIZE": 16,
|
||||
"RP_NUM_DOPPLER_BINS": 32,
|
||||
"RP_CHIRPS_PER_FRAME": 32,
|
||||
"RP_CHIRPS_PER_SUBFRAME": 16,
|
||||
"RP_DATA_WIDTH": 16,
|
||||
"RP_PROCESSING_RATE_MHZ": 100,
|
||||
"RP_RANGE_PER_BIN_DM": 240, # 24.0 m in decimeters
|
||||
}
|
||||
|
||||
def test_radar_params_vh_parseable(self):
|
||||
"""radar_params.vh must exist and parse without error."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
assert len(params) > 10, (
|
||||
f"Only parsed {len(params)} defines from radar_params.vh — "
|
||||
f"expected > 10. Parser may be broken."
|
||||
)
|
||||
|
||||
def test_frozen_constants_unchanged(self):
|
||||
"""
|
||||
Critical architectural constants must match frozen values.
|
||||
|
||||
If this test fails, someone changed a fundamental parameter.
|
||||
Verify the change is intentional, update FROZEN_PARAMS, AND
|
||||
update all downstream consumers (GUI, testbenches, docs).
|
||||
"""
|
||||
params = cp.parse_radar_params_vh()
|
||||
for name, expected in self.FROZEN_PARAMS.items():
|
||||
assert name in params, (
|
||||
f"{name} missing from radar_params.vh! "
|
||||
f"Was it renamed or removed?"
|
||||
)
|
||||
assert params[name] == expected, (
|
||||
f"ARCHITECTURAL CHANGE DETECTED: {name} = {params[name]}, "
|
||||
f"expected {expected}. If intentional, update FROZEN_PARAMS "
|
||||
f"in this test AND all downstream consumers."
|
||||
)
|
||||
|
||||
def test_fpga_python_fft_size_match(self):
|
||||
"""FPGA FFT size must match Python WaveformConfig.fft_size."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_FFT_SIZE"] == wc.fft_size, (
|
||||
f"FFT size mismatch: radar_params.vh={params['RP_FFT_SIZE']}, "
|
||||
f"WaveformConfig={wc.fft_size}"
|
||||
)
|
||||
|
||||
def test_fpga_python_decimation_match(self):
|
||||
"""FPGA decimation factor must match Python WaveformConfig."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_DECIMATION_FACTOR"] == wc.decimation_factor, (
|
||||
f"Decimation mismatch: radar_params.vh={params['RP_DECIMATION_FACTOR']}, "
|
||||
f"WaveformConfig={wc.decimation_factor}"
|
||||
)
|
||||
|
||||
def test_fpga_python_range_bins_match(self):
|
||||
"""FPGA 3km output bins must match Python WaveformConfig.n_range_bins."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_OUTPUT_RANGE_BINS_3KM"] == wc.n_range_bins, (
|
||||
f"Range bins mismatch: radar_params.vh={params['RP_OUTPUT_RANGE_BINS_3KM']}, "
|
||||
f"WaveformConfig={wc.n_range_bins}"
|
||||
)
|
||||
|
||||
def test_fpga_python_doppler_bins_match(self):
|
||||
"""FPGA Doppler bins must match Python WaveformConfig.n_doppler_bins."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_NUM_DOPPLER_BINS"] == wc.n_doppler_bins, (
|
||||
f"Doppler bins mismatch: radar_params.vh={params['RP_NUM_DOPPLER_BINS']}, "
|
||||
f"WaveformConfig={wc.n_doppler_bins}"
|
||||
)
|
||||
|
||||
def test_fpga_python_sample_rate_match(self):
|
||||
"""FPGA processing rate must match Python WaveformConfig.sample_rate_hz."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
fpga_rate_hz = params["RP_PROCESSING_RATE_MHZ"] * 1e6
|
||||
assert fpga_rate_hz == wc.sample_rate_hz, (
|
||||
f"Sample rate mismatch: radar_params.vh={fpga_rate_hz/1e6} MHz, "
|
||||
f"WaveformConfig={wc.sample_rate_hz/1e6} MHz"
|
||||
)
|
||||
|
||||
|
||||
class TestTier1PacketConstants:
|
||||
"""Verify packet header/footer/size constants match across layers."""
|
||||
|
||||
@@ -826,3 +996,107 @@ class TestTier3CStub:
|
||||
assert result.get("parse_ok") == "true", (
|
||||
f"Boundary values rejected: {result}"
|
||||
)
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# TIER 4: Stale Value Detection (LLM Agent Guardrails)
|
||||
# ===================================================================
|
||||
|
||||
class TestTier4BannedPatterns:
|
||||
"""
|
||||
Scan source files for known-wrong values that have been corrected.
|
||||
|
||||
These patterns are stale ADI Phaser defaults, wrong sample rates,
|
||||
and incorrect range calculations that were cleaned up in commit
|
||||
d259e5c. If an LLM agent reintroduces them, this test catches it.
|
||||
|
||||
IMPORTANT: Allowlist entries exist for legitimate uses (e.g. comments
|
||||
explaining what was wrong, test files checking for these values).
|
||||
"""
|
||||
|
||||
# (regex_pattern, description, file_extensions_to_scan)
|
||||
BANNED_PATTERNS: ClassVar[list[tuple[str, str, tuple[str, ...]]]] = [
|
||||
# Wrong carrier frequency (ADI Phaser default, not PLFM)
|
||||
(r'10[._]?525\s*e\s*9|10\.525\s*GHz|10525000000',
|
||||
"Stale ADI Phaser carrier freq — PLFM uses 10.5 GHz",
|
||||
("*.py", "*.v", "*.vh", "*.cpp", "*.h")),
|
||||
|
||||
# Wrong post-DDC sample rate (ADI Phaser uses 4 MSPS)
|
||||
(r'(?<!\d)4e6(?!\d)|4_?000_?000\.0?\s*#.*(?:sample|samp|rate|fs)',
|
||||
"Stale ADI 4 MSPS rate — PLFM post-DDC is 100 MSPS",
|
||||
("*.py",)),
|
||||
|
||||
# Wrong range per bin values from old calculations
|
||||
(r'(?<!\d)4\.8\s*(?:m/bin|m per|meters?\s*per)',
|
||||
"Stale bin spacing 4.8 m — PLFM is 24.0 m/bin",
|
||||
("*.py", "*.cpp")),
|
||||
|
||||
(r'(?<!\d)5\.6\s*(?:m/bin|m per|meters?\s*per)',
|
||||
"Stale bin spacing 5.6 m — PLFM is 24.0 m/bin",
|
||||
("*.py", "*.cpp")),
|
||||
|
||||
# Wrong range resolution from deramped FMCW formula
|
||||
(r'781\.25',
|
||||
"Stale ADI range value 781.25 — not applicable to PLFM",
|
||||
("*.py",)),
|
||||
]
|
||||
|
||||
# Files that are allowed to contain banned patterns
|
||||
# (this test file, historical docs, comparison scripts)
|
||||
# ADI co-sim files legitimately reference ADI Phaser hardware params
|
||||
# because they process ADI test stimulus data (10.525 GHz, 4 MSPS).
|
||||
ALLOWLIST: ClassVar[set[str]] = {
|
||||
"test_cross_layer_contract.py", # This file — contains the patterns!
|
||||
"CLAUDE.md", # Context doc may reference old values
|
||||
"golden_reference.py", # Processes ADI Phaser test data
|
||||
"tb_fullchain_mti_cfar_realdata.v", # $display of ADI test stimulus info
|
||||
"tb_doppler_realdata.v", # $display of ADI test stimulus info
|
||||
"tb_range_fft_realdata.v", # $display of ADI test stimulus info
|
||||
"tb_fullchain_realdata.v", # $display of ADI test stimulus info
|
||||
}
|
||||
|
||||
def _scan_files(self, pattern_re, extensions):
|
||||
"""Scan firmware source files for a regex pattern."""
|
||||
hits = []
|
||||
firmware_dir = cp.REPO_ROOT / "9_Firmware"
|
||||
|
||||
for ext in extensions:
|
||||
for fpath in firmware_dir.rglob(ext.replace("*", "**/*") if "**" in ext else ext):
|
||||
# Skip allowlisted files
|
||||
if fpath.name in self.ALLOWLIST:
|
||||
continue
|
||||
# Skip __pycache__, .git, etc.
|
||||
parts = set(fpath.parts)
|
||||
if parts & {"__pycache__", ".git", ".venv", "venv", ".ruff_cache"}:
|
||||
continue
|
||||
|
||||
try:
|
||||
text = fpath.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
for i, line in enumerate(text.splitlines(), 1):
|
||||
if pattern_re.search(line):
|
||||
hits.append(f"{fpath.relative_to(cp.REPO_ROOT)}:{i}: {line.strip()[:120]}")
|
||||
|
||||
return hits
|
||||
|
||||
def test_no_banned_stale_values(self):
|
||||
"""
|
||||
No source file should contain known-wrong stale values.
|
||||
|
||||
If this fails, an LLM agent likely reintroduced a corrected value.
|
||||
Check the flagged lines and fix them. If a line is a legitimate
|
||||
use (e.g. a comment explaining history), add the file to ALLOWLIST.
|
||||
"""
|
||||
all_hits = []
|
||||
for pattern_str, description, extensions in self.BANNED_PATTERNS:
|
||||
pattern_re = re.compile(pattern_str, re.IGNORECASE)
|
||||
hits = self._scan_files(pattern_re, extensions)
|
||||
all_hits.extend(f"[{description}] {hit}" for hit in hits)
|
||||
|
||||
assert not all_hits, (
|
||||
f"Found {len(all_hits)} stale/banned value(s) in source files:\n"
|
||||
+ "\n".join(f" {h}" for h in all_hits[:20])
|
||||
+ ("\n ... and more" if len(all_hits) > 20 else "")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user