Files
Oleksandr Bezdieniezhnykh 6554d568f1 [AZ-596] Batch 76: fc_proxy_runtime driver (FDR-replay mode)
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>
2026-05-17 09:08:48 +03:00

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()