Files
gps-denied-onboard/_docs/02_tasks/todo/AZ-275_fake_fdr_sink.md
T
Oleksandr Bezdieniezhnykh 880eabcb3f Decompose Step 6 snapshot: 140 task specs + contract docs
Closes out greenfield Step 6 (Decompose) for all 14 components
(C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446
plus the _dependencies_table.md and component contract documents.

State file updated to greenfield Step 7 (Implement), not_started.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:39:48 +03:00

9.3 KiB

FakeFdrSink for Component-Level Tests

Task: AZ-275_fake_fdr_sink Name: FakeFdrSink Description: An in-process, in-memory test double for FdrClient that conforms to the fdr_client_protocol contract's public surface and lets component-level tests assert on every record their code emits to the FDR. Drop-in replacement for FdrClient everywhere it is injected; no writer thread, no segment files, no real ring buffer — just a list-of-records the test inspects. Complexity: 2 points Dependencies: AZ-272_fdr_record_schema, AZ-273_fdr_client_ringbuf Component: shared.fdr_client (cross-cutting; epic AZ-247 / E-CC-FDR-CLIENT) Tracker: AZ-275 Epic: AZ-247 (E-CC-FDR-CLIENT)

Document Dependencies

  • _docs/02_document/contracts/shared_fdr_client/fdr_client_protocol.md — the public surface this fake conforms to.
  • _docs/02_document/contracts/shared_fdr_client/fdr_record_schema.md — the record envelope this fake stores in memory.

Problem

Component-level tests (every component under tests/unit/components/<name>/ and tests/integration/<name>/) must assert on what their code writes to the FDR. Without a fake:

  • Tests would have to spin up the C13 writer thread + a tmp segment file just to read records back — slow, brittle, cross-component coupling.
  • Tests would all reach into FdrClient's private buffer state, freezing internal layout into every test and blocking future implementation changes.

A simple, contract-conforming FakeFdrSink lets each component's test assert on records via a stable public API — and crucially, the same API every other component test uses, so test infrastructure does not fork per component.

Outcome

  • Tests inject FakeFdrSink(producer_id="c1_vio") wherever production code expects an FdrClient. The component code is unchanged; the test reads sink.records after exercising the component.
  • Every assertion the contract test of fdr_client_protocol makes against a real FdrClient ALSO holds against FakeFdrSink — except the lock-free / allocation-free / SPSC-guard NFRs (those are real-buffer concerns and are explicitly out of scope for the fake).
  • Tests can opt in to drop-oldest semantics (FakeFdrSink(capacity=N, with_default_overrun_policy=True)) when verifying overrun behaviour, or leave it disabled and rely on unbounded list mode for general assertions.

Scope

Included

  • FakeFdrSink(producer_id: str, capacity: int | None = None, with_default_overrun_policy: bool = False) constructor implementing the FdrClient public surface from fdr_client_protocol.md:
    • enqueue, pop_one, drain, flush, producer_id, on_overrun getter/setter.
  • An FakeFdrSink.records: list[FdrRecord] property returning the records currently in-buffer in FIFO order. Tests use this directly for assertions.
  • An FakeFdrSink.all_records_ever: list[FdrRecord] property returning every record ever enqueued, INCLUDING records dropped by the overrun policy when it is active. Lets tests assert on what the producer TRIED to send vs. what the buffer KEPT.
  • Behaviour parity with FdrClient for the contract-relevant subset:
    • Returns EnqueueResult.OVERRUN when capacity is set and the buffer is full.
    • Invokes on_overrun exactly once per overrun event when wired.
    • Stamps producer_id correctly per the protocol (does NOT mutate record.producer_id).
  • A pytest fixture (fake_fdr_sink) under tests/conftest.py that constructs a default-configuration sink and yields it to tests.

Excluded

  • The lock-free SPSC ring buffer, allocation-free hot path, and SPSC guards — owned by AZ-273 (this is a fake; real concurrency primitives are explicitly NOT replicated).
  • The drop-oldest closure itself — owned by AZ-274; the fake imports and reuses it when the user opts in via with_default_overrun_policy=True.
  • The FdrRecord schema — owned by AZ-272.
  • The C13 writer thread, segment files, etc. — owned by E-C13 (AZ-248).
  • A "fake C13 writer" that drains the sink — out of scope. Tests that need the drained side use pop_one / drain directly on the fake.

Acceptance Criteria

AC-1: Drop-in for FdrClient public surface Given any production code that takes an FdrClient parameter (e.g. Vio(fdr=fdr_client, ...)) When the test passes a FakeFdrSink instead Then the production code's calls (enqueue, flush) work identically; no AttributeError, no signature mismatch

AC-2: records reflects in-buffer state Given a FakeFdrSink with no capacity limit When the producer enqueues 3 records, then the test calls pop_one() once Then sink.records returns the 2 remaining records in FIFO order

AC-3: all_records_ever captures dropped records Given a FakeFdrSink(capacity=2, with_default_overrun_policy=True) filled to capacity When the producer enqueues a 3rd record (drop-oldest fires) Then sink.records has 2 entries (newest 2) AND sink.all_records_ever has 3 entries (all of them, including the dropped one)

AC-4: Overrun policy parity with real FdrClient Given a FakeFdrSink(capacity=4, with_default_overrun_policy=True) When the test reproduces AC-1 from AZ-274 (overflow + canonical overrun record) Then the same assertion that holds against real FdrClient holds against FakeFdrSink — same overrun record shape, same coalescing across bursts

AC-5: pytest fixture available Given a test file imports the standard project conftest When the test signature is def test_x(fake_fdr_sink): ... Then pytest injects a default-configuration FakeFdrSink and yields it; teardown clears the sink

AC-6: producer_id is preserved Given FakeFdrSink(producer_id="c2_vpr") and an enqueued record carrying producer_id="c2_vpr" When the test inspects sink.records[0] Then records[0].producer_id == "c2_vpr" (the fake does NOT rewrite producer_id)

Non-Functional Requirements

Performance

  • enqueue p99 ≤ 100 µs on Tier-2 (developer machines + CI). The fake is not in the production critical path; the budget exists only to keep tests fast (10k assertions in a long fixture should add < 1 s).

Reliability

  • The fake is single-threaded only. Concurrent enqueue / pop_one is undefined behaviour and not tested. Documented in the docstring.

Compatibility

  • The fake's public surface mirrors the fdr_client_protocol.md contract version it conforms to. The fake's docstring records the contract version. Bumping the protocol contract major version requires bumping the fake's surface in lock-step.

Unit Tests

AC Ref What to Test Required Outcome
AC-1 Inject FakeFdrSink into a stub component that expects FdrClient No AttributeError; calls succeed
AC-2 3 enqueues + 1 pop on unbounded sink len(sink.records) == 2 in FIFO order
AC-3 Capacity-2 sink with overrun policy + 3 enqueues len(sink.records) == 2, len(sink.all_records_ever) == 3
AC-4 Re-run AZ-274 AC-1 + AC-2 against the fake Same overrun record shape; same coalescing
AC-5 A trivial test using fake_fdr_sink fixture Fixture provides a clean sink per test
AC-6 Construct sink + enqueue with explicit producer_id producer_id preserved on the popped record

Constraints

  • Public surface is fixed by fdr_client_protocol.md v1.0.0. The fake is allowed to expose ADDITIONAL test-only attributes (records, all_records_ever) — these are documented as fake-only and never appear on the real FdrClient (so production code accidentally using them fails the type checker).
  • The fake lives at src/gps_denied_onboard/fdr_client/fakes.py — a separate module from the production code so production imports never pick it up. Tests import from gps_denied_onboard.fdr_client.fakes import FakeFdrSink.
  • The fake reuses default_overrun_policy from AZ-274 verbatim; it does NOT re-implement the policy.

Risks & Mitigation

Risk 1: Fake drift from real client

  • Risk: Engineers add a method to FdrClient and forget to mirror it on FakeFdrSink; tests pass against the fake but production fails.
  • Mitigation: A contract test (tests/contract/fdr_client_fake_parity.py) iterates over every public method on FdrClient and asserts the same method exists on FakeFdrSink with a compatible signature. Failure mode is loud and immediate.

Risk 2: Tests reach into _records private state, freezing implementation

  • Risk: A test does sink._buffer[3] instead of sink.records[3]; later refactor breaks the test.
  • Mitigation: records and all_records_ever are the documented public access; pyright/mypy mark _buffer as private with _ prefix; code review catches private-state access.

Runtime Completeness

  • Named capability: FakeFdrSink test double — it is NOT a runtime capability; it is test infrastructure. Production code MUST NOT import from fakes.py (verified by import-linter rule in the project's pyproject.toml).
  • Production code that must exist: import-linter rule preventing src/gps_denied_onboard/**/*.py (excluding tests/) from importing gps_denied_onboard.fdr_client.fakes. Otherwise none — this PBI's deliverable is test infrastructure.
  • Allowed external stubs: this IS the stub. It is allowed in tests only.
  • Unacceptable substitutes: production code wiring FakeFdrSink instead of FdrClient (would silently disable real FDR writes); per-test ad-hoc fakes that drift from the contract.