Skip to content

Commit ecce224

Browse files
committed
Add GUI tests for config and camera fallback
Expand main window GUI coverage with a regression test that ensures runtime camera fallback updates only the active inference camera, not the user’s preferred inference camera. Add a new test module for user-config persistence, covering config path validation, dialog path suggestion logic, successful save side effects (last path, snapshot, sync), and failure behavior that avoids persistence and reports errors. Also add file header comments in related test files.
1 parent 6c7efcc commit ecce224

4 files changed

Lines changed: 181 additions & 0 deletions

File tree

tests/gui/main_window/test_preview.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
# tests/gui/main_window/test_preview.py
12
from __future__ import annotations
23

4+
from types import SimpleNamespace
5+
36
import numpy as np
47
import pytest
58
from PySide6.QtGui import QPixmap
69

10+
from dlclivegui.services.multi_camera_controller import get_camera_id
11+
712

813
@pytest.mark.gui
914
class TestPreviewLifecycle:
@@ -85,3 +90,35 @@ def test_on_multi_camera_started_updates_primary_buttons(self, window):
8590

8691
assert not w.preview_button.isEnabled()
8792
assert w.stop_preview_button.isEnabled()
93+
94+
def test_processing_runtime_fallback_does_not_overwrite_preferred_inference_camera(self, window):
95+
w = window
96+
97+
active_cams = w._config.multi_camera.get_active_cameras()
98+
if len(active_cams) < 2:
99+
pytest.skip("This regression test requires at least two active cameras.")
100+
101+
fallback_cam = active_cams[0]
102+
preferred_cam = active_cams[1]
103+
104+
fallback_id = get_camera_id(fallback_cam)
105+
preferred_id = get_camera_id(preferred_cam)
106+
107+
w._inference_camera_id = preferred_id
108+
w._active_inference_camera_id = preferred_id
109+
w._running_cams_ids = set()
110+
w._dlc_active = False
111+
112+
frame = np.zeros((4, 4, 3), dtype=np.uint8)
113+
frame_data = SimpleNamespace(
114+
frames={fallback_id: frame},
115+
display_ids={fallback_id: "Fallback camera"},
116+
source_camera_id=fallback_id,
117+
timestamps={fallback_id: 123.0},
118+
)
119+
120+
w._on_multi_frame_processing_ready(frame_data)
121+
122+
assert w._inference_camera_id == preferred_id
123+
assert w._active_inference_camera_id == fallback_id
124+
assert w.dlc_camera_combo.currentData() == fallback_id

tests/gui/main_window/test_recording.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# tests/gui/main_window/test_recording.py
12
from __future__ import annotations
23

34
import numpy as np

tests/gui/main_window/test_ui.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# tests/gui/main_window/test_ui.py
12
from __future__ import annotations
23

34
import pytest
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# tests/gui/main_window/test_user_config.py
2+
from __future__ import annotations
3+
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
9+
@pytest.mark.gui
10+
class TestUserConfigPersistence:
11+
def test_valid_config_file_path_accepts_existing_file(self, window, tmp_path: Path):
12+
w = window
13+
14+
config_path = tmp_path / "dlclive_config.json"
15+
config_path.write_text("{}", encoding="utf-8")
16+
17+
assert w._valid_config_file_path(str(config_path)) == config_path.resolve()
18+
19+
def test_valid_config_file_path_rejects_missing_file(self, window, tmp_path: Path):
20+
w = window
21+
22+
missing = tmp_path / "missing_config.json"
23+
24+
assert w._valid_config_file_path(str(missing)) is None
25+
assert w._valid_config_file_path(None) is None
26+
assert w._valid_config_file_path("") is None
27+
28+
def test_suggest_config_dialog_path_prefers_current_config_path(self, window, tmp_path: Path):
29+
w = window
30+
31+
config_path = tmp_path / "current_config.json"
32+
config_path.write_text("{}", encoding="utf-8")
33+
34+
w._config_path = config_path
35+
36+
assert w._suggest_config_dialog_path() == str(config_path)
37+
38+
def test_suggest_config_dialog_path_uses_last_config_path_when_current_path_missing(
39+
self,
40+
monkeypatch,
41+
window,
42+
tmp_path: Path,
43+
):
44+
w = window
45+
46+
config_path = tmp_path / "last_config.json"
47+
config_path.write_text("{}", encoding="utf-8")
48+
49+
w._config_path = None
50+
monkeypatch.setattr(w._settings_store, "get_last_config_path", lambda: str(config_path))
51+
52+
assert w._suggest_config_dialog_path() == str(config_path.resolve())
53+
54+
def test_suggest_config_dialog_path_uses_parent_of_missing_last_config(
55+
self,
56+
monkeypatch,
57+
window,
58+
tmp_path: Path,
59+
):
60+
w = window
61+
62+
missing_config_path = tmp_path / "missing_config.json"
63+
64+
w._config_path = None
65+
monkeypatch.setattr(w._settings_store, "get_last_config_path", lambda: str(missing_config_path))
66+
67+
assert w._suggest_config_dialog_path() == str(missing_config_path)
68+
69+
def test_save_config_to_path_persists_last_path_snapshot_and_syncs(
70+
self,
71+
monkeypatch,
72+
window,
73+
tmp_path: Path,
74+
):
75+
w = window
76+
77+
calls: list[tuple[str, object]] = []
78+
79+
class FakeConfig:
80+
def save(self, path: Path | str) -> None:
81+
Path(path).write_text("{}", encoding="utf-8")
82+
83+
class FakeSettings:
84+
def sync(self) -> None:
85+
calls.append(("sync", True))
86+
87+
config_path = tmp_path / "saved_config.json"
88+
fake_config = FakeConfig()
89+
90+
monkeypatch.setattr(w, "_current_config", lambda allow_empty_model_path=False: fake_config)
91+
monkeypatch.setattr(
92+
w._settings_store,
93+
"set_last_config_path",
94+
lambda path: calls.append(("last_path", path)),
95+
)
96+
monkeypatch.setattr(
97+
w._settings_store,
98+
"save_full_config_snapshot",
99+
lambda cfg: calls.append(("snapshot", cfg)),
100+
)
101+
monkeypatch.setattr(w, "settings", FakeSettings())
102+
103+
assert w._save_config_to_path(config_path) is True
104+
assert config_path.exists()
105+
assert ("last_path", str(config_path.resolve())) in calls
106+
assert ("snapshot", fake_config) in calls
107+
assert ("sync", True) in calls
108+
109+
def test_save_config_to_path_returns_false_without_persisting_after_failure(
110+
self,
111+
monkeypatch,
112+
window,
113+
tmp_path: Path,
114+
):
115+
w = window
116+
117+
calls: list[tuple[str, object]] = []
118+
errors: list[str] = []
119+
120+
class FakeConfig:
121+
def save(self, path: Path | str) -> None:
122+
raise OSError("cannot save")
123+
124+
config_path = tmp_path / "failed_config.json"
125+
126+
monkeypatch.setattr(w, "_current_config", lambda allow_empty_model_path=False: FakeConfig())
127+
monkeypatch.setattr(
128+
w._settings_store,
129+
"set_last_config_path",
130+
lambda path: calls.append(("last_path", path)),
131+
)
132+
monkeypatch.setattr(
133+
w._settings_store,
134+
"save_full_config_snapshot",
135+
lambda cfg: calls.append(("snapshot", cfg)),
136+
)
137+
monkeypatch.setattr(w, "_show_error", errors.append)
138+
139+
assert w._save_config_to_path(config_path) is False
140+
assert not config_path.exists()
141+
assert calls == []
142+
assert errors

0 commit comments

Comments
 (0)