Skip to content

Commit 0d5e1dc

Browse files
committed
Add tests for stable camera ID usage
Add unit tests to ensure services use stable camera IDs (get_camera_id) rather than GUI display IDs (get_display_id). Tests added to tests/gui/test_rec_manager.py verify RecordingManager routes frames by stable ID, doesn't accept frames keyed by display ID, and doesn't infer frame size from display-only keys. Tests added/updated in tests/services/test_multicam_controller.py assert MultiCameraController emits frames and timestamps keyed by stable IDs, exposes a display_id mapping, and no internal use of display IDs; also update an existing rotation test to reference the stable camera ID. Imported get_camera_id/get_display_id and CameraSettings where needed.
1 parent ed1a9f5 commit 0d5e1dc

2 files changed

Lines changed: 180 additions & 6 deletions

File tree

tests/gui/test_rec_manager.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import numpy as np
44
import pytest
55

6+
from dlclivegui.config import CameraSettings
67
from dlclivegui.gui.recording_manager import RecordingManager
7-
from dlclivegui.services.multi_camera_controller import get_camera_id
8+
from dlclivegui.services.multi_camera_controller import get_camera_id, get_display_id
89
from dlclivegui.services.video_recorder import RecorderStats
910

1011

@@ -277,3 +278,103 @@ def test_get_stats_summary_multi_aggregates(
277278
assert "30 frames" in summary # 10 + 20
278279
assert "dropped 4" in summary # 1 + 3
279280
assert "queue 6" in summary # 2 + 4
281+
282+
283+
@pytest.mark.unit
284+
def test_recording_manager_uses_stable_camera_id_not_display_id(
285+
recording_settings,
286+
patch_video_recorder,
287+
patch_build_run_dir,
288+
):
289+
mgr = RecordingManager()
290+
291+
cam = CameraSettings(
292+
name="GenTL cam",
293+
backend="gentl",
294+
index=0,
295+
fps=30.0,
296+
enabled=True,
297+
properties={
298+
"gentl": {
299+
"device_id": "serial:SER0",
300+
"serial_number": "SER0",
301+
}
302+
},
303+
).apply_defaults()
304+
305+
stable_id = get_camera_id(cam)
306+
display_id = get_display_id(cam)
307+
308+
assert stable_id == "gentl:serial:SER0"
309+
assert display_id == "gentl:0"
310+
assert stable_id != display_id
311+
312+
frame = np.zeros((480, 640, 3), dtype=np.uint8)
313+
current_frames = {stable_id: frame}
314+
315+
run_dir = mgr.start_all(
316+
recording_settings,
317+
[cam],
318+
current_frames,
319+
session_name="Sess",
320+
)
321+
322+
assert run_dir is not None
323+
assert stable_id in mgr.recorders
324+
assert display_id not in mgr.recorders
325+
326+
rec = mgr.recorders[stable_id]
327+
assert rec.frame_size == (480, 640)
328+
329+
mgr.write_frame(stable_id, frame, timestamp=123.0)
330+
assert len(rec.write_calls) == 1
331+
assert rec.write_calls[-1][1] == 123.0
332+
333+
# Display ID is GUI-only and must not route frames internally.
334+
mgr.write_frame(display_id, frame, timestamp=456.0)
335+
assert len(rec.write_calls) == 1
336+
337+
338+
@pytest.mark.unit
339+
def test_start_all_does_not_infer_frame_size_from_display_id(
340+
recording_settings,
341+
patch_video_recorder,
342+
patch_build_run_dir,
343+
):
344+
mgr = RecordingManager()
345+
346+
cam = CameraSettings(
347+
name="GenTL cam",
348+
backend="gentl",
349+
index=0,
350+
fps=30.0,
351+
enabled=True,
352+
properties={
353+
"gentl": {
354+
"device_id": "serial:SER0",
355+
"serial_number": "SER0",
356+
}
357+
},
358+
).apply_defaults()
359+
360+
stable_id = get_camera_id(cam)
361+
display_id = get_display_id(cam)
362+
363+
frame = np.zeros((480, 640, 3), dtype=np.uint8)
364+
365+
# Simulate the buggy situation: frames are keyed by display ID.
366+
current_frames = {display_id: frame}
367+
368+
mgr.start_all(
369+
recording_settings,
370+
[cam],
371+
current_frames,
372+
session_name="Sess",
373+
)
374+
375+
assert stable_id in mgr.recorders
376+
assert display_id not in mgr.recorders
377+
378+
# Since RecordingManager uses stable IDs internally, it should not find this frame.
379+
rec = mgr.recorders[stable_id]
380+
assert rec.frame_size is None

tests/services/test_multicam_controller.py

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@
55

66
# from dlclivegui.config import CameraSettings
77
from dlclivegui.config import CameraSettings
8-
from dlclivegui.services.multi_camera_controller import MultiCameraController, get_display_id
8+
from dlclivegui.services.multi_camera_controller import MultiCameraController, get_camera_id, get_display_id
99

1010

1111
@pytest.mark.unit
1212
def test_start_and_frames(qtbot, patch_factory):
1313
mc = MultiCameraController()
1414

15-
# One dataclass + one dict (simulate mixed inputs)
1615
cam1 = CameraSettings(name="C1", backend="opencv", index=0, fps=25.0).apply_defaults()
1716
cam2 = {"name": "C2", "backend": "opencv", "index": 1, "fps": 30.0, "enabled": True}
1817
cam2 = CameraSettings.from_dict(cam2).apply_defaults()
1918

19+
cam1_id = get_camera_id(cam1)
20+
cam2_id = get_camera_id(cam2)
21+
22+
cam1_display = get_display_id(cam1)
23+
cam2_display = get_display_id(cam2)
24+
2025
frames_seen = []
2126

2227
def on_ready(mfd):
@@ -28,11 +33,24 @@ def on_ready(mfd):
2833
with qtbot.waitSignal(mc.all_started, timeout=1500):
2934
mc.start([cam1, cam2])
3035

31-
# Wait for at least one composite emission
3236
qtbot.waitUntil(lambda: len(frames_seen) >= 1, timeout=2000)
3337

3438
assert mc.is_running()
35-
# We should have at least one entry with 1 or 2 frames (depending on timing)
39+
40+
# Internal IDs should be used as frame keys.
41+
seen_keys = set()
42+
seen_sources = set()
43+
for source_id, shape_map in frames_seen:
44+
seen_sources.add(source_id)
45+
seen_keys.update(shape_map.keys())
46+
47+
assert seen_keys <= {cam1_id, cam2_id}
48+
assert seen_sources <= {cam1_id, cam2_id}
49+
50+
# Display IDs should not be used as internal frame keys.
51+
assert cam1_display not in seen_keys
52+
assert cam2_display not in seen_keys
53+
3654
assert any(len(shape_map) >= 1 for _, shape_map in frames_seen)
3755

3856
finally:
@@ -60,7 +78,7 @@ def test_rotation_and_crop(qtbot, patch_factory):
6078
last_shape = {"shape": None}
6179

6280
def on_ready(mfd):
63-
f = mfd.frames.get(get_display_id(cam))
81+
f = mfd.frames.get(get_camera_id(cam))
6482
if f is not None:
6583
last_shape["shape"] = f.shape
6684

@@ -95,3 +113,58 @@ def _create(_settings):
95113
# Expect initialization_failed with the camera id
96114
with qtbot.waitSignals([mc.initialization_failed, mc.all_stopped], timeout=2000) as _:
97115
mc.start([cam])
116+
117+
118+
@pytest.mark.unit
119+
def test_controller_uses_stable_camera_id_not_display_id(qtbot, patch_factory):
120+
mc = MultiCameraController()
121+
122+
cam = CameraSettings(
123+
name="C1",
124+
backend="gentl",
125+
index=0,
126+
fps=30.0,
127+
enabled=True,
128+
properties={
129+
"gentl": {
130+
"device_id": "serial:SER0",
131+
"serial_number": "SER0",
132+
}
133+
},
134+
).apply_defaults()
135+
136+
stable_id = get_camera_id(cam)
137+
display_id = get_display_id(cam)
138+
139+
assert stable_id == "gentl:serial:SER0"
140+
assert display_id == "gentl:0"
141+
assert stable_id != display_id
142+
143+
seen = []
144+
145+
def on_ready(mfd):
146+
seen.append(mfd)
147+
148+
mc.frame_ready.connect(on_ready)
149+
150+
try:
151+
with qtbot.waitSignal(mc.all_started, timeout=1500):
152+
mc.start([cam])
153+
154+
qtbot.waitUntil(lambda: bool(seen), timeout=2000)
155+
156+
mfd = seen[-1]
157+
158+
assert mfd.source_camera_id == stable_id
159+
assert stable_id in mfd.frames
160+
assert stable_id in mfd.timestamps
161+
162+
assert display_id not in mfd.frames
163+
assert display_id not in mfd.timestamps
164+
165+
assert mfd.display_ids is not None
166+
assert mfd.display_ids[stable_id] == display_id
167+
168+
finally:
169+
with qtbot.waitSignal(mc.all_stopped, timeout=2000):
170+
mc.stop(wait=True)

0 commit comments

Comments
 (0)