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