diff --git a/tests/unit_tests/test_error_utils.py b/tests/unit_tests/test_error_utils.py index e8f5cce9..004e5486 100644 --- a/tests/unit_tests/test_error_utils.py +++ b/tests/unit_tests/test_error_utils.py @@ -1,10 +1,33 @@ +import sys from unittest.mock import patch import pytest import pytestqt +from bec_lib.logger import bec_logger +from qtpy.QtCore import QObject from qtpy.QtWidgets import QMessageBox -from bec_widgets.qt_utils.error_popups import ErrorPopupUtility, ExampleWidget +from bec_widgets.qt_utils.error_popups import ErrorPopupUtility, ExampleWidget, SafeProperty + + +class TestSafePropertyClass(QObject): + def __init__(self, parent=None): + super().__init__(parent) + self._my_value = 10 # internal store + + @SafeProperty(int, default=-1) + def my_value(self) -> int: + # artificially raise if it's 999 for testing + if self._my_value == 999: + raise ValueError("Invalid internal state in getter!") + return self._my_value + + @my_value.setter + def my_value(self, val: int): + # artificially raise if user sets -999 for testing + if val == -999: + raise ValueError("Invalid user input in setter!") + self._my_value = val @pytest.fixture @@ -18,46 +41,109 @@ def widget(qtbot): @patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok) def test_show_error_message_global(mock_exec, widget, qtbot): + """ + Test that an error popup is shown if global error popups are enabled + and the error_occurred signal is emitted manually. + """ error_utility = ErrorPopupUtility() error_utility.enable_global_error_popups(True) with qtbot.waitSignal(error_utility.error_occurred, timeout=1000) as blocker: error_utility.error_occurred.emit("Test Error", "This is a test error message.", widget) - assert mock_exec.called assert blocker.signal_triggered + assert mock_exec.called @pytest.mark.parametrize("global_pop", [False, True]) @patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok) def test_slot_with_popup_on_error(mock_exec, widget, qtbot, global_pop): + """ + If the slot is decorated with @SafeSlot(popup_error=True), + we always expect a popup on error (and a signal) even if global popups are off. + """ error_utility = ErrorPopupUtility() error_utility.enable_global_error_popups(global_pop) - with qtbot.waitSignal(error_utility.error_occurred, timeout=200) as blocker: + with qtbot.waitSignal(error_utility.error_occurred, timeout=500) as blocker: widget.method_with_error_handling() assert blocker.signal_triggered - assert mock_exec.called + assert mock_exec.called # Because popup_error=True forces popup @pytest.mark.parametrize("global_pop", [False, True]) +@patch.object(bec_logger.logger, "error") @patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok) -def test_slot_no_popup_by_default_on_error(mock_exec, widget, qtbot, capsys, global_pop): +def test_slot_no_popup_by_default_on_error(mock_exec, mock_log_error, widget, qtbot, global_pop): + """ + If the slot is decorated with @SafeSlot() (no popup_error=True), + we never show a popup, even if global popups are on, + because the code does not check 'enable_error_popup' for normal slots. + """ error_utility = ErrorPopupUtility() error_utility.enable_global_error_popups(global_pop) - try: - with qtbot.waitSignal(error_utility.error_occurred, timeout=200) as blocker: - widget.method_without_error_handling() - except pytestqt.exceptions.TimeoutError: - assert not global_pop + # We do NOT expect a popup or signal in either case, since code only logs + with qtbot.assertNotEmitted(error_utility.error_occurred): + widget.method_without_error_handling() - if global_pop: - assert blocker.signal_triggered - assert mock_exec.called - else: - assert not blocker.signal_triggered - assert not mock_exec.called - stdout, stderr = capsys.readouterr() - assert "ValueError" in stderr + assert not mock_exec.called + + # Confirm logger.error(...) was called + mock_log_error.assert_called_once() + logged_msg = mock_log_error.call_args[0][0] + assert "ValueError" in logged_msg + assert "SafeSlot error in slot" in logged_msg + + +@pytest.mark.parametrize("global_pop", [False, True]) +@patch.object(bec_logger.logger, "error") +@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok) +def test_safe_property_getter_error(mock_exec, mock_log_error, qtbot, global_pop): + """ + If a property getter raises an error, we log it by default. + (No popup is shown unless code specifically calls it.) + """ + error_utility = ErrorPopupUtility() + error_utility.enable_global_error_popups(global_pop) + + test_obj = TestSafePropertyClass() + test_obj._my_value = 999 # triggers ValueError in getter => logs => returns default (-1) + + val = test_obj.my_value + assert val == -1 + + # No popup => mock_exec not called + assert not mock_exec.called + + # logger.error(...) is called once + mock_log_error.assert_called_once() + logged_msg = mock_log_error.call_args[0][0] + assert "SafeProperty error in GETTER" in logged_msg + assert "ValueError" in logged_msg + + +@pytest.mark.parametrize("global_pop", [False, True]) +@patch.object(bec_logger.logger, "error") +@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok) +def test_safe_property_setter_error(mock_exec, mock_log_error, qtbot, global_pop): + """ + If a property setter raises an error, we log it by default. + (No popup is shown unless code specifically calls it.) + """ + error_utility = ErrorPopupUtility() + error_utility.enable_global_error_popups(global_pop) + + test_obj = TestSafePropertyClass() + # Setting to -999 triggers setter error => logs => property returns None + test_obj.my_value = -999 + + # No popup => mock_exec not called + assert not mock_exec.called + + # logger.error(...) is called once + mock_log_error.assert_called_once() + logged_msg = mock_log_error.call_args[0][0] + assert "SafeProperty error in SETTER" in logged_msg + assert "ValueError" in logged_msg