Files
pyGUI/tests/test_slot_error_boundary.py

100 lines
2.7 KiB
Python

"""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