82 lines
3.3 KiB
Python
82 lines
3.3 KiB
Python
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"),))
|