mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 10:11:12 +00:00
702a0c0ff3
AZ-408 (3pt) — Replace AZ-406 injector scaffolds with concrete generators: - outlier.py: deterministic stride + far-away tile replacement; AC-2 ≥350m offset - blackout_spoof.py: paired video blackout + FC GPS spoof with ≤40ms alignment; AC-4 realistic fix_type/hdop; AC-NEW-8 200-500m inter-spoof deltas - multi_segment.py: ≥3 disjoint windows, ≥30s gaps, ≤25% coverage - fc_proxy.py: timed-splice runtime proxy with pre-activate RuntimeError guard - _common.py: derive_rng + tile-manifest reader + tmpfs helpers - injector_fixtures.py: pytest fixtures wired via runner conftest AZ-410 (3pt) — FT-P-02 cumulative drift between satellite anchors: - anchor_pair_detector.py: AC-1 detection, AC-2/3 pass-fraction, AC-4 monotonicity check, CSV evidence - test_ft_p_02_derkachi_drift.py: scenario gated on upstream helper NotImplementedError (frame_source_replay / fdr_reader / imu_replay) AZ-411 (2pt) — FT-P-03 + FT-P-14 schema + WGS84: - estimate_schema.py: AC-1 schema completeness, AC-2 source-label set containment, AC-3 WGS84 range + int32 1e-7 decode - test_ft_p_03_14_schema_wgs84.py: shared single-image-push scenario Tests: 248 unit tests pass (+91 vs batch 68). Reports: batch_69_report.md, batch_69_review.md (PASS), cumulative_review_batches_67-69_cycle1_report.md (PASS). Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
6.3 KiB
Python
181 lines
6.3 KiB
Python
"""pytest fixtures wrapping the AZ-408 runtime synthetic-injection injectors.
|
|
|
|
Per-scenario tests (FT-N-01, FT-N-04, FT-P-08, NFT-RES-04, NFT-PERF-04)
|
|
opt into an injector by requesting one of the fixtures below. Each
|
|
fixture:
|
|
|
|
1. Builds the injector output under the pytest ``tmp_path_factory`` root
|
|
(so unit-test runs never touch ``/tmp``).
|
|
2. Yields a typed handle the test asserts against (out_root, schedule,
|
|
summary).
|
|
3. Tears down the scratch directory at fixture exit per AC-6 (≤2 s).
|
|
|
|
The fixtures are intentionally **session-scoped per parameter set** —
|
|
within one parametrize variant the same injector tree is reused across
|
|
multiple test methods so we don't pay the ~3 s build cost per assertion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from fixtures.injectors import blackout_spoof, multi_segment, outlier
|
|
from fixtures.injectors._common import cleanup_tmpfs
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Source data discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def derkachi_source_frames() -> Path:
|
|
"""Path to the AD*.jpg frames the injectors operate on.
|
|
|
|
Looks up the project's ``_docs/00_problem/input_data/`` (the test
|
|
container mounts this read-only) and asserts the AD-stills exist.
|
|
"""
|
|
# Walk up from this file: e2e/runner/helpers/injector_fixtures.py
|
|
repo_root = Path(__file__).resolve().parents[3]
|
|
candidates = [
|
|
repo_root / "_docs/00_problem/input_data",
|
|
Path("/test-data"), # docker-compose bind-mount target
|
|
]
|
|
for c in candidates:
|
|
if (c / "AD000001.jpg").is_file():
|
|
return c
|
|
raise FileNotFoundError(
|
|
"Derkachi source frames not found in any of: "
|
|
+ ", ".join(str(c) for c in candidates)
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def tile_cache_fixture(pytestconfig: pytest.Config) -> Path:
|
|
"""Path to the AZ-407 tile-cache fixture tree.
|
|
|
|
Two strategies:
|
|
|
|
1. ``--tile-cache-fixture=<path>`` CLI flag (added by tests/fixtures
|
|
that explicitly need to point at a pre-built cache).
|
|
2. Default Docker mount at ``/tile-cache`` inside the runner image.
|
|
|
|
Skips the consuming test when the cache is missing — the injector
|
|
unit tests use a synthetic mini-cache (see ``test_outlier.py``) and
|
|
don't need this fixture.
|
|
"""
|
|
explicit = pytestconfig.getoption("--tile-cache-fixture", default=None)
|
|
if explicit is not None:
|
|
p = Path(str(explicit))
|
|
if p.is_dir():
|
|
return p
|
|
default = Path("/tile-cache")
|
|
if default.is_dir():
|
|
return default
|
|
pytest.skip("tile-cache fixture not available (build with `make fixtures`)")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Per-injector fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def outlier_injection_derkachi(
|
|
request: pytest.FixtureRequest,
|
|
derkachi_source_frames: Path,
|
|
tile_cache_fixture: Path,
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
) -> Iterator[outlier.OutlierInjectionReport]:
|
|
"""Build the outlier-injection-derkachi fixture for a single test.
|
|
|
|
Density is read from the parametrize ID (e.g.
|
|
``@pytest.mark.parametrize("density", ["medium"], indirect=True)``)
|
|
or defaults to ``"medium"``. Seed defaults to ``0`` — override via
|
|
``request.param["seed"]`` when a test needs a different stream.
|
|
"""
|
|
params = request.param if hasattr(request, "param") else {}
|
|
density = params.get("density", "medium")
|
|
seed = params.get("seed", 0)
|
|
out_root = tmp_path_factory.mktemp(f"outlier-{density}-{seed}")
|
|
report = outlier.build(
|
|
outlier.OutlierInjectionPlan(
|
|
source_frames_dir=derkachi_source_frames,
|
|
tile_cache_dir=tile_cache_fixture,
|
|
density=density,
|
|
seed=seed,
|
|
),
|
|
out_root,
|
|
)
|
|
yield report
|
|
cleanup_tmpfs(out_root)
|
|
|
|
|
|
@pytest.fixture
|
|
def blackout_spoof_derkachi(
|
|
request: pytest.FixtureRequest,
|
|
derkachi_source_frames: Path,
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
) -> Iterator[blackout_spoof.BlackoutSpoofReport]:
|
|
"""Build the blackout-spoof-derkachi fixture for a single test."""
|
|
params = request.param if hasattr(request, "param") else {}
|
|
window_seconds = params.get("window_seconds", 15.0)
|
|
seed = params.get("seed", 0)
|
|
out_root = tmp_path_factory.mktemp(f"blackout-spoof-{int(window_seconds)}s-{seed}")
|
|
report = blackout_spoof.build(
|
|
blackout_spoof.BlackoutSpoofPlan(
|
|
source_frames_dir=derkachi_source_frames,
|
|
blackout_seconds=window_seconds,
|
|
seed=seed,
|
|
),
|
|
out_root,
|
|
)
|
|
yield report
|
|
cleanup_tmpfs(out_root)
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_segment_derkachi(
|
|
request: pytest.FixtureRequest,
|
|
derkachi_source_frames: Path,
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
) -> Iterator[multi_segment.MultiSegmentReport]:
|
|
"""Build the multi-segment-derkachi fixture for a single test."""
|
|
params = request.param if hasattr(request, "param") else {}
|
|
n_segments = params.get("n_segments", 3)
|
|
segment_seconds = params.get("segment_seconds", 12.0)
|
|
out_root = tmp_path_factory.mktemp(f"multi-segment-{n_segments}x{int(segment_seconds)}s")
|
|
report = multi_segment.build(
|
|
multi_segment.MultiSegmentPlan(
|
|
source_frames_dir=derkachi_source_frames,
|
|
n_segments=n_segments,
|
|
segment_seconds=segment_seconds,
|
|
),
|
|
out_root,
|
|
)
|
|
yield report
|
|
cleanup_tmpfs(out_root)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tile-cache CLI flag registration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
"""Register the ``--tile-cache-fixture`` flag at plugin load time.
|
|
|
|
Imported by the runner's ``conftest.py`` via ``pytest_plugins`` so it
|
|
runs once per session before fixture resolution.
|
|
"""
|
|
group = parser.getgroup("e2e-runner")
|
|
group.addoption(
|
|
"--tile-cache-fixture",
|
|
action="store",
|
|
default=None,
|
|
help="Path to a pre-built tile-cache fixture tree. Default: /tile-cache (Docker mount).",
|
|
)
|