"""Pure-geometry tests for :class:`gowelcome.types.RoadInfo` and :class:`gowelcome.types.Detection` derived properties. No cv2 / SDK / hardware. """ from __future__ import annotations import pytest from gowelcome.types import Detection, RoadInfo # --------------------------------------------------------------------------- # # RoadInfo.clearer_side # --------------------------------------------------------------------------- # def test_clearer_side_prefers_right_when_right_has_less_road(): """Less road on the right (right <= left) -> clearer_side +1 (right is the clear side). Note: clearer_side is a *side index* (+1 = right), not a yaw sign; the state machine negates it to obtain the CW/right turn that steers away from the road. """ info = RoadInfo(coverage=0.5, left=0.8, center=0.5, right=0.1) assert info.clearer_side == 1 def test_clearer_side_prefers_left_when_left_has_less_road(): """Less road on the left -> -1.""" info = RoadInfo(coverage=0.5, left=0.1, center=0.5, right=0.8) assert info.clearer_side == -1 def test_clearer_side_ties_to_right(): """Equal coverage is a tie; the contract resolves ties to +1 (right).""" info = RoadInfo(coverage=0.4, left=0.4, center=0.4, right=0.4) assert info.clearer_side == 1 def test_clearer_side_is_pure_without_mask(): """clearer_side works with mask=None (no cv2 involvement).""" info = RoadInfo(coverage=0.0, left=0.2, center=0.0, right=0.05, mask=None) assert info.mask is None assert info.clearer_side == 1 # --------------------------------------------------------------------------- # # Detection geometry # --------------------------------------------------------------------------- # def test_detection_centroid_and_dimensions(): """cx, cy, w, h, area are computed from the corners.""" det = Detection(label="person", conf=0.9, x1=100, y1=200, x2=300, y2=500) assert det.cx == pytest.approx(200.0) # (100 + 300) / 2 assert det.cy == pytest.approx(350.0) # (200 + 500) / 2 assert det.w == pytest.approx(200.0) assert det.h == pytest.approx(300.0) assert det.area == pytest.approx(200.0 * 300.0) def test_detection_height_ratio(): """height_ratio is box height over frame height.""" det = Detection(label="person", conf=0.9, x1=0, y1=120, x2=50, y2=360) # h = 240, frame_h = 480 -> 0.5 assert det.height_ratio(480) == pytest.approx(0.5) def test_detection_height_ratio_guards_zero_frame(): """A zero/garbage frame height does not divide-by-zero.""" det = Detection(label="person", conf=0.9, x1=0, y1=0, x2=10, y2=100) # max(1, frame_h) guard -> finite result. val = det.height_ratio(0) assert val == pytest.approx(100.0) def test_detection_horizontal_offset_centre_is_zero(): """A box centred in the frame has ~zero horizontal offset.""" frame_w = 640 det = Detection(label="person", conf=0.9, x1=300, y1=0, x2=340, y2=10) # cx=320 assert det.horizontal_offset(frame_w) == pytest.approx(0.0) def test_detection_horizontal_offset_sign_right_positive(): """A box right of centre has a positive (+) offset, left has negative (-).""" frame_w = 640 right = Detection(label="person", conf=0.9, x1=600, y1=0, x2=640, y2=10) left = Detection(label="person", conf=0.9, x1=0, y1=0, x2=40, y2=10) assert right.horizontal_offset(frame_w) > 0.0 assert left.horizontal_offset(frame_w) < 0.0 def test_detection_horizontal_offset_full_right_near_one(): """A box hard against the right edge approaches +1.""" frame_w = 640 det = Detection(label="person", conf=0.9, x1=636, y1=0, x2=640, y2=10) # cx=638 off = det.horizontal_offset(frame_w) assert 0.9 < off <= 1.0