summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoofar <toofar@spalge.com>2024-02-24 21:50:38 +1300
committertoofar <toofar@spalge.com>2024-02-24 23:23:42 +1300
commitf6c0e4ebc81b1ef1afb1fb71d103a42f39d62779 (patch)
treec9f3f1bb66652d05ccd41e0823c222d3bc5ff0a1
parentaafb44cbaaabd8a1e99af27108573dbf592724fd (diff)
downloadqutebrowser-f6c0e4ebc81b1ef1afb1fb71d103a42f39d62779.tar.gz
qutebrowser-f6c0e4ebc81b1ef1afb1fb71d103a42f39d62779.zip
Add some basic tests for SignalHandler
It doesn't look like this class has any unit tests currently. And since we are adding a new signal handler to it I took the opportunity to add a few tests to it to establish a bit of a framework so that next time we touch it it will be easier to add more. I didn't go for full coverage here. It's an improvement over what was there previously and it should make merging more palatable. I don't think handling SIGHUP is a very risky feature. I chose to use mocker.patch over monkeypatch.setattr because I think it provides a more friendly and powerful API due to being more tightly integrated with magic mocks. And with `mocker.patch.object` you can even use actual object names instead of strings, the same as monkeypatch allows. One thing I'm less confident with here is mocking all multiple things in a couple of fixtures. Writing one fixture for each little thing I mock doesn't feel appealing to me right now, but for mocks that tests need to access there isn't really any idiomatic way to let the tests access them. I previously just had the one fixture but pulled out the read config one for that purpose. It's a pattern I'll have to think on a bit I think. Probably having to have developers thing about the balance of boilerplate vs accessibility is cognitive load that we want to avoid. Hmm. Anyway, here are the options I was looking at to let test code access mocks that where all shoved away into the one fixture: 1. group the tests into a class and put the mocks in class variables: could work fine but I don't think it's very idiomatic for pytest? 2. return some kind of meta object from the fixture that has the object under test as one attribute and the mocks as other ones: hmm, returning multiple values from a method always seemed a bit smelly to me 3. make one fixture for each of the mocks, have the main fixture depend on them all, tests that need to modify them can depend on them too: writing all those fixtures seems tedious and I don't think it would be a clear win in terms of readability. *sigh*, I suppose I should pull the ones I'm modifying out at least so other people don't copy my lazyness in the future 4. have the test code access the mocks from the real code, eg `configfiles.sys.exit.side_effect = ...`: this works fine but I feel like it's relying on an implementation detail and probably shouldn't be encouraged? Not sure In case anyone is struggling with git blame and wondering what the QSocketNotifier stuff is about, this commit should explain it: https://github.com/qutebrowser/qutebrowser/commit/210ce8ca7cb16e1d0ce859baa64ce56a5e1894cf
-rw-r--r--qutebrowser/misc/crashsignal.py17
-rw-r--r--tests/unit/misc/test_crashsignal.py102
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"
+ )