Files
gps-denied-onboard/e2e/_unit_tests/fixtures/test_multi_segment.py
T
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

173 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()