100 lines
2.7 KiB
Python
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
|