import importlib.util import pytest from argparse import Namespace from pathlib import Path def load_local_module(name: str): path = Path(__file__).resolve().with_name(f"{name}.py") spec = importlib.util.spec_from_file_location(f"test_local_{name}", path) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module import_queue = load_local_module("import_manual_review_queue") common = load_local_module("common") build_review_classifier = load_local_module("build_review_classifier_dataset") localized_review = load_local_module("build_localized_bookmark_review_queue") select_queue = load_local_module("select_manual_review_queue") compare_queues = load_local_module("compare_manual_review_queues") rescore_queue = load_local_module("rescore_manual_review_queue") is_classifier_reject = import_queue.is_classifier_reject split_for_key = import_queue.split_for_key split_group_key = import_queue.split_group_key select_rows = select_queue.select_rows def test_raw_comma_camera_uses_real_frame_rate(): assert common.source_video_fps(Path("route/fcamera.hevc"), 25.0) == 20.0 assert common.source_video_fps(Path("clip.mp4"), 29.97) == 29.97 assert common.source_video_fps(Path("clip.mp4"), 0.0) == 20.0 def test_extended_classifier_order_matches_lexical_dataset_classes(): assert common.SUPPORTED_SPEED_VALUES == (5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 90, 100) assert common.EXTENDED_CLASSIFIER_SPEED_VALUES == (10, 100, 15, 20, 25, 30, 35, 40, 45, 5, 50, 55, 60, 65, 70, 75, 80, 90) @pytest.mark.parametrize("speed", (5, 10, 80, 90, 100)) def test_manual_import_accepts_extended_speed_values(speed): assert import_queue.parse_speed(str(speed)) == speed def test_localized_bookmark_source_position_normalizes_previous_segment(): previous = {"segment": "26", "relative_time_s": "-18.950"} current = {"segment": "26", "relative_time_s": "12.500"} explicit = {"segment": "26", "relative_time_s": "-18.950", "source_segment": "25", "source_time_s": "41.050"} assert localized_review.source_position(previous) == ("25", "41.050") assert localized_review.source_position(current) == ("26", "12.500") assert localized_review.source_position(explicit) == ("25", "41.050") def review_row(key: str, route: str, speed: int, priority: float) -> dict[str, str]: return { "record_key": key, "route": route, "detector_class": "regulatory_speed_limit", "candidate_speed_limit_mph": str(speed), "review_priority": str(priority), "proposal_confidence": "0.8", "candidate_confidence": "0.99", } def test_review_selection_balances_routes_and_speeds(): rows = [ *(review_row(f"a-{index}", "route-a", 30, 10 - index) for index in range(5)), *(review_row(f"b-{index}", "route-b", 65, 10 - index) for index in range(5)), ] args = Namespace( max_rows=4, max_per_route=2, max_per_speed=2, max_no_read=2, max_school=2, max_advisory=2, ) selected = select_rows(rows, args) assert len(selected) == 4 assert sum(row["route"] == "route-a" for row in selected) == 2 assert sum(row["route"] == "route-b" for row in selected) == 2 def test_review_selection_prioritizes_model_disagreements(): unchanged = review_row("unchanged", "route-a", 30, 5.0) changed = {**review_row("changed", "route-a", 30, 2.0), "comparison_change": "value_changed"} args = Namespace(max_rows=1, max_per_route=2, max_per_speed=2, max_no_read=2, max_school=2, max_advisory=2) assert select_rows([unchanged, changed], args)[0]["record_key"] == "changed" def test_review_selection_deduplicates_adjacent_same_speed_frames(): first = {**review_row("first", "route-a", 40, 5.0), "segment": "1", "frame_time_s": "10.0"} duplicate = {**review_row("duplicate", "route-a", 40, 4.0), "segment": "1", "frame_time_s": "11.0"} different_speed = {**review_row("different", "route-a", 45, 3.0), "segment": "1", "frame_time_s": "11.0"} args = Namespace( max_rows=3, max_per_route=3, max_per_speed=3, max_no_read=3, max_school=3, max_advisory=3, min_seconds_per_route_speed=3.0, ) selected_keys = {row["record_key"] for row in select_rows([first, duplicate, different_speed], args)} assert selected_keys == {"first", "different"} def test_lost_reads_remain_balanced_by_previous_speed(): row = { **review_row("lost", "route-a", 0, 5.0), "candidate_speed_limit_mph": "", "before_speed_limit_mph": "55", "comparison_change": "lost_read", } assert select_queue.predicted_speed(row) == 55 assert select_queue.bucket_name(row) == "speed_55" def test_primary_speed_limits_override_general_limit(): rows = [ *(review_row(f"primary-{index}", f"route-p-{index}", 40, 10 - index) for index in range(3)), *(review_row(f"low-{index}", f"route-l-{index}", 15, 10 - index) for index in range(3)), ] args = Namespace( max_rows=6, max_per_route=1, max_per_speed=1, max_primary_speed=3, max_speed_20=2, max_no_read=1, max_school=1, max_advisory=1, min_seconds_per_route_speed=0.0, ) selected = select_rows(rows, args) assert sum(select_queue.predicted_speed(row) == 40 for row in selected) == 3 assert sum(select_queue.predicted_speed(row) == 15 for row in selected) == 1 def test_manual_import_splits_adjacent_frames_by_route(): rows = [{"record_key": f"frame-{index}", "route": "dongle/route"} for index in range(8)] splits = {split_for_key(split_group_key(row), 5, 0) for row in rows} assert len(splits) == 1 def test_only_reviewed_proposal_crops_become_classifier_rejects(tmp_path): crop_path = tmp_path / "crop.jpg" crop_path.write_bytes(b"crop") row = { "review_status": "ignore", "review_sign_type": "not_speed_limit", "detector_class": "regulatory_speed_limit", "crop_path": str(crop_path), } assert is_classifier_reject(row) assert not is_classifier_reject({**row, "detector_class": "negative_empty"}) assert not is_classifier_reject({**row, "review_status": "uncertain"}) def test_advisory_positive_is_a_runtime_negative(tmp_path): crop_path = tmp_path / "crop.jpg" frame_path = tmp_path / "frame.jpg" crop_path.write_bytes(b"crop") frame_path.write_bytes(b"frame") row = { "record_key": "advisory", "review_status": "corrected", "review_sign_type": "advisory", "review_speed_limit_mph": "40", "crop_path": str(crop_path), "frame_path": str(frame_path), } assert import_queue.is_advisory_positive(row) runtime_row = import_queue.runtime_row(row, "val", "advisory_negative") assert runtime_row["sample_type"] == "advisory_negative" assert runtime_row["speed_limit_mph"] == 40 assert build_review_classifier.is_advisory(row) assert build_review_classifier.keep_advisory_reject({**row, "split": "val"}, 0.0) assert not build_review_classifier.keep_advisory_reject({**row, "split": "train"}, 0.0) def test_queue_comparison_distinguishes_gained_lost_and_changed_reads(): no_read = {"candidate_speed_limit_mph": "", "candidate_confidence": ""} speed_20 = {"candidate_speed_limit_mph": "20", "candidate_confidence": "0.99"} speed_30 = {"candidate_speed_limit_mph": "30", "candidate_confidence": "0.98"} assert compare_queues.classify_change(no_read, speed_20, 0.05) == "gained_read" assert compare_queues.classify_change(speed_20, no_read, 0.05) == "lost_read" assert compare_queues.classify_change(speed_20, speed_30, 0.05) == "value_changed" assert compare_queues.classify_change(speed_20, {**speed_20, "candidate_confidence": "0.97"}, 0.05) == "" def test_rescore_row_preserves_before_values_and_marks_gained_read(tmp_path): crop_path = tmp_path / "crop.jpg" import cv2 import numpy as np cv2.imwrite(str(crop_path), np.zeros((32, 32, 3), dtype=np.uint8)) daemon = type("Daemon", (), {"_classify_speed_limit_from_model": lambda self, crop: (20, 0.99)})() row = { "record_key": "candidate", "crop_path": str(crop_path), "candidate_speed_limit_mph": "", "candidate_confidence": "", } rescored = rescore_queue.rescore_row(row, daemon, "model", 0.05) assert rescored["before_speed_limit_mph"] == "" assert rescored["candidate_speed_limit_mph"] == "20" assert rescored["comparison_change"] == "gained_read" def test_corrected_bbox_regenerates_classifier_crop(tmp_path): import cv2 import numpy as np frame_path = tmp_path / "frame.jpg" original_crop_path = tmp_path / "original_crop.jpg" frame = np.zeros((100, 200, 3), dtype=np.uint8) frame[20:80, 60:140] = 255 cv2.imwrite(str(frame_path), frame) cv2.imwrite(str(original_crop_path), np.zeros((20, 20, 3), dtype=np.uint8)) row = { "record_key": "corrected-box", "frame_path": str(frame_path), "crop_path": str(original_crop_path), "bbox": "0,0,20,20", "crop_bbox": "0,0,24,24", "review_bbox": "60,20,140,80", } crop_path, crop_bbox, corrected = import_queue.corrected_classifier_crop(row, tmp_path, overwrite=False) crop = cv2.imread(crop_path) assert corrected assert crop is not None and crop.shape[:2] == (72, 96) assert crop.mean() > 150 assert crop_bbox == "52,14,148,86" def test_corrected_bbox_requires_readable_source_frame(tmp_path): row = { "record_key": "missing-corrected-box-frame", "frame_path": str(tmp_path / "missing-frame.jpg"), "crop_path": str(tmp_path / "original-crop.jpg"), "bbox": "0,0,20,20", "crop_bbox": "0,0,24,24", "review_bbox": "60,20,140,80", } with pytest.raises(RuntimeError, match="unreadable frame"): import_queue.corrected_classifier_crop(row, tmp_path, overwrite=False) def test_corrected_record_removes_inherited_classifier_sample(tmp_path): stale = tmp_path / "train" / "55" / "base_review_bad_record_key_hash.jpg" retained = tmp_path / "train" / "55" / "base_review_other_record_hash.jpg" stale.parent.mkdir(parents=True) stale.write_bytes(b"stale") retained.write_bytes(b"retained") removed = build_review_classifier.remove_inherited_records(tmp_path, ["bad:record/key"]) assert removed == 1 assert not stale.exists() assert retained.exists() def test_reject_repeat_spec_preserves_record_key_punctuation(): counts = build_review_classifier.parse_reject_repeat_counts(["route/sign=track:55=32"]) positive_counts = build_review_classifier.parse_record_repeat_counts(["route/sign=track:75=16"], "positive") assert counts == {"route/sign=track:55": 32} assert positive_counts == {"route/sign=track:75": 16} with pytest.raises(ValueError, match="at least 1"): build_review_classifier.parse_reject_repeat_counts(["bad-record=0"]) def test_review_crop_staging_matches_runtime_letterbox(tmp_path): import cv2 import numpy as np source = tmp_path / "portrait.jpg" image = np.full((80, 40, 3), 255, dtype=np.uint8) cv2.imwrite(str(source), image) assert build_review_classifier.stage_crop(source, tmp_path / "train" / "75", "portrait", 128) staged = cv2.imread(str(next((tmp_path / "train" / "75").iterdir()))) assert staged is not None and staged.shape == (128, 128, 3) assert staged[:, :20].mean() == pytest.approx(114, abs=2) assert staged[:, 32:96].mean() > 245 def test_conditional_reject_generates_runtime_crop_expansions(tmp_path): import cv2 import numpy as np frame_path = tmp_path / "frame.jpg" crop_path = tmp_path / "crop.jpg" frame = np.zeros((100, 200, 3), dtype=np.uint8) cv2.imwrite(str(frame_path), frame) cv2.imwrite(str(crop_path), frame[20:80, 60:140]) row = { "record_key": "conditional-sign", "frame_path": str(frame_path), "crop_path": str(crop_path), "bbox": "60,20,140,80", "review_bbox": "60,20,140,80", "review_ignore_reason": "conditional_restriction", } rows = import_queue.classifier_reject_variant_rows(row, "train", tmp_path, overwrite=False) assert len(rows) == 4 assert rows[1]["crop_bbox"] == "60,20,140,80" assert rows[2]["crop_bbox"] == "52,16,148,87" assert rows[3]["crop_bbox"] == "60,20,154,90" assert all(Path(variant["crop_path"]).is_file() for variant in rows)