"""AZ-275 — FakeFdrSink test double + production isolation guard.""" from __future__ import annotations import ast from pathlib import Path import pytest from gps_denied_onboard.fdr_client import ( EnqueueResult, FdrClient, FdrRecord, ) from gps_denied_onboard.fdr_client.fakes import FakeFdrSink from gps_denied_onboard.fdr_client.records import OVERRUN_KIND, OVERRUN_PRODUCER_ID _REPO_ROOT = Path(__file__).resolve().parents[2] _SRC_ROOT = _REPO_ROOT / "src" / "gps_denied_onboard" def _make_record(producer_id: str = "test.producer", frame_id: int = 0) -> FdrRecord: return FdrRecord( schema_version=1, ts="2026-05-11T00:00:00.000000Z", producer_id=producer_id, kind="log", payload={ "level": "INFO", "component": producer_id, "frame_id": frame_id, "kind": "test.tick", "msg": "hello", "kv": {}, "exc": None, }, ) # --------------------------------------------------------------------------- # AC-1: drop-in for FdrClient public surface. def test_ac1_drop_in_for_fdr_client_public_surface() -> None: # Arrange sink = FakeFdrSink(producer_id="c1_vio") record = _make_record(producer_id="c1_vio") # Act result = sink.enqueue(record) popped = sink.pop_one() # Assert assert result == EnqueueResult.OK assert popped is record assert sink.producer_id == "c1_vio" # --------------------------------------------------------------------------- # AC-2: records reflects in-buffer state in FIFO order. def test_ac2_records_reflects_in_buffer_state_fifo() -> None: # Arrange sink = FakeFdrSink(producer_id="test") records = [_make_record(frame_id=i) for i in range(3)] # Act for r in records: sink.enqueue(r) sink.pop_one() # Assert assert sink.records == records[1:] # --------------------------------------------------------------------------- # AC-3: all_records_ever captures dropped records too. def test_ac3_all_records_ever_captures_dropped() -> None: # Arrange sink = FakeFdrSink(producer_id="test", capacity=2, with_default_overrun_policy=True) a = _make_record(frame_id=0) b = _make_record(frame_id=1) c = _make_record(frame_id=2) # Act sink.enqueue(a) sink.enqueue(b) sink.enqueue(c) # Assert # Buffer carries the newest 2 (b dropped first, c retried into a's slot) # plus the synthesised overrun record at the burst end. assert any(r.kind == OVERRUN_KIND for r in sink.records) user_records = [r for r in sink.records if r.kind != OVERRUN_KIND] assert c in user_records # all_records_ever includes a (which was dropped by drop-oldest) too. assert a in sink.all_records_ever assert b in sink.all_records_ever assert c in sink.all_records_ever # --------------------------------------------------------------------------- # AC-4: overrun policy parity with real FdrClient. def test_ac4_overrun_policy_parity_with_real_client() -> None: # Arrange sink = FakeFdrSink(producer_id="c1_vio", capacity=4, with_default_overrun_policy=True) # Fill (capacity 4 holds 4 records before overrun starts). for i in range(4): sink.enqueue(_make_record(frame_id=i)) # Act sink.enqueue(_make_record(frame_id=999)) # Assert overruns = [r for r in sink.records if r.kind == OVERRUN_KIND] assert overruns, "fake must emit an overrun record when policy is wired" assert overruns[0].producer_id == OVERRUN_PRODUCER_ID assert overruns[0].payload["producer_id"] == "c1_vio" # --------------------------------------------------------------------------- # AC-5: pytest fixture available. def test_ac5_fixture_provides_clean_sink_per_test(fake_fdr_sink: FakeFdrSink) -> None: # Arrange / Act / Assert assert isinstance(fake_fdr_sink, FakeFdrSink) assert fake_fdr_sink.records == [] fake_fdr_sink.enqueue(_make_record()) assert len(fake_fdr_sink.records) == 1 # --------------------------------------------------------------------------- # AC-6: producer_id preserved on round-trip. def test_ac6_producer_id_preserved_on_roundtrip() -> None: # Arrange sink = FakeFdrSink(producer_id="c2_vpr") record = _make_record(producer_id="c2_vpr") # Act sink.enqueue(record) popped = sink.pop_one() # Assert assert popped is not None assert popped.producer_id == "c2_vpr" # --------------------------------------------------------------------------- # Empty producer_id rejected (parity with real client). def test_empty_producer_id_raises_value_error() -> None: # Arrange / Act / Assert with pytest.raises(ValueError, match="producer_id"): FakeFdrSink(producer_id="") # --------------------------------------------------------------------------- # Production isolation: no `src/gps_denied_onboard/**.py` imports the fakes. def test_production_does_not_import_fakes() -> None: # Arrange violations: list[str] = [] target_module_prefix = "gps_denied_onboard.fdr_client.fakes" # Act for path in _SRC_ROOT.rglob("*.py"): if path.name == "fakes.py": continue # the module itself try: tree = ast.parse(path.read_text(encoding="utf-8")) except SyntaxError: continue for node in ast.walk(tree): if isinstance(node, ast.ImportFrom) and node.module: if node.module.startswith(target_module_prefix): violations.append(str(path.relative_to(_REPO_ROOT))) elif isinstance(node, ast.Import): for alias in node.names: if alias.name.startswith(target_module_prefix): violations.append(str(path.relative_to(_REPO_ROOT))) # Assert assert not violations, ( "Production code must not import gps_denied_onboard.fdr_client.fakes. " f"Violations: {violations}" ) # --------------------------------------------------------------------------- # Contract parity: every public method on FdrClient also exists on FakeFdrSink. def test_contract_parity_public_methods() -> None: # Arrange public_attrs = { name for name in dir(FdrClient) if not name.startswith("_") and callable(getattr(FdrClient, name)) } public_attrs |= {"producer_id", "on_overrun"} # property pair # Act missing = [name for name in public_attrs if not hasattr(FakeFdrSink, name)] # Assert assert not missing, f"FakeFdrSink missing public surface: {missing}"