""" Tests for XMLGenerator.generate — the sync_v2.xml writer. Covers the block-emission contract: * blocks are emitted for array indices 1..nb (seed at index 0 is represented by the header tag), so the last real block is never dropped; * ADC is reported as a single event per continuous HIGH run, and its window duration equals the full (un-truncated) intended duration; * the gradient system goes HIGH on the very last event; * every emitted CL value is within [20, 999999] ticks. """ from __future__ import annotations import xml.etree.ElementTree as ET from seq_interp.src.core.synchronizer import Synchronizer from seq_interp.src.hardware.constraints import HardwareConstraints from seq_interp.src.interfaces.xml_generator import XMLGenerator MIN_TICKS = 20 MAX_TICKS = 999999 def _hw(**flags) -> HardwareConstraints: hw = HardwareConstraints() for k, v in flags.items(): setattr(hw, k, v) return hw def _generate(make_seq, specs, tmp_path, **flags): """Run synchronizer + generator, return (parsed_channels, adc_vals, adc_starts).""" hw = _hw(**flags) sync_data = Synchronizer(hw).process(make_seq(specs)) path = tmp_path / "sync_v2.xml" adc_vals, adc_starts = XMLGenerator().generate(sync_data, str(path), hw) root = ET.parse(str(path)).getroot() channels = {} for ch in ("RF", "SW", "ADC", "GR", "CL"): node = root.find(ch) channels[ch] = [int(child.text) for child in node] channels["ParamCount"] = int(root.find("ParamCount").text) return channels, adc_vals, adc_starts # --------------------------------------------------------------------------- # # Emission contract / off-by-one regression # --------------------------------------------------------------------------- # def test_param_count_and_channel_lengths(make_seq, tmp_path): ch, _, _ = _generate( make_seq, [{"rf": True, "dur": 1e-3}, {"adc": True, "dur": 2e-3}], tmp_path, ) n = ch["ParamCount"] for name in ("RF", "SW", "ADC", "GR", "CL"): assert len(ch[name]) == n, name def test_trailing_adc_block_is_emitted(make_seq, tmp_path): """Regression: a sequence whose LAST block is ADC must still emit ADC=1.""" ch, adc_vals, _ = _generate(make_seq, [{"adc": True, "dur": 2e-3}], tmp_path) assert sum(ch["ADC"]) >= 1 # the ADC HIGH is not dropped assert len(adc_vals) == 1 # exactly one ADC event # --------------------------------------------------------------------------- # # ADC single event + exact window # --------------------------------------------------------------------------- # def test_split_adc_single_event_full_window(make_seq, tmp_path): """A 50 ms ADC block split into parts is ONE event spanning the full 50 ms.""" ch, adc_vals, _ = _generate(make_seq, [{"adc": True, "dur": 50e-3}], tmp_path) assert sum(ch["ADC"]) >= 2 # multiple HIGH columns (was split) assert len(adc_vals) == 1 # but a single ADC event / trigger assert adc_vals[0] == 50e-3 # full window, not a truncated part def test_adc_window_not_truncated_by_min_clamp(make_seq, tmp_path): """Min-clamp compensation must not steal duration from the ADC block.""" _, adc_vals, _ = _generate( make_seq, [{"rf": True, "dur": 1e-3}, {"adc": True, "dur": 2e-3}], tmp_path, TR_DELAY_ENABLED=True, ) assert len(adc_vals) == 1 assert adc_vals[0] == 2e-3 # --------------------------------------------------------------------------- # # Gradient system: last event HIGH # --------------------------------------------------------------------------- # def test_gradient_high_on_last_event(make_seq, tmp_path): ch, _, _ = _generate( make_seq, [{"rf": True, "dur": 1e-3}, {"dur": 5e-3}, {"adc": True, "dur": 2e-3}], tmp_path, ) assert ch["GR"][-1] == 1 # last event HIGH assert all(v == 0 for v in ch["GR"][1:-1]) # all middle blocks LOW # --------------------------------------------------------------------------- # # CL bounds # --------------------------------------------------------------------------- # def test_cl_values_within_bounds(make_seq, tmp_path): ch, _, _ = _generate( make_seq, [{"rf": True, "dur": 1e-3}, {"adc": True, "dur": 50e-3}], tmp_path, ) assert min(ch["CL"]) >= MIN_TICKS assert max(ch["CL"]) <= MAX_TICKS