"""StereoYoloFrontend 出口契约测试(需求说明 §2.2/§2.3/§2.5/§5)。 全部用注入的假检测器驱动:不需要模型权重、不需要图像素材、不需要硬件; 真并行用双人屏障断言(串行实现会屏障超时)。 """ import logging import threading import numpy as np import pytest from stereo_ranger.types import Detection from stereo_yolo import StereoYoloConfig, StereoYoloFrontend class ScriptedDetector: """按调用顺序返回脚本项:list[Detection] 正常返回;Exception 抛出。""" def __init__(self, script): self._script = list(script) self.calls = 0 def detect(self, image): self.calls += 1 if not self._script: raise AssertionError("假检测器脚本已耗尽(调用次数多于预期)") item = self._script.pop(0) if isinstance(item, Exception): raise item return list(item) class BarrierDetector: """两次调用必须同时在途(屏障超时即证明串行或单线程执行)。""" def __init__(self, barrier: threading.Barrier, detections): self._barrier = barrier self._detections = list(detections) def detect(self, image): self._barrier.wait(timeout=2.0) return list(self._detections) def _img(fill=0, shape=(8, 8, 3)) -> np.ndarray: return np.full(shape, fill, dtype=np.uint8) def _det(x1=1.0, y1=2.0, x2=11.0, y2=12.0, class_id=0, confidence=0.9, track_id=None) -> Detection: return Detection(x1, y1, x2, y2, confidence=confidence, class_id=class_id, track_id=track_id) def _frontend(script, **cfg_kwargs) -> StereoYoloFrontend: kwargs = {"concurrency": "sequential"} kwargs.update(cfg_kwargs) return StereoYoloFrontend(StereoYoloConfig(**kwargs), detector=ScriptedDetector(script)) def test_frames_share_frame_id_and_keep_boxes(): left_box, right_box = _det(1.0, 2.0, 11.0, 12.0, confidence=0.77), _det(3.0, 4.0, 13.0, 14.0) frontend = _frontend([[left_box], [right_box]]) left_frame, right_frame = frontend.detect(_img(), _img(), frame_id=7) assert left_frame.frame_id == right_frame.frame_id == 7 assert left_frame.detections == (left_box,) assert right_frame.detections == (right_box,) assert left_frame.detections[0].confidence == pytest.approx(0.77) assert left_frame.detections[0].track_id is None # 无追踪器时留空(§2.1) assert frontend._detector.calls == 2 def test_whitelist_filters_by_class(): frontend = _frontend([[_det(class_id=3)], [_det(class_id=0)]], class_whitelist=(0,)) left_frame, right_frame = frontend.detect(_img(), _img(), frame_id=1) assert left_frame.detections == () assert len(right_frame.detections) == 1 and right_frame.detections[0].class_id == 0 def test_no_whitelist_keeps_all_classes(): frontend = _frontend([[_det(class_id=3)], [_det(class_id=5)]]) left_frame, right_frame = frontend.detect(_img(), _img(), frame_id=1) assert len(left_frame.detections) == 1 and len(right_frame.detections) == 1 def test_degenerate_boxes_dropped(caplog): nan_box = _det(x1=float("nan")) zero_width = _det(x1=5.0, x2=5.0) inverted = _det(x1=20.0, x2=10.0) good = _det() frontend = _frontend([[nan_box, zero_width, inverted, good], []]) with caplog.at_level(logging.WARNING, logger="stereo_yolo"): left_frame, right_frame = frontend.detect(_img(), _img(), frame_id=2) assert left_frame.detections == (good,) assert right_frame.detections == () assert "丢弃退化检测框" in caplog.text assert "累计 1 次" in caplog.text def test_boundary_touching_box_kept(): hairline = _det(x1=10.0, x2=10.0 + 1e-9) frontend = _frontend([[hairline], []]) left_frame, _ = frontend.detect(_img(), _img(), frame_id=3) assert left_frame.detections == (hairline,) def test_one_eye_failure_isolated(caplog): good = _det() frontend = _frontend([[good], RuntimeError("boom")]) with caplog.at_level(logging.WARNING, logger="stereo_yolo"): left_frame, right_frame = frontend.detect(_img(), _img(), frame_id=4) assert left_frame.detections == (good,) assert right_frame.detections == () assert left_frame.frame_id == right_frame.frame_id == 4 assert "右目推理失败" in caplog.text def test_shape_mismatch_rejected(caplog): frontend = _frontend([]) # 脚本为空:若检测器被调用会抛断言 with caplog.at_level(logging.WARNING, logger="stereo_yolo"): left_frame, right_frame = frontend.detect(_img(shape=(8, 8, 3)), _img(shape=(8, 6, 3)), frame_id=5) assert left_frame.detections == () and right_frame.detections == () assert left_frame.frame_id == right_frame.frame_id == 5 assert frontend._detector.calls == 0 assert "左右图尺寸不一致" in caplog.text def test_parallel_eyes_run_concurrently(): barrier = threading.Barrier(2) box = _det() frontend = StereoYoloFrontend( StereoYoloConfig(concurrency="parallel"), detector=BarrierDetector(barrier, [box]), ) try: left_frame, right_frame = frontend.detect(_img(), _img(), frame_id=6) finally: frontend.close() assert left_frame.detections == (box,) assert right_frame.detections == (box,) def test_close_is_idempotent(): frontend = StereoYoloFrontend(StereoYoloConfig(concurrency="parallel"), detector=ScriptedDetector([])) frontend.close() frontend.close()