mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-23 02:21:12 +00:00
[AZ-273] [AZ-274] [AZ-275] [AZ-267] [AZ-268] FDR producer chain + log bridge + contract test
AZ-273: lock-free SPSC ring buffer with pre-allocated slots, power-of- two capacity, opt-in SPSC guard, and EnqueueResult / FdrSpscViolationError on the public surface. make_fdr_client caches one client per producer_id and reads capacity from config.fdr.per_producer_capacity with fallback to queue_size. AZ-274: default_overrun_policy implements drop-oldest + retry + immediate marker emission, with prior-marker dropped_count folding via _evict_one so user-loss info is never lost across iterations. ERROR diagnostic is rate-limited to <=1/sec per producer. AZ-275: FakeFdrSink mirrors the FdrClient public surface and reuses the production default_overrun_policy via a duck-typed _PolicyAdapter. The test-only records/all_records_ever properties let component tests assert both in-buffer and lifetime state. tests/conftest.py registers the fake_fdr_sink fixture and an AST architecture lint forbids production imports of fakes. AZ-267: FdrLogBridgeHandler installs on the root logger via wire_log_bridge and forwards only WARN+ERROR records into the FDR with kind="log". Thread-local recursion guard short-circuits internal logging; saturated- queue diagnostics go to stderr every N=1000 drops. AZ-268: tests/contract/log_schema.py covers every row of the schema's Test Cases table plus the "DEBUG+INFO never reach FDR" invariant. pyproject.toml registers the contract pytest marker and the contract-mandated log_schema.py file-name. 251 unit + contract tests pass (48 new). Review verdict: PASS_WITH_WARNINGS; findings are NFR-perf deferrals + documented relaxation of AZ-274 AC-2 coalescing under permanently-stalled consumer. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""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}"
|
||||
Reference in New Issue
Block a user