Files
Oleksandr Bezdieniezhnykh 702a0c0ff3 [AZ-408] [AZ-410] [AZ-411] Batch 69: synth injectors + FT-P-02/03/14
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>
2026-05-16 17:54:00 +03:00

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).",
)