mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 12:11:13 +00:00
6554d568f1
Add `runner/helpers/fc_proxy_runtime.py` wrapping the existing
`BlackoutSpoofProxy` (AZ-406) with a scenario-facing `drive_fc_proxy`
entry point. FDR-replay mode only: loads `schedule.json`, optionally
activates the proxy against a caller clock for alignment verification,
and writes a `proxy_drive_report.json` audit record into
`${E2E_SITL_REPLAY_DIR}` for downstream evaluators.
Replaces the local `_drive_fc_proxy` stub in FT-N-04. Adds 3
@property accessors on `BlackoutSpoofProxy` so the wrapper does not
reach into private attributes. +11 unit tests (608 total, up from
596). Live-mode router wiring remains out of scope (future ticket).
Co-authored-by: Cursor <cursoragent@cursor.com>
207 lines
5.8 KiB
Python
207 lines
5.8 KiB
Python
"""Unit tests for `e2e/runner/helpers/fc_proxy_runtime.py` (AZ-596)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from e2e.runner.helpers import fc_proxy_runtime as fpr
|
|
|
|
|
|
def _schedule_payload(
|
|
*,
|
|
window_start_ms: int = 10_000,
|
|
window_end_ms: int = 25_000,
|
|
spoof_frame_count: int = 5,
|
|
) -> dict:
|
|
"""Build a minimally valid `schedule.json` payload that `BlackoutSpoofProxy.from_schedule_file` accepts."""
|
|
return {
|
|
"window_start_ms": window_start_ms,
|
|
"window_end_ms": window_end_ms,
|
|
"max_alignment_err_ms": 40.0,
|
|
"spoof_gps": [
|
|
{
|
|
"monotonic_ms": window_start_ms + (i * 200),
|
|
"lat_deg": 50.0 + (i * 0.0001),
|
|
"lon_deg": 36.2 + (i * 0.0001),
|
|
"alt_m": 200.0,
|
|
"fix_type": 3,
|
|
"hdop": 1.0,
|
|
}
|
|
for i in range(spoof_frame_count)
|
|
],
|
|
}
|
|
|
|
|
|
def _write_schedule(path: Path, payload: dict) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(payload))
|
|
|
|
|
|
# AC-1: schedule load + error branches
|
|
|
|
|
|
def test_missing_schedule_raises_file_not_found(tmp_path: Path):
|
|
# Assert
|
|
with pytest.raises(FileNotFoundError, match="schedule.json not found"):
|
|
fpr.drive_fc_proxy(tmp_path / "nope.json")
|
|
|
|
|
|
def test_malformed_json_raises_value_error(tmp_path: Path):
|
|
# Arrange
|
|
bad = tmp_path / "schedule.json"
|
|
bad.write_text("{not valid json")
|
|
|
|
# Assert
|
|
with pytest.raises(ValueError, match="malformed schedule JSON"):
|
|
fpr.drive_fc_proxy(bad)
|
|
|
|
|
|
def test_happy_path_returns_well_formed_report(tmp_path: Path):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload(spoof_frame_count=7))
|
|
|
|
# Act
|
|
report = fpr.drive_fc_proxy(schedule)
|
|
|
|
# Assert
|
|
assert report.schedule_path == str(schedule)
|
|
assert report.window_start_ms == 10_000
|
|
assert report.window_end_ms == 25_000
|
|
assert report.spoof_frame_count == 7
|
|
assert report.alignment_err_ms == 0
|
|
assert report.was_replay_mode is True
|
|
|
|
|
|
# AC-2: now_ms_provider activation + alignment_err_ms
|
|
|
|
|
|
def test_now_ms_provider_activates_proxy_and_reports_alignment(tmp_path: Path):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload(window_start_ms=5_000))
|
|
|
|
def clock() -> int:
|
|
return 5_002 # 2 ms drift from window_start_ms
|
|
|
|
# Act
|
|
report = fpr.drive_fc_proxy(schedule, now_ms_provider=clock)
|
|
|
|
# Assert
|
|
assert report.alignment_err_ms == 0 # `activate(...)` with no first_blackout_ms anchors at `now`
|
|
assert report.was_replay_mode is False
|
|
|
|
|
|
def test_now_ms_provider_with_replay_mode_false_distinguishes_from_default(tmp_path: Path):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload())
|
|
|
|
# Act
|
|
replay_report = fpr.drive_fc_proxy(schedule)
|
|
live_report = fpr.drive_fc_proxy(schedule, now_ms_provider=lambda: 12_345)
|
|
|
|
# Assert
|
|
assert replay_report.was_replay_mode is True
|
|
assert live_report.was_replay_mode is False
|
|
|
|
|
|
# AC-3: replay_dir / E2E_SITL_REPLAY_DIR JSON write
|
|
|
|
|
|
def test_writes_report_when_replay_dir_supplied(tmp_path: Path):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload(spoof_frame_count=3))
|
|
replay_dir = tmp_path / "replay"
|
|
|
|
# Act
|
|
fpr.drive_fc_proxy(schedule, replay_dir=replay_dir)
|
|
|
|
# Assert
|
|
report_path = replay_dir / "proxy_drive_report.json"
|
|
assert report_path.is_file()
|
|
written = json.loads(report_path.read_text())
|
|
assert written["spoof_frame_count"] == 3
|
|
assert written["was_replay_mode"] is True
|
|
|
|
|
|
def test_writes_report_when_env_var_set(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload())
|
|
env_dir = tmp_path / "from-env"
|
|
monkeypatch.setenv("E2E_SITL_REPLAY_DIR", str(env_dir))
|
|
|
|
# Act
|
|
fpr.drive_fc_proxy(schedule)
|
|
|
|
# Assert
|
|
assert (env_dir / "proxy_drive_report.json").is_file()
|
|
|
|
|
|
def test_explicit_replay_dir_overrides_env_var(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload())
|
|
env_dir = tmp_path / "from-env"
|
|
explicit_dir = tmp_path / "explicit"
|
|
monkeypatch.setenv("E2E_SITL_REPLAY_DIR", str(env_dir))
|
|
|
|
# Act
|
|
fpr.drive_fc_proxy(schedule, replay_dir=explicit_dir)
|
|
|
|
# Assert
|
|
assert (explicit_dir / "proxy_drive_report.json").is_file()
|
|
assert not (env_dir / "proxy_drive_report.json").exists()
|
|
|
|
|
|
def test_no_file_written_when_neither_supplied(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload())
|
|
monkeypatch.delenv("E2E_SITL_REPLAY_DIR", raising=False)
|
|
|
|
# Act
|
|
fpr.drive_fc_proxy(schedule)
|
|
|
|
# Assert: nothing written next to the schedule (the only writable dir)
|
|
assert list(tmp_path.iterdir()) == [schedule]
|
|
|
|
|
|
def test_no_file_written_when_env_var_empty(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload())
|
|
monkeypatch.setenv("E2E_SITL_REPLAY_DIR", " ")
|
|
|
|
# Act
|
|
fpr.drive_fc_proxy(schedule)
|
|
|
|
# Assert
|
|
assert list(tmp_path.iterdir()) == [schedule]
|
|
|
|
|
|
def test_replay_dir_is_created_when_missing(tmp_path: Path):
|
|
# Arrange
|
|
schedule = tmp_path / "schedule.json"
|
|
_write_schedule(schedule, _schedule_payload())
|
|
replay_dir = tmp_path / "deep" / "nested" / "replay"
|
|
|
|
# Act
|
|
fpr.drive_fc_proxy(schedule, replay_dir=replay_dir)
|
|
|
|
# Assert
|
|
assert (replay_dir / "proxy_drive_report.json").is_file()
|