"""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()