Skip to content

Commit 4404f78

Browse files
committed
Robust GUI test teardown; block QMessageBox
Improve camera dialog test fixture to show the dialog, yield it to tests, and perform a robust teardown: stop preview, reject/close the dialog, and wait for loader/scan worker threads and preview state to clear. Also add an autouse fixture that monkeypatches QMessageBox methods to raise on any unexpected modal dialog, preventing tests from hanging due to blocking message boxes. Small formatting/inline cleanup of MultiCameraSettings initialization included.
1 parent ff797b2 commit 4404f78

File tree

3 files changed

+67
-9
lines changed

3 files changed

+67
-9
lines changed

tests/gui/camera_config/test_cam_dialog_e2e.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,32 @@ def patch_factory(monkeypatch):
4747

4848
@pytest.fixture
4949
def dialog(qtbot, patch_factory):
50-
s = MultiCameraSettings(
51-
cameras=[
52-
CameraSettings(name="A", backend="opencv", index=0, enabled=True),
53-
]
54-
)
50+
s = MultiCameraSettings(cameras=[CameraSettings(name="A", backend="opencv", index=0, enabled=True)])
5551
d = CameraConfigDialog(None, s)
5652
qtbot.addWidget(d)
57-
return d
53+
d.show()
54+
qtbot.waitExposed(d)
5855

56+
yield d
5957

60-
# ---------------- End‑to‑End tests ----------------
58+
# --- robust teardown ---
59+
try:
60+
d._stop_preview()
61+
except Exception:
62+
pass
6163

64+
try:
65+
d.reject() # calls _stop_preview + cancels scan worker
66+
except Exception:
67+
d.close()
6268

69+
# wait for threads to stop
70+
qtbot.waitUntil(lambda: getattr(d, "_loader", None) is None, timeout=2000)
71+
qtbot.waitUntil(lambda: getattr(d, "_scan_worker", None) is None, timeout=2000)
72+
qtbot.waitUntil(lambda: not getattr(d, "_preview_active", False), timeout=2000)
73+
74+
75+
# ---------------- End‑to‑End tests ----------------
6376
def test_e2e_async_camera_scan(dialog, qtbot):
6477
qtbot.mouseClick(dialog.refresh_btn, Qt.LeftButton)
6578

tests/gui/camera_config/test_cam_dialog_unit.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
# tests/gui/camera_config/test_cam_dialog_unit.py
22
from __future__ import annotations
33

4+
import numpy as np
45
import pytest
56
from PySide6.QtCore import Qt
67

8+
from dlclivegui.cameras import CameraFactory
9+
from dlclivegui.cameras.base import CameraBackend
710
from dlclivegui.cameras.factory import DetectedCamera
811
from dlclivegui.config import CameraSettings, MultiCameraSettings
912
from dlclivegui.gui.camera_config_dialog import CameraConfigDialog
1013

1114

15+
class FakeBackend(CameraBackend):
16+
def open(self):
17+
pass
18+
19+
def close(self):
20+
pass
21+
22+
def read(self):
23+
return np.zeros((10, 10, 3), dtype=np.uint8), 0.0
24+
25+
1226
@pytest.fixture
1327
def dialog(qtbot, monkeypatch):
14-
# Patch detect_cameras to avoid hardware access
1528
monkeypatch.setattr(
1629
"dlclivegui.cameras.CameraFactory.detect_cameras",
1730
lambda backend, max_devices=10, **kw: [
1831
DetectedCamera(index=0, label=f"{backend}-X"),
1932
DetectedCamera(index=1, label=f"{backend}-Y"),
2033
],
2134
)
35+
monkeypatch.setattr(CameraFactory, "create", lambda s: FakeBackend(s))
36+
37+
# Optional: prevent probe from running at all in pure unit tests
38+
monkeypatch.setattr(CameraConfigDialog, "_start_probe_for_camera", lambda *a, **k: None)
2239

2340
s = MultiCameraSettings(
2441
cameras=[
@@ -28,7 +45,15 @@ def dialog(qtbot, monkeypatch):
2845
)
2946
d = CameraConfigDialog(None, s)
3047
qtbot.addWidget(d)
31-
return d
48+
d.show()
49+
qtbot.waitExposed(d)
50+
51+
yield d
52+
53+
try:
54+
d.reject()
55+
except Exception:
56+
d.close()
3257

3358

3459
# ---------------------- UNIT TESTS ----------------------

tests/gui/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import pytest
5+
from PySide6.QtWidgets import QMessageBox
56

67
from dlclivegui.cameras.factory import CameraFactory
78
from dlclivegui.config import CameraSettings
@@ -73,3 +74,22 @@ def _isolate_qsettings(tmp_path):
7374
s.sync()
7475

7576
yield
77+
78+
79+
@pytest.fixture(autouse=True)
80+
def no_modal_messageboxes(monkeypatch):
81+
"""
82+
Fail fast if a QMessageBox is shown unexpectedly.
83+
This prevents teardown hangs caused by modal dialogs.
84+
"""
85+
86+
def _report(*args, **kwargs):
87+
# args often: (parent, title, text, ...)
88+
title = args[1] if len(args) > 1 else "<no-title>"
89+
text = args[2] if len(args) > 2 else "<no-text>"
90+
raise AssertionError(f"Unexpected QMessageBox: {title}\n{text}")
91+
92+
monkeypatch.setattr(QMessageBox, "warning", staticmethod(_report))
93+
monkeypatch.setattr(QMessageBox, "critical", staticmethod(_report))
94+
monkeypatch.setattr(QMessageBox, "information", staticmethod(_report))
95+
monkeypatch.setattr(QMessageBox, "question", staticmethod(_report))

0 commit comments

Comments
 (0)