mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 17:31:13 +00:00
[AZ-398] Replay: FrameSource + Clock Protocols + Clock injection
Ship the two Layer-1 cross-cutting Protocols replay mode needs to leave production C1-C5 components mode-agnostic (Invariant 1) and replay- deterministic (Invariant 2). Live + replay binaries see the same interfaces; only the strategy differs. * Clock Protocol (monotonic_ns / time_ns / sleep_until_ns) + WallClock (live + REALTIME replay) + TlogDerivedClock (ASAP replay; advance-on-call; non-monotonic source → ClockOrderingError). * FrameSource Protocol (next_frame -> NavCameraFrame | None / close) + LiveCameraFrameSource (cv2.VideoCapture device index) + VideoFileFrameSource (cv2.VideoCapture file). * Build-flag gating: BUILD_VIDEO_FILE_FRAME_SOURCE, BUILD_LIVE_CAMERA_FRAME_SOURCE (constructor-time check; Tier-0 OFF refuses construction with FrameSourceConfigError). * Composition-root factories: build_clock + build_frame_source. * Injected Clock across every component that previously called time.monotonic_ns() / time.sleep() directly: c5_state (estimator, ESKF, fallback watcher, source-label SM, isam2 handle), c8_fc_adapter (inbound MAVLink + MSP2, AP outbound, iNav outbound, QGC GCS), c13_fdr writer, c12_operator_tooling httpx flights client. All constructors default to WallClock() so existing call sites keep live-binary behaviour without a wiring change. * AC-4 CI guard (tests/_meta/test_no_direct_time_in_components.py) AST-scans components/**/*.py for direct time.monotonic_ns / time.time_ns / time.sleep references and fails loudly with file:line. * Conformance + factory tests: tests/unit/clock + tests/unit/frame_source. * Test fixture updates: FallbackWatcher / SourceLabelStateMachine clock_ns is now required (removed time.monotonic_ns default); test_az388 patches estimator._clock instead of a module-level time; test_az393 ardupilot adapter uses a _FixedClock test double. Excluded per the task spec: TlogReplayFcAdapter (AZ-399), ReplaySink (AZ-400), compose_replay (AZ-401), CLI (AZ-402), Docker/CI (AZ-403), E2E fixture (AZ-404), IMU auto-sync (AZ-405). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -213,7 +213,7 @@ def test_ac6_custom_threshold_5s_engages_at_5s() -> None:
|
||||
|
||||
def test_ac6_zero_threshold_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="threshold_s must be > 0"):
|
||||
FallbackWatcher(threshold_s=0.0, fdr_client=None)
|
||||
FallbackWatcher(threshold_s=0.0, fdr_client=None, clock_ns=lambda: 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@@ -378,17 +378,14 @@ def test_ac7_isam2_current_estimate_entry_engages_after_threshold() -> None:
|
||||
# and call current_estimate WITHOUT a seeded prior so it raises
|
||||
# EstimatorFatalError after the entry hook engages fallback.
|
||||
estimator._fallback._last_successful_estimate_ns = 0
|
||||
# Patch monotonic_ns inside the estimator module so the entry
|
||||
# hook sees the synthesised "now".
|
||||
# Patch the estimator's injected Clock so the entry hook sees the
|
||||
# synthesised "now" (AZ-398: components consume an injected
|
||||
# :class:`Clock`, not :func:`time.monotonic_ns`).
|
||||
from gps_denied_onboard.components.c5_state.errors import EstimatorFatalError
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"gps_denied_onboard.components.c5_state.gtsam_isam2_estimator.time.monotonic_ns",
|
||||
return_value=int(4.0 * 1e9),
|
||||
),
|
||||
pytest.raises(EstimatorFatalError),
|
||||
):
|
||||
estimator._clock = mock.MagicMock()
|
||||
estimator._clock.monotonic_ns.return_value = int(4.0 * 1e9)
|
||||
with pytest.raises(EstimatorFatalError):
|
||||
estimator.current_estimate()
|
||||
|
||||
assert len(engaged_seen) == 1
|
||||
|
||||
Reference in New Issue
Block a user