from __future__ import annotations import pytest from yovision_brain.rules import ( AreaDefinition, DirectionalLineDefinition, NormalizedPoint, RuleConfigError, RuleEngine, RuleSet, ) from yovision_brain.vision import BoundingBox, TrackedObject def point(x: float, y: float) -> NormalizedPoint: return NormalizedPoint(x, y) def rules() -> RuleSet: return RuleSet( version="rules-v7", profile_id="main", width=100, height=100, areas=(AreaDefinition("yard", (point(0.2, 0.2), point(0.8, 0.2), point(0.8, 0.8), point(0.2, 0.8))),), directional_lines=(DirectionalLineDefinition("gate", point(0.5, 0.1), point(0.5, 0.9), "left_to_right", 0.01),), ) def track(track_id: str, anchor_x: int, anchor_y: int, sequence: int = 0) -> TrackedObject: return TrackedObject(track_id, BoundingBox(anchor_x - 1, anchor_y - 2, anchor_x + 1, anchor_y), "anonymous_target", 1.0, sequence, sequence) def decisions(engine: RuleEngine, item: TrackedObject): return engine.evaluate((item,), profile_id="main", width=100, height=100) def test_area_outside_entered_inside_and_boundary() -> None: engine = RuleEngine(rules()) assert decisions(engine, track("one", 10, 50))[0].state == "outside" entered = decisions(engine, track("one", 20, 50, 1))[0] assert (entered.state, entered.triggered) == ("entered", True) inside = decisions(engine, track("one", 50, 50, 2))[0] assert (inside.state, inside.triggered) == ("inside", False) assert inside.config_version == "rules-v7" def test_direction_and_reverse_crossing_are_distinct() -> None: engine = RuleEngine(rules()) decisions(engine, track("one", 40, 50)) forward = decisions(engine, track("one", 60, 50, 1))[1] assert (forward.state, forward.triggered) == ("triggered", True) reverse_engine = RuleEngine(rules()) decisions(reverse_engine, track("two", 60, 50)) reverse = decisions(reverse_engine, track("two", 40, 50, 1))[1] assert (reverse.state, reverse.triggered) == ("reverse_crossing", False) def test_line_deadband_prevents_jitter_trigger() -> None: engine = RuleEngine(rules()) decisions(engine, track("one", 40, 50)) on_line = decisions(engine, track("one", 50, 50, 1))[1] assert (on_line.state, on_line.triggered) == ("on_line", False) triggered = decisions(engine, track("one", 60, 50, 2))[1] assert triggered.triggered is True def test_profile_resolution_mismatch_is_rejected() -> None: with pytest.raises(RuleConfigError, match="Profile/resolution"): RuleEngine(rules()).evaluate((track("one", 20, 20),), profile_id="sub", width=100, height=100) def test_invalid_polygon_line_and_duplicate_ids_are_rejected() -> None: with pytest.raises(RuleConfigError, match="non-degenerate"): RuleSet("v", "main", 10, 10, areas=(AreaDefinition("bad", (point(0, 0), point(0.5, 0.5), point(1, 1))),)) with pytest.raises(RuleConfigError, match="distinct endpoints"): RuleSet("v", "main", 10, 10, directional_lines=(DirectionalLineDefinition("bad", point(0, 0), point(0, 0), "left_to_right"),)) with pytest.raises(RuleConfigError, match="unique"): RuleSet("v", "main", 10, 10, areas=(AreaDefinition("same", (point(0, 0), point(1, 0), point(0, 1))),), directional_lines=(DirectionalLineDefinition("same", point(0, 0), point(1, 1), "left_to_right"),))