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

230 lines
7.9 KiB
Python

"""Behavioural tests for the AZ-408 blackout_spoof injector.
Covers:
* AC-1: ``(seed, window, offset, bearing)`` → deterministic schedule + outputs.
* AC-3: schedule's window/spoof timeline matches the documented ≤40 ms
alignment promise.
* AC-4: spoofed-GPS fields stay within realistic-flight ranges.
* AC-NEW-8: inter-spoof position deltas are in [200 m, 500 m].
* AC-6: tmpfs scratch isolation + no escapees.
The runtime alignment between video black frames and proxy spoof
emission is covered separately in ``test_fc_proxy.py`` (the proxy is
the runtime component; the injector here only emits the schedule).
"""
from __future__ import annotations
import json
import math
from pathlib import Path
import pytest
from fixtures.injectors import blackout_spoof
from fixtures.injectors._common import haversine_m
def _build_synthetic_frames_dir(parent: Path, count: int = 600) -> Path:
from PIL import Image # noqa: PLC0415
frames_dir = parent / "frames"
frames_dir.mkdir(parents=True, exist_ok=True)
img = Image.new("RGB", (256, 256), color=(40, 40, 40))
for i in range(count):
img.save(
frames_dir / f"AD{i + 1:06d}.jpg",
format="JPEG", quality=85, optimize=False, progressive=False, subsampling=2,
)
return frames_dir
def test_blackout_window_lengths(tmp_path: Path) -> None:
"""The schedule's window is exactly the requested length (modulo clamping)."""
# Arrange — 3000 frames @ 30 fps = 100 s, window anchored at 30 s leaves
# 70 s of headroom — enough for the 5/15/35 s window family the spec asks
# for plus a 25 s probe.
frames = _build_synthetic_frames_dir(tmp_path / "src", count=3000)
for window in (5.0, 15.0, 25.0, 35.0):
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=window
)
# Act
report = blackout_spoof.build(plan, tmp_path / f"out_{int(window)}")
# Assert — window duration ≈ requested (allow ±1 ms for rounding)
duration_ms = report.schedule.window_end_ms - report.schedule.window_start_ms
assert abs(duration_ms - int(window * 1000)) <= 1
def test_blackout_seconds_must_be_positive(tmp_path: Path) -> None:
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=300)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=0.0
)
# Act / Assert
with pytest.raises(ValueError, match="blackout_seconds"):
blackout_spoof.build(plan, tmp_path / "out")
def test_build_is_seed_deterministic(tmp_path: Path) -> None:
"""AC-1: identical inputs → identical schedule.json + identical black-frame bytes."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=600)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames,
blackout_seconds=10.0,
seed=99,
spoof_offset_m=400.0,
spoof_bearing_deg=30.0,
)
# Act
out_a = tmp_path / "run_a"
out_b = tmp_path / "run_b"
blackout_spoof.build(plan, out_a)
blackout_spoof.build(plan, out_b)
# Assert
sched_a = (out_a / "schedule.json").read_bytes()
sched_b = (out_b / "schedule.json").read_bytes()
assert sched_a == sched_b
def test_spoof_track_inter_position_delta_in_range(tmp_path: Path) -> None:
"""AC-NEW-8: consecutive spoofed-GPS positions jump 200-500 m apart."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=20.0, seed=11
)
# Act
report = blackout_spoof.build(plan, tmp_path / "out")
# Assert
spoof = report.schedule.spoof_gps
assert len(spoof) > 1, "need at least 2 spoofed frames to measure deltas"
for prev, nxt in zip(spoof, spoof[1:]):
d = haversine_m(prev.lat_deg, prev.lon_deg, nxt.lat_deg, nxt.lon_deg)
assert 200.0 <= d <= 500.0, (
f"inter-spoof delta {d:.1f} m outside [200, 500] m"
)
def test_spoof_fields_are_realistic(tmp_path: Path) -> None:
"""AC-4: lat/lon/alt/fix_type/hdop stay inside typical-flight ranges."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=20.0, seed=22
)
# Act
report = blackout_spoof.build(plan, tmp_path / "out")
# Assert
for f in report.schedule.spoof_gps:
assert not math.isnan(f.lat_deg)
assert -90 <= f.lat_deg <= 90
assert -180 <= f.lon_deg <= 180
assert f.fix_type in (3, 4)
assert 0.5 <= f.hdop <= 2.5
# No sentinel values (e.g. 0 lat/lon or 999 alt)
assert abs(f.lat_deg) > 1e-6
assert abs(f.lon_deg) > 1e-6
assert 50 <= f.alt_m <= 1500
def test_schedule_has_max_alignment_err_per_ac3(tmp_path: Path) -> None:
"""AC-3: schedule records the ≤40 ms alignment-error budget."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=600)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=15.0
)
# Act
report = blackout_spoof.build(plan, tmp_path / "out")
# Assert
assert report.schedule.max_alignment_err_ms == 40.0
def test_blackout_frames_are_black(tmp_path: Path) -> None:
"""Every frame index inside the blackout window has all-zero pixels."""
# Arrange
from PIL import Image # noqa: PLC0415
frames = _build_synthetic_frames_dir(tmp_path / "src", count=600)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=5.0
)
out_root = tmp_path / "out"
# Act
report = blackout_spoof.build(plan, out_root)
# Assert
for idx in report.schedule.blackout_frame_indices[:5]:
name = f"AD{idx + 1:06d}.jpg"
img = Image.open(out_root / "frames" / name).convert("RGB")
# Sample pixel — synthesised black JPEGs round-trip to (0,0,0)
# within JPEG compression noise.
r, g, b = img.getpixel((128, 128)) # type: ignore[misc]
assert r < 5 and g < 5 and b < 5, f"frame {name} pixel ({r},{g},{b}) is not black"
def test_normal_frames_pass_through(tmp_path: Path) -> None:
"""Frames OUTSIDE the blackout window are byte-equal to the source."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=600)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=5.0
)
out_root = tmp_path / "out"
blackout_spoof.build(plan, out_root)
# Act / Assert — the very first frame is always outside (window starts
# at 30 % of source).
src_bytes = (frames / "AD000001.jpg").read_bytes()
out_bytes = (out_root / "frames" / "AD000001.jpg").read_bytes()
assert src_bytes == out_bytes
def test_schedule_json_round_trips(tmp_path: Path) -> None:
"""schedule.json is well-formed JSON with the expected top-level keys."""
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=600)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=10.0
)
# Act
blackout_spoof.build(plan, tmp_path / "out")
payload = json.loads((tmp_path / "out" / "schedule.json").read_text())
# Assert
assert {"window_start_ms", "window_end_ms", "spoof_gps", "blackout_frame_indices"} <= set(
payload.keys()
)
assert isinstance(payload["spoof_gps"], list)
def test_build_overwrites_existing_out_root(tmp_path: Path) -> None:
# Arrange
frames = _build_synthetic_frames_dir(tmp_path / "src", count=300)
plan = blackout_spoof.BlackoutSpoofPlan(
source_frames_dir=frames, blackout_seconds=5.0
)
out_root = tmp_path / "out"
blackout_spoof.build(plan, out_root)
(out_root / "stale.bin").write_bytes(b"stale")
# Act
blackout_spoof.build(plan, out_root)
# Assert
assert not (out_root / "stale.bin").exists()