move_detect/tests/test_probe.py
유창욱 5444f19d45 feat(deploy): probe every camera before enabling the service
build_runtimes() logs-and-skips a camera it cannot open, so a wrong
/dev/video index or an unseated ribbon produced a green systemd unit
that silently monitored one camera less.

- edge/probe.py: opens each camera in CAMERAS, retries while the sensor
  settles, reports frame geometry, optionally saves preview PNGs, and
  exits non-zero with a backend-specific hint (v4l2-ctl / rpicam-hello)
- setup_online.sh: new step [8/9] runs it against the installed package
  and the CAMERAS line actually written to edge.env, and refuses to
  enable the unit on failure. SKIP_CAMERA_CHECK=1 opts out.

Tests: 174 pass, 2 skipped (7 new).
2026-07-20 23:22:20 +09:00

78 lines
2.5 KiB
Python

"""Camera probe (edge/probe.py) — all with fakes, no hardware."""
import pytest
from edge import probe
class FakeFrame:
def __init__(self, shape):
self.shape = shape
class FakeSource:
"""Yields `blanks` empty reads, then frames. Records that close() was called."""
def __init__(self, blanks=0, shape=(480, 640, 3)):
self._blanks = blanks
self._shape = shape
self.closed = False
def read(self):
if self._blanks > 0:
self._blanks -= 1
return False, None
return True, FakeFrame(self._shape)
def close(self):
self.closed = True
def test_probe_reports_frame_geometry():
r = probe.probe_one("uvc", "0", "cam-02", build_source=lambda b, d: FakeSource())
assert r.ok
assert "640x480" in r.detail and "3 channel" in r.detail
def test_probe_retries_while_the_sensor_settles():
# A UVC camera commonly returns nothing for the first reads; that is not a failure.
src = FakeSource(blanks=probe._READ_ATTEMPTS - 1)
r = probe.probe_one("uvc", "0", "cam-02", build_source=lambda b, d: src)
assert r.ok
def test_probe_fails_when_no_frame_ever_arrives(monkeypatch):
monkeypatch.setattr(probe, "_READ_DELAY_S", 0)
src = FakeSource(blanks=probe._READ_ATTEMPTS)
r = probe.probe_one("csi", "0", "cam-01", build_source=lambda b, d: src)
assert not r.ok
assert "no frame" in r.detail
def test_probe_fails_when_the_device_cannot_be_opened():
def boom(backend, device):
raise RuntimeError("UVC device 0 failed to open")
r = probe.probe_one("uvc", "0", "cam-02", build_source=boom)
assert not r.ok
assert "open failed" in r.detail
def test_probe_closes_the_source_even_on_failure(monkeypatch):
monkeypatch.setattr(probe, "_READ_DELAY_S", 0)
src = FakeSource(blanks=probe._READ_ATTEMPTS)
probe.probe_one("uvc", "0", "cam-02", build_source=lambda b, d: src)
assert src.closed, "a probe that holds the device open would block the daemon start"
def test_probe_all_covers_every_configured_camera():
env = {"CAMERAS": "csi:0:cam-01,uvc:0:cam-02"}
results = probe.probe_all(env, build_source=lambda b, d: FakeSource())
assert [r.camera_id for r in results] == ["cam-01", "cam-02"]
assert [r.backend for r in results] == ["csi", "uvc"]
assert all(r.ok for r in results)
def test_probe_all_rejects_a_malformed_cameras_value():
with pytest.raises(ValueError):
probe.probe_all({"CAMERAS": "csi:0"}, build_source=lambda b, d: FakeSource())