2026-08-11 11:02:37 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
2026-08-11 15:41:07 +08:00
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
from Brain.yovision_brain.domain import Box, Detection
|
|
|
|
|
from Brain.yovision_brain.runtime import DemoEngine, EventIngressUnavailable
|
|
|
|
|
from Brain.yovision_brain.source import FramePacket, SyntheticSource, cv2, np
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _TwoFrameSource:
|
|
|
|
|
fixture = True
|
|
|
|
|
mode = "test"
|
|
|
|
|
label = "test"
|
|
|
|
|
source_ref = "safe-source"
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.index = 0
|
|
|
|
|
|
|
|
|
|
def read(self) -> FramePacket:
|
|
|
|
|
boxes = (Box(0.10, 0.4, 0.20, 0.8), Box(0.60, 0.4, 0.70, 0.8))
|
|
|
|
|
detection = Detection("P-1", "person", boxes[min(self.index, 1)])
|
|
|
|
|
self.index += 1
|
|
|
|
|
return FramePacket(np.zeros((180, 320, 3), dtype=np.uint8), datetime.now(timezone.utc), (detection,))
|
|
|
|
|
|
|
|
|
|
def close(self) -> None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Ingress:
|
|
|
|
|
def __init__(self, fail: bool = False) -> None:
|
|
|
|
|
self.items = []
|
|
|
|
|
self.fail = fail
|
|
|
|
|
|
|
|
|
|
def submit(self, value: object) -> None:
|
|
|
|
|
if self.fail:
|
|
|
|
|
raise RuntimeError("disk unavailable")
|
|
|
|
|
self.items.append(value)
|
|
|
|
|
|
|
|
|
|
def start(self) -> None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def stop(self) -> None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def status(self) -> dict[str, object]:
|
|
|
|
|
return {"enabled": True, "queued": len(self.items)}
|
2026-08-11 11:02:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@unittest.skipIf(cv2 is None, "pinned OpenCV package is not installed")
|
|
|
|
|
class RuntimeTests(unittest.TestCase):
|
|
|
|
|
def test_rejects_non_finite_fps(self) -> None:
|
|
|
|
|
with self.assertRaisesRegex(ValueError, "fps"):
|
|
|
|
|
DemoEngine(SyntheticSource(width=320, height=180), fps=float("nan"))
|
|
|
|
|
|
|
|
|
|
def test_synthetic_step_produces_safe_state_and_jpeg(self) -> None:
|
|
|
|
|
engine = DemoEngine(SyntheticSource(width=320, height=180), fps=2.0)
|
|
|
|
|
engine.step()
|
|
|
|
|
state = engine.state()
|
|
|
|
|
self.assertTrue(state["source"]["fixture"])
|
|
|
|
|
self.assertEqual(state["detector"]["name"], "scripted_fixture")
|
|
|
|
|
self.assertEqual(state["frame"]["width"], 320)
|
|
|
|
|
self.assertGreater(len(engine.frame_jpeg() or b""), 100)
|
|
|
|
|
self.assertNotIn("url", state["source"])
|
|
|
|
|
|
|
|
|
|
def test_zone_update_increments_version(self) -> None:
|
|
|
|
|
engine = DemoEngine(SyntheticSource(width=320, height=180))
|
|
|
|
|
updated = engine.update_zone({"name": "新区域", "points": [{"x": 0.1, "y": 0.1}, {"x": 0.9, "y": 0.1}, {"x": 0.5, "y": 0.9}]})
|
|
|
|
|
self.assertEqual(updated.version, 2)
|
|
|
|
|
self.assertEqual(engine.state()["zone"]["name"], "新区域")
|
|
|
|
|
|
2026-08-11 15:41:07 +08:00
|
|
|
def test_event_is_persisted_before_it_is_exposed(self) -> None:
|
|
|
|
|
ingress = _Ingress()
|
|
|
|
|
engine = DemoEngine(_TwoFrameSource(), event_ingress=ingress)
|
|
|
|
|
engine.step()
|
|
|
|
|
engine.step()
|
|
|
|
|
state = engine.state()
|
|
|
|
|
self.assertEqual(1, len(ingress.items))
|
|
|
|
|
self.assertEqual("outbox_persisted", state["events"][0]["delivery_status"])
|
|
|
|
|
self.assertEqual(1, state["event_ingress"]["queued"])
|
|
|
|
|
|
|
|
|
|
def test_outbox_failure_does_not_claim_delivery(self) -> None:
|
|
|
|
|
engine = DemoEngine(_TwoFrameSource(), event_ingress=_Ingress(fail=True))
|
|
|
|
|
engine.step()
|
|
|
|
|
with self.assertRaises(EventIngressUnavailable):
|
|
|
|
|
engine.step()
|
|
|
|
|
self.assertEqual([], engine.state()["events"])
|
|
|
|
|
|
2026-08-11 11:02:37 +08:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|