diff options
-rw-r--r-- | qutebrowser/misc/crashsignal.py | 17 | ||||
-rw-r--r-- | tests/unit/misc/test_crashsignal.py | 102 |
2 files changed, 111 insertions, 8 deletions
diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 277ae53bf..daffa6341 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -21,9 +21,8 @@ from qutebrowser.qt.core import (pyqtSlot, qInstallMessageHandler, QObject, QSocketNotifier, QTimer, QUrl) from qutebrowser.qt.widgets import QApplication -from qutebrowser.config import configfiles, configexc - from qutebrowser.api import cmdutils +from qutebrowser.config import configfiles, configexc from qutebrowser.misc import earlyinit, crashdialog, ipc, objects from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils, message from qutebrowser.qt import sip @@ -324,6 +323,12 @@ class SignalHandler(QObject): self._activated = False self._orig_wakeup_fd: Optional[int] = None + self._handlers = { + signal.SIGINT: self.interrupt, + signal.SIGTERM: self.interrupt, + signal.SIGHUP: self.reload_config, + } + def activate(self): """Set up signal handlers. @@ -333,12 +338,8 @@ class SignalHandler(QObject): On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get notified. """ - self._orig_handlers[signal.SIGINT] = signal.signal( - signal.SIGINT, self.interrupt) - self._orig_handlers[signal.SIGTERM] = signal.signal( - signal.SIGTERM, self.interrupt) - self._orig_handlers[signal.SIGHUP] = signal.signal( - signal.SIGHUP, self.reload_config) + for sig, handler in self._handlers.items(): + self._orig_handlers[sig] = signal.signal(sig, handler) if utils.is_posix and hasattr(signal, 'set_wakeup_fd'): # pylint: disable=import-error,no-member,useless-suppression diff --git a/tests/unit/misc/test_crashsignal.py b/tests/unit/misc/test_crashsignal.py new file mode 100644 index 000000000..e4570420e --- /dev/null +++ b/tests/unit/misc/test_crashsignal.py @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Tests for qutebrowser.misc.crashsignal.""" + +import signal + +import pytest + +from qutebrowser.config import configexc +from qutebrowser.qt.widgets import QApplication +from qutebrowser.misc import crashsignal, quitter + + +@pytest.fixture +def read_config_mock(mocker): + # covers reload_config + mocker.patch.object( + crashsignal.standarddir, + "config_py", + return_value="config.py-unittest", + ) + return mocker.patch.object( + crashsignal.configfiles, + "read_config_py", + autospec=True, + ) + + +@pytest.fixture +def signal_handler(qtbot, mocker, read_config_mock): + """Signal handler instance with all external methods mocked out.""" + # covers init + mocker.patch.object(crashsignal.sys, "exit", autospec=True) + signal_handler = crashsignal.SignalHandler( + app=mocker.Mock(spec=QApplication), + quitter=mocker.Mock(spec=quitter.Quitter), + ) + + return signal_handler + + +def test_handlers_registered(signal_handler): + signal_handler.activate() + + for sig, handler in signal_handler._handlers.items(): + registered = signal.signal(sig, signal.SIG_DFL) + assert registered == handler + + +def test_handlers_deregistered(signal_handler): + known_handler = lambda *_args: None + for sig in signal_handler._handlers: + signal.signal(sig, known_handler) + + signal_handler.activate() + signal_handler.deactivate() + + for sig in signal_handler._handlers: + registered = signal.signal(sig, signal.SIG_DFL) + assert registered == known_handler + + +def test_interrupt_repeatedly(signal_handler): + signal_handler.activate() + test_signal = signal.SIGINT + + expected_handlers = [ + signal_handler.interrupt, + signal_handler.interrupt_forcefully, + signal_handler.interrupt_really_forcefully, + ] + + # Call the SIGINT handler multiple times and make sure it calls the + # expected sequence of functions. + for expected in expected_handlers: + registered = signal.signal(test_signal, signal.SIG_DFL) + assert registered == expected + expected(test_signal, None) + + +def test_reload_config_call_on_hup(signal_handler, read_config_mock): + signal_handler._handlers[signal.SIGHUP](None, None) + + read_config_mock.assert_called_once_with("config.py-unittest") + + +def test_reload_config_displays_errors(signal_handler, read_config_mock, mocker): + read_config_mock.side_effect = configexc.ConfigFileErrors( + "config.py", + [ + configexc.ConfigErrorDesc("no config.py", ValueError("asdf")) + ] + ) + message_mock = mocker.patch.object(crashsignal.message, "error") + + signal_handler._handlers[signal.SIGHUP](None, None) + + message_mock.assert_called_once_with( + "Errors occurred while reading config.py:\n no config.py: asdf" + ) |