mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 15:31:13 +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>
173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
"""Behavioural tests for the AZ-408 multi_segment injector.
|
||
|
||
Covers AC-5 (≥3 disjoint windows, ≥30 s gaps, ≤25 % total coverage) and
|
||
AC-6 (tmpfs scratch isolation).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from fixtures.injectors import multi_segment
|
||
|
||
|
||
def _build_synthetic_frames_dir(parent: Path, count: int) -> 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=(60, 60, 60))
|
||
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_produces_three_disjoint_segments(tmp_path: Path) -> None:
|
||
"""AC-5: 3 disjoint blackout windows."""
|
||
# Arrange
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000) # 5 min @ 30 fps
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=15.0
|
||
)
|
||
|
||
# Act
|
||
report = multi_segment.build(plan, tmp_path / "out")
|
||
|
||
# Assert
|
||
assert len(report.segments) == 3
|
||
# Each segment is non-empty
|
||
for s in report.segments:
|
||
assert s.end_ms > s.start_ms
|
||
# Disjoint
|
||
for prev, nxt in zip(report.segments, report.segments[1:]):
|
||
assert prev.end_ms < nxt.start_ms
|
||
|
||
|
||
def test_segments_are_at_least_30_seconds_apart(tmp_path: Path) -> None:
|
||
"""AC-5: consecutive segments separated by ≥30 s of normal frames."""
|
||
# Arrange
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=12.0
|
||
)
|
||
|
||
# Act
|
||
report = multi_segment.build(plan, tmp_path / "out")
|
||
|
||
# Assert
|
||
for prev, nxt in zip(report.segments, report.segments[1:]):
|
||
gap_ms = nxt.start_ms - prev.end_ms
|
||
assert gap_ms >= 30_000, f"gap {gap_ms} ms < 30 s between segments"
|
||
|
||
|
||
def test_total_blackout_below_25_percent(tmp_path: Path) -> None:
|
||
"""AC-5: total blackout coverage ≤ 25 %."""
|
||
# Arrange
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=15.0
|
||
)
|
||
|
||
# Act
|
||
report = multi_segment.build(plan, tmp_path / "out")
|
||
|
||
# Assert
|
||
assert report.total_blackout_fraction <= 0.25
|
||
|
||
|
||
def test_rejects_overlapping_gap(tmp_path: Path) -> None:
|
||
"""Infeasible plan: too many segments inside too short a source."""
|
||
# Arrange — 30 s source can't fit 3×12 s segments with 30 s gaps
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=12.0
|
||
)
|
||
# Act / Assert
|
||
with pytest.raises(ValueError, match="gap between segment|blackout fraction"):
|
||
multi_segment.build(plan, tmp_path / "out")
|
||
|
||
|
||
def test_rejects_too_few_segments(tmp_path: Path) -> None:
|
||
"""AC-5: n_segments must be ≥3."""
|
||
# Arrange
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=2, segment_seconds=5.0
|
||
)
|
||
# Act / Assert
|
||
with pytest.raises(ValueError, match="n_segments must be ≥3"):
|
||
multi_segment.build(plan, tmp_path / "out")
|
||
|
||
|
||
def test_rejects_zero_segment_seconds(tmp_path: Path) -> None:
|
||
# Arrange
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=900)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=0.0
|
||
)
|
||
# Act / Assert
|
||
with pytest.raises(ValueError, match="segment_seconds"):
|
||
multi_segment.build(plan, tmp_path / "out")
|
||
|
||
|
||
def test_blackout_frames_are_black(tmp_path: Path) -> None:
|
||
"""Frames inside any segment are all-zero (black) on disk."""
|
||
# Arrange
|
||
from PIL import Image # noqa: PLC0415
|
||
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=10.0
|
||
)
|
||
out_root = tmp_path / "out"
|
||
report = multi_segment.build(plan, out_root)
|
||
|
||
# Act
|
||
for seg in report.segments[:1]: # spot-check first segment
|
||
for idx in range(seg.first_frame_idx, min(seg.first_frame_idx + 5, seg.last_frame_idx)):
|
||
name = f"AD{idx + 1:06d}.jpg"
|
||
img = Image.open(out_root / "frames" / name).convert("RGB")
|
||
r, g, b = img.getpixel((128, 128)) # type: ignore[misc]
|
||
# Assert
|
||
assert r < 5 and g < 5 and b < 5
|
||
|
||
|
||
def test_summary_json_present_with_expected_fields(tmp_path: Path) -> None:
|
||
# Arrange
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=10.0
|
||
)
|
||
|
||
# Act
|
||
multi_segment.build(plan, tmp_path / "out")
|
||
payload = json.loads((tmp_path / "out" / "summary.json").read_text())
|
||
|
||
# Assert
|
||
assert payload["scenario"] == "multi-segment-derkachi"
|
||
assert payload["n_segments"] == 3
|
||
assert payload["total_blackout_fraction"] <= 0.25
|
||
|
||
|
||
def test_overwrites_existing_out_root(tmp_path: Path) -> None:
|
||
# Arrange
|
||
frames = _build_synthetic_frames_dir(tmp_path / "src", count=9000)
|
||
plan = multi_segment.MultiSegmentPlan(
|
||
source_frames_dir=frames, n_segments=3, segment_seconds=10.0
|
||
)
|
||
out_root = tmp_path / "out"
|
||
multi_segment.build(plan, out_root)
|
||
(out_root / "stale.txt").write_text("stale")
|
||
|
||
# Act
|
||
multi_segment.build(plan, out_root)
|
||
|
||
# Assert
|
||
assert not (out_root / "stale.txt").exists()
|