feat: implement slot_error_boundary decorator and add unit tests for session and device controllers
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Tests for slot_error_boundary decorator."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pygui.backend.utils import slot_error_boundary
|
||||
|
||||
|
||||
class _FakeSlot:
|
||||
"""Minimal QObject-like object to test the decorator against."""
|
||||
|
||||
def __init__(self, has_log_message=True):
|
||||
self.logMessage = MagicMock() if has_log_message else None
|
||||
self.__class__.__module__ = "test_module"
|
||||
|
||||
|
||||
class TestSlotErrorBoundary:
|
||||
|
||||
def test_passes_through_on_success(self):
|
||||
@slot_error_boundary
|
||||
def good(self) -> str:
|
||||
return "ok"
|
||||
|
||||
result = good(_FakeSlot())
|
||||
assert result == "ok"
|
||||
|
||||
def test_catches_and_logs_exception(self, caplog):
|
||||
@slot_error_boundary
|
||||
def bad(self) -> None:
|
||||
raise ValueError("boom")
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
bad(_FakeSlot())
|
||||
|
||||
assert "ValueError" in caplog.text
|
||||
assert "bad" in caplog.text
|
||||
|
||||
def test_emits_log_message_on_error(self):
|
||||
obj = _FakeSlot(has_log_message=True)
|
||||
|
||||
@slot_error_boundary
|
||||
def bad(self) -> None:
|
||||
raise RuntimeError("fail")
|
||||
|
||||
bad(obj)
|
||||
|
||||
obj.logMessage.emit.assert_called_once()
|
||||
emitted_msg = obj.logMessage.emit.call_args[0][0]
|
||||
assert "bad" in emitted_msg
|
||||
assert "fail" in emitted_msg
|
||||
|
||||
def test_returns_none_on_error(self):
|
||||
@slot_error_boundary
|
||||
def bad(self) -> None:
|
||||
raise KeyError("missing")
|
||||
|
||||
result = bad(_FakeSlot())
|
||||
assert result is None
|
||||
|
||||
def test_no_log_message_signal_is_ok(self):
|
||||
"""Controller without logMessage signal should not crash."""
|
||||
|
||||
class _NoLogMsg:
|
||||
__module__ = "test_module"
|
||||
|
||||
@slot_error_boundary
|
||||
def bad(self) -> None:
|
||||
raise ValueError("boom")
|
||||
|
||||
# Should not raise — hasattr check guards the emit
|
||||
result = bad(_NoLogMsg())
|
||||
assert result is None
|
||||
|
||||
def test_preserves_function_name(self):
|
||||
@slot_error_boundary
|
||||
def named_method(self) -> None:
|
||||
pass
|
||||
|
||||
assert named_method.__name__ == "named_method"
|
||||
|
||||
def test_passes_args_through(self):
|
||||
@slot_error_boundary
|
||||
def with_args(self, a: int, b: str) -> tuple[int, str]:
|
||||
return (a, b)
|
||||
|
||||
result = with_args(_FakeSlot(), 42, "hello")
|
||||
assert result == (42, "hello")
|
||||
|
||||
def test_exception_in_wrapper_does_not_leak(self):
|
||||
"""The decorator must never re-raise — QML would crash."""
|
||||
|
||||
@slot_error_boundary
|
||||
def crashing(self) -> None:
|
||||
raise TypeError("uncaught type error")
|
||||
|
||||
# Should return None, not raise
|
||||
result = crashing(_FakeSlot())
|
||||
assert result is None
|
||||
Reference in New Issue
Block a user