diff options
Diffstat (limited to 'tests')
24 files changed, 402 insertions, 38 deletions
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 082b999b1..b7f112182 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -759,3 +759,9 @@ def set_up_fileselector(quteproc, py_proc, tmpdir, kind, files, output_type): fileselect_cmd = json.dumps([cmd, *args]) quteproc.set_setting('fileselect.handler', 'external') quteproc.set_setting(f'fileselect.{kind}.command', fileselect_cmd) + + +@bdd.then(bdd.parsers.parse("I run {command}")) +def run_command_then(quteproc, command): + """Run a qutebrowser command.""" + quteproc.send_cmd(command) diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index c2f359f14..a1bbee870 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -712,3 +712,4 @@ Feature: Downloading things from a website. And I wait for "Asking question *" in the log And I run :prompt-fileselect-external Then the error "Can only launch external fileselect for FilenamePrompt, not LineEditPrompt" should be shown + And I run :mode-leave diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature index 9ca855d27..018d65b9f 100644 --- a/tests/end2end/features/editor.feature +++ b/tests/end2end/features/editor.feature @@ -188,6 +188,8 @@ Feature: Opening external editors And I run :cmd-edit Then the error "command must start with one of :/?" should be shown And "Leaving mode KeyMode.command *" should not be logged + And I run :mode-leave + And "Leaving mode KeyMode.command *" should be logged ## select single file diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index ddf42132f..b2a549fb5 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -279,6 +279,7 @@ Feature: Using hints When I open data/hints/iframe_scroll.html And I wait for "* simple loaded" in the log And I hint with args "all normal" and follow a + And I wait for "Clicked non-editable element!" in the log And I run :scroll bottom And I hint with args "links normal" and follow a Then "navigation request: url http://localhost:*/data/hello2.txt (current http://localhost:*/data/hints/iframe_scroll.html), type link_clicked, *" should be logged diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index 3ab5d2434..f7f354def 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -32,6 +32,7 @@ Feature: Keyboard input Scenario: :fake-key sending key to the website When I open data/keyinput/log.html + And I wait 0.01s And I run :fake-key x Then the javascript message "key press: 88" should be logged And the javascript message "key release: 88" should be logged @@ -48,12 +49,14 @@ Feature: Keyboard input Scenario: :fake-key sending special key to the website When I open data/keyinput/log.html + And I wait 0.01s And I run :fake-key <Escape> Then the javascript message "key press: 27" should be logged And the javascript message "key release: 27" should be logged Scenario: :fake-key sending keychain to the website When I open data/keyinput/log.html + And I wait 0.01s And I run :fake-key x<greater>y<less>" " Then the javascript message "key press: 88" should be logged And the javascript message "key release: 88" should be logged diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 90ce5334a..6e606a195 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -507,7 +507,7 @@ Feature: Various utility commands. Scenario: Clicking on focused element When I open data/click_element.html - And I run :fake-key <Tab> + And I run :jseval document.getElementById("qute-input").focus() And I wait for the javascript message "qute-input focused" And I run :click-element focused Then "Entering mode KeyMode.insert (reason: clicking input)" should be logged diff --git a/tests/end2end/features/prompts.feature b/tests/end2end/features/prompts.feature index 32bdd29e7..43199fa3b 100644 --- a/tests/end2end/features/prompts.feature +++ b/tests/end2end/features/prompts.feature @@ -413,7 +413,7 @@ Feature: Prompts } @qtwebengine_skip - Scenario: Cancellling webpage authentication with QtWebKit + Scenario: Cancelling webpage authentication with QtWebKit When I open basic-auth/user6/password6 without waiting And I wait for a prompt And I run :mode-leave diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 924cb520b..f8e28cc40 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -115,8 +115,8 @@ class ExpectedRequest: def is_ignored_webserver_message(line: str) -> bool: return testutils.pattern_match( pattern=( - "Client ('127.0.0.1', *) lost — peer dropped the TLS connection suddenly, " - "during handshake: (1, '[SSL: SSLV3_ALERT_CERTIFICATE_UNKNOWN] ssl/tls " + "Client ('127.0.0.1', *) lost * peer dropped the TLS connection suddenly, " + "during handshake: (1, '[SSL: SSLV3_ALERT_CERTIFICATE_UNKNOWN] * " "alert certificate unknown (_ssl.c:*)')" ), value=line, diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index a55efb129..ccf4534dd 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -905,6 +905,7 @@ def test_sandboxing( f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No", f"{yama_text} (Broker)": "Yes" if has_yama else "No", + # pylint: disable-next=used-before-assignment f"{yama_text} (Non-broker)": "Yes" if has_yama_non_broker else "No", } diff --git a/tests/unit/browser/test_pdfjs.py b/tests/unit/browser/test_pdfjs.py index cb5c26229..867e8e1de 100644 --- a/tests/unit/browser/test_pdfjs.py +++ b/tests/unit/browser/test_pdfjs.py @@ -9,7 +9,7 @@ import pytest from qutebrowser.qt.core import QUrl from qutebrowser.browser import pdfjs -from qutebrowser.utils import urlmatch +from qutebrowser.utils import urlmatch, utils pytestmark = [pytest.mark.usefixtures('data_tmpdir')] @@ -154,6 +154,8 @@ def test_read_from_system(names, expected_name, tmpdir): expected = (b'text2', str(file2)) elif expected_name is None: expected = (None, None) + else: + raise utils.Unreachable(expected_name) assert pdfjs._read_from_system(str(tmpdir), names) == expected diff --git a/tests/unit/browser/webengine/test_darkmode.py b/tests/unit/browser/webengine/test_darkmode.py index bda05feb8..2f7021e95 100644 --- a/tests/unit/browser/webengine/test_darkmode.py +++ b/tests/unit/browser/webengine/test_darkmode.py @@ -7,6 +7,7 @@ import logging from typing import List, Tuple import pytest +QWebEngineSettings = pytest.importorskip("qutebrowser.qt.webenginecore").QWebEngineSettings from qutebrowser.config import configdata from qutebrowser.utils import usertypes, version, utils @@ -28,6 +29,150 @@ def gentoo_versions(): ) +class TestSetting: + + @pytest.mark.parametrize("value, mapping, expected", [ + ("val", None, ("key", "val")), + (5, None, ("key", "5")), + (True, darkmode._BOOLS, ("key", "true")), + ("excluded", {"excluded": None}, None), + ]) + def test_chromium_tuple(self, value, mapping, expected): + setting = darkmode._Setting(option="opt", chromium_key="key", mapping=mapping) + assert setting.chromium_tuple(value) == expected + + def test_with_prefix(self): + mapping = {"val": "mapped"} + setting = darkmode._Setting(option="opt", chromium_key="key", mapping=mapping) + prefixed = setting.with_prefix("prefix") + assert prefixed == darkmode._Setting( + option="opt", chromium_key="prefixkey", mapping=mapping + ) + + +class TestDefinition: + + @pytest.fixture + def setting1(self) -> darkmode._Setting: + return darkmode._Setting("opt1", "key1") + + @pytest.fixture + def setting2(self) -> darkmode._Setting: + return darkmode._Setting("opt2", "key2") + + @pytest.fixture + def setting3(self) -> darkmode._Setting: + return darkmode._Setting("opt3", "key3") + + @pytest.fixture + def definition( + self, setting1: darkmode._Setting, setting2: darkmode._Setting + ) -> darkmode._Definition: + return darkmode._Definition(setting1, setting2, mandatory=set(), prefix="") + + def _get_settings(self, definition: darkmode._Definition) -> List[darkmode._Setting]: + return [setting for _key, setting in definition.prefixed_settings()] + + @pytest.mark.parametrize("prefix", ["", "prefix"]) + def test_prefixed_settings( + self, + prefix: str, + definition: darkmode._Definition, + setting1: darkmode._Setting, + setting2: darkmode._Setting, + ): + assert definition.prefix == "" # default value + definition.prefix = prefix + prefixed = self._get_settings(definition) + assert prefixed == [setting1.with_prefix(prefix), setting2.with_prefix(prefix)] + + def test_switch_names( + self, + definition: darkmode._Definition, + setting1: darkmode._Setting, + setting2: darkmode._Setting, + setting3: darkmode._Setting, + ): + switch_names = { + setting1.option: "opt1-switch", + None: "default-switch", + } + definition = darkmode._Definition( + setting1, + setting2, + setting3, + mandatory=set(), + prefix="", + switch_names=switch_names, + ) + settings = list(definition.prefixed_settings()) + assert settings == [ + ("opt1-switch", setting1), + ("default-switch", setting2), + ("default-switch", setting3), + ] + + def test_copy_remove_setting( + self, + definition: darkmode._Definition, + setting1: darkmode._Setting, + setting2: darkmode._Setting, + ): + copy = definition.copy_remove_setting(setting2.option) + orig_settings = self._get_settings(definition) + copy_settings = self._get_settings(copy) + assert orig_settings == [setting1, setting2] + assert copy_settings == [setting1] + + def test_copy_remove_setting_not_found(self, definition: darkmode._Definition): + with pytest.raises(ValueError, match="Setting not-found not found in "): + definition.copy_remove_setting("not-found") + + def test_copy_add_setting( + self, + definition: darkmode._Definition, + setting1: darkmode._Setting, + setting2: darkmode._Setting, + setting3: darkmode._Setting, + ): + copy = definition.copy_add_setting(setting3) + orig_settings = self._get_settings(definition) + copy_settings = self._get_settings(copy) + assert orig_settings == [setting1, setting2] + assert copy_settings == [setting1, setting2, setting3] + + def test_copy_add_setting_already_exists( + self, + definition: darkmode._Definition, + setting1: darkmode._Setting, + setting2: darkmode._Setting, + ): + copy = definition.copy_add_setting(setting2) + orig_settings = self._get_settings(definition) + copy_settings = self._get_settings(copy) + assert orig_settings == [setting1, setting2] + assert copy_settings == [setting1, setting2, setting2] + + def test_copy_replace_setting( + self, + definition: darkmode._Definition, + setting1: darkmode._Setting, + setting2: darkmode._Setting, + ): + replaced = darkmode._Setting(setting2.option, setting2.chromium_key + "-replaced") + copy = definition.copy_replace_setting(setting2.option, replaced.chromium_key) + orig_settings = self._get_settings(definition) + copy_settings = self._get_settings(copy) + assert orig_settings == [setting1, setting2] + assert copy_settings == [setting1, replaced] + + def test_copy_replace_setting_not_found( + self, definition: darkmode._Definition, setting3: darkmode._Setting + ): + with pytest.raises(ValueError, match="Setting opt3 not found in "): + definition.copy_replace_setting(setting3.option, setting3.chromium_key) + + @pytest.mark.parametrize('value, webengine_version, expected', [ # Auto ("auto", "5.15.2", [("preferredColorScheme", "2")]), # QTBUG-89753 @@ -122,16 +267,49 @@ QT_64_SETTINGS = { 'dark-mode-settings': [ ('InversionAlgorithm', '1'), ('ImagePolicy', '2'), + ('ForegroundBrightnessThreshold', '100'), # name changed + ], +} + + +QT_66_SETTINGS = { + 'blink-settings': [('forceDarkModeEnabled', 'true')], + 'dark-mode-settings': [ + ('InversionAlgorithm', '1'), + ('ImagePolicy', '2'), ('ForegroundBrightnessThreshold', '100'), + ("ImageClassifierPolicy", "0"), # added ], } +QT_67_SETTINGS = { + # blink-settings removed + 'dark-mode-settings': [ + ('InversionAlgorithm', '1'), + ('ImagePolicy', '2'), + ('ForegroundBrightnessThreshold', '100'), + ("ImageClassifierPolicy", "0"), + ], +} -@pytest.mark.parametrize('qversion, expected', [ - ('5.15.2', QT_515_2_SETTINGS), - ('5.15.3', QT_515_3_SETTINGS), - ('6.4', QT_64_SETTINGS), -]) + +@pytest.mark.parametrize( + "qversion, expected", + [ + ("5.15.2", QT_515_2_SETTINGS), + ("5.15.3", QT_515_3_SETTINGS), + ("6.4", QT_64_SETTINGS), + ("6.6", QT_66_SETTINGS), + pytest.param( + "6.7", + QT_67_SETTINGS, + marks=pytest.mark.skipif( + not hasattr(QWebEngineSettings.WebAttribute, "ForceDarkMode"), + reason="needs PyQt 6.7", + ), + ), + ], +) def test_qt_version_differences(config_stub, qversion, expected): settings = { 'enabled': True, @@ -213,6 +391,16 @@ def test_variant(webengine_version, expected): assert darkmode._variant(versions) == expected +def test_variant_qt67() -> None: + versions = version.WebEngineVersions.from_pyqt("6.7.0") + # We can't monkeypatch the enum, so compare against the real situation + if hasattr(QWebEngineSettings.WebAttribute, "ForceDarkMode"): + expected = darkmode.Variant.qt_67 + else: + expected = darkmode.Variant.qt_66 + assert darkmode._variant(versions) == expected + + def test_variant_gentoo_workaround(gentoo_versions): assert darkmode._variant(gentoo_versions) == darkmode.Variant.qt_515_3 @@ -257,8 +445,9 @@ def test_options(configdata_init): if not name.startswith('colors.webpage.darkmode.'): continue - assert not opt.supports_pattern, name - assert opt.restart, name + if name != 'colors.webpage.darkmode.enabled': + assert not opt.supports_pattern, name + assert opt.restart, name if opt.backends: # On older Qt versions, this is an empty list. diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index add81c04d..5a012c634 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -55,7 +55,7 @@ def completion_widget_stub(): def completer_obj(qtbot, status_command_stub, config_stub, monkeypatch, stubs, completion_widget_stub): """Create the completer used for testing.""" - monkeypatch.setattr(completer, 'QTimer', stubs.InstaTimer) + monkeypatch.setattr(completer.usertypes, 'Timer', stubs.InstaTimer) config_stub.val.completion.show = 'auto' return completer.Completer(cmd=status_command_stub, win_id=0, parent=completion_widget_stub) diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 4ab9dfca0..387339f4f 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -7,7 +7,7 @@ from unittest import mock import pytest -from qutebrowser.qt.core import QRect +from qutebrowser.qt.core import QRect, QItemSelectionModel from qutebrowser.completion import completionwidget from qutebrowser.completion.models import completionmodel, listcategory @@ -285,6 +285,23 @@ def test_completion_show(show, rows, quick_complete, completionview, model, assert not completionview.isVisible() +def test_completion_selection_clear_no_model(completionview): + completionview.show() + completionview.on_clear_completion_selection() + assert completionview.isVisible() is False + + +def test_completion_selection_clear_with_model(completionview, mocker): + selmod = mock.Mock(spec=QItemSelectionModel) + mocker.patch.object(completionview, "selectionModel", return_value=selmod) + completionview.show() + completionview.on_clear_completion_selection() + + assert completionview.isVisible() is False + selmod.clearSelection.assert_called_once() + selmod.clearCurrentIndex.assert_called_once() + + def test_completion_item_del(completionview, model): """Test that completion_item_del invokes delete_cur_item in the model.""" func = mock.Mock(spec=[]) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index e0bce8f04..ad73d9eb5 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -29,7 +29,7 @@ from qutebrowser.completion import completer from qutebrowser.completion.models import ( configmodel, listcategory, miscmodels, urlmodel, filepathcategory) from qutebrowser.config import configdata, configtypes -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, utils from qutebrowser.mainwindow import tabbedbrowser @@ -417,6 +417,8 @@ def test_filesystem_completion(qtmodeltester, config_stub, info, base = '~' expected_1 = str(pathlib.Path('~') / 'file1.txt') expected_2 = str(pathlib.Path('~') / 'file2.txt') + else: + raise utils.Unreachable(method) config_stub.val.completion.open_categories = ['filesystem'] model = urlmodel.url(info=info) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 10d9e2292..f9dca3e74 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -840,6 +840,8 @@ class TestBind: func = functools.partial(commands.bind, 0) elif command == 'unbind': func = commands.unbind + else: + raise utils.Unreachable(command) with pytest.raises(cmdutils.CommandError, match=expected): func(*args, **kwargs) diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index cde0a180b..7a47b6cb0 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -399,6 +399,7 @@ class TestYaml: yaml._save() if not insert and old_config is None: + data = {} # unused lines = [] else: data = autoconfig.read() diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index bcd257ed7..09fc4fa75 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1602,6 +1602,8 @@ class TestDict: valtype=configtypes.String(), required_keys=['one', 'two']) message = 'Required keys .*' + else: + raise utils.Unreachable(kind) if ok: expectation = testutils.nop_contextmanager() diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index 4ba11ed58..6a83d614b 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -115,10 +115,12 @@ class TestHintKeyParser: seq = keyutils.KeySequence.parse(keychain) assert len(seq) == 2 + # pylint: disable-next=no-member match = keyparser.handle(seq[0].to_event()) assert match == QKeySequence.SequenceMatch.PartialMatch assert hintmanager.keystr == prefix + # pylint: disable-next=no-member match = keyparser.handle(seq[1].to_event()) assert match == QKeySequence.SequenceMatch.ExactMatch assert hintmanager.keystr == hint @@ -132,10 +134,12 @@ class TestHintKeyParser: seq = keyutils.KeySequence.parse('ασ') assert len(seq) == 2 + # pylint: disable-next=no-member match = keyparser.handle(seq[0].to_event()) assert match == QKeySequence.SequenceMatch.PartialMatch assert hintmanager.keystr == 'a' + # pylint: disable-next=no-member match = keyparser.handle(seq[1].to_event()) assert match == QKeySequence.SequenceMatch.ExactMatch assert hintmanager.keystr == 'as' diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index d1bc6e7c1..7c4ff1a5d 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -9,7 +9,7 @@ import logging import signal import pytest -from qutebrowser.qt.core import QProcess, QUrl +from qutebrowser.qt.core import QProcess, QUrl, Qt from qutebrowser.misc import guiprocess from qutebrowser.utils import usertypes, utils, version @@ -534,6 +534,7 @@ def test_str(proc, py_proc): def test_cleanup(proc, py_proc, qtbot): + proc._cleanup_timer.setTimerType(Qt.TimerType.CoarseTimer) proc._cleanup_timer.setInterval(100) with qtbot.wait_signal(proc._cleanup_timer.timeout): diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 2dcfbd5b1..59185a380 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -177,7 +177,7 @@ class TestFindWebengineResources: def test_nowhere(self, fallback_path: pathlib.Path): """Test we raise if we can't find the resources.""" with pytest.raises( - binparsing.ParseError, match="Couldn't find webengine resources dir" + FileNotFoundError, match="Couldn't find webengine resources dir, candidates:\n*" ): pakjoy._find_webengine_resources() diff --git a/tests/unit/test_qt_machinery.py b/tests/unit/test_qt_machinery.py index 25fc83ffd..cf7990393 100644 --- a/tests/unit/test_qt_machinery.py +++ b/tests/unit/test_qt_machinery.py @@ -9,7 +9,7 @@ import sys import html import argparse import typing -from typing import Any, Optional, List, Dict, Union +from typing import Any, Optional, List, Dict, Union, Type import dataclasses import pytest @@ -45,14 +45,14 @@ def undo_init(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.mark.parametrize( - "exception", + "exception, base", [ - machinery.Unavailable(), - machinery.NoWrapperAvailableError(machinery.SelectionInfo()), + (machinery.Unavailable(), ModuleNotFoundError), + (machinery.NoWrapperAvailableError(machinery.SelectionInfo()), ImportError), ], ) -def test_importerror_exceptions(exception: Exception): - with pytest.raises(ImportError): +def test_importerror_exceptions(exception: Exception, base: Type[Exception]): + with pytest.raises(base): raise exception diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index c7af3162c..0a3afa416 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -758,8 +758,10 @@ class TestPyQIODevice: # pylint: enable=no-member,useless-suppression else: pytest.skip("Needs os.SEEK_HOLE or os.SEEK_DATA available.") + pyqiodev.open(QIODevice.OpenModeFlag.ReadOnly) with pytest.raises(io.UnsupportedOperation): + # pylint: disable=possibly-used-before-assignment pyqiodev.seek(0, whence) @pytest.mark.flaky @@ -1116,13 +1118,13 @@ class TestQObjRepr: assert qtutils.qobj_repr(obj) == expected def test_class_name(self): - obj = QTimer() + obj = QTimer() # misc: ignore hidden = sip.cast(obj, QObject) expected = f"<{self._py_repr(hidden)}, className='QTimer'>" assert qtutils.qobj_repr(hidden) == expected def test_both(self): - obj = QTimer() + obj = QTimer() # misc: ignore obj.setObjectName("Pomodoro") hidden = sip.cast(obj, QObject) expected = f"<{self._py_repr(hidden)}, objectName='Pomodoro', className='QTimer'>" diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 38134b40e..5d2863100 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -899,21 +899,45 @@ class TestWebEngineVersions: webengine=utils.VersionNumber(5, 15, 2), chromium=None, source='UA'), - "QtWebEngine 5.15.2", + ( + "QtWebEngine 5.15.2\n" + " (source: UA)" + ), ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='UA'), - "QtWebEngine 5.15.2, based on Chromium 87.0.4280.144", + ( + "QtWebEngine 5.15.2\n" + " based on Chromium 87.0.4280.144\n" + " (source: UA)" + ), ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='faked'), - "QtWebEngine 5.15.2, based on Chromium 87.0.4280.144 (from faked)", + ( + "QtWebEngine 5.15.2\n" + " based on Chromium 87.0.4280.144\n" + " (source: faked)" + ), + ), + ( + version.WebEngineVersions( + webengine=utils.VersionNumber(5, 15, 2), + chromium='87.0.4280.144', + chromium_security='9000.1', + source='faked'), + ( + "QtWebEngine 5.15.2\n" + " based on Chromium 87.0.4280.144\n" + " with security patches up to 9000.1 (plus any distribution patches)\n" + " (source: faked)" + ), ), ]) def test_str(self, version, expected): @@ -950,6 +974,7 @@ class TestWebEngineVersions: expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', + chromium_security='86.0.4240.183', source='UA', ) assert version.WebEngineVersions.from_ua(ua) == expected @@ -959,21 +984,27 @@ class TestWebEngineVersions: expected = version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='83.0.4103.122', + chromium_security='86.0.4240.183', source='ELF', ) assert version.WebEngineVersions.from_elf(elf_version) == expected - @pytest.mark.parametrize('pyqt_version, chromium_version', [ - ('5.15.2', '83.0.4103.122'), - ('5.15.3', '87.0.4280.144'), - ('5.15.4', '87.0.4280.144'), - ('5.15.5', '87.0.4280.144'), - ('6.2.0', '90.0.4430.228'), - ('6.3.0', '94.0.4606.126'), + @pytest.mark.parametrize('pyqt_version, chromium_version, security_version', [ + ('5.15.2', '83.0.4103.122', '86.0.4240.183'), + ('5.15.3', '87.0.4280.144', '88.0.4324.150'), + ('5.15.4', '87.0.4280.144', None), + ('5.15.5', '87.0.4280.144', None), + ('5.15.6', '87.0.4280.144', None), + ('5.15.7', '87.0.4280.144', '94.0.4606.61'), + ('6.2.0', '90.0.4430.228', '93.0.4577.63'), + ('6.2.99', '90.0.4430.228', None), + ('6.3.0', '94.0.4606.126', '99.0.4844.84'), + ('6.99.0', None, None), ]) - def test_from_pyqt(self, freezer, pyqt_version, chromium_version): - if freezer and pyqt_version in ['5.15.3', '5.15.4', '5.15.5']: + def test_from_pyqt(self, freezer, pyqt_version, chromium_version, security_version): + if freezer and utils.VersionNumber(5, 15, 3) <= utils.VersionNumber.parse(pyqt_version) < utils.VersionNumber(6): chromium_version = '83.0.4103.122' + security_version = '86.0.4240.183' expected_pyqt_version = '5.15.2' else: expected_pyqt_version = pyqt_version @@ -981,6 +1012,7 @@ class TestWebEngineVersions: expected = version.WebEngineVersions( webengine=utils.VersionNumber.parse(expected_pyqt_version), chromium=chromium_version, + chromium_security=security_version, source='PyQt', ) assert version.WebEngineVersions.from_pyqt(pyqt_version) == expected @@ -1024,6 +1056,39 @@ class TestWebEngineVersions: assert inferred == real + def test_real_chromium_security_version(self, qapp): + """Check the API for reading the chromium security patch version.""" + try: + from qutebrowser.qt.webenginecore import ( + qWebEngineChromiumVersion, + qWebEngineChromiumSecurityPatchVersion, + ) + except ImportError: + pytest.skip("Requires QtWebEngine 6.3+") + + base = utils.VersionNumber.parse(qWebEngineChromiumVersion()) + security = utils.VersionNumber.parse(qWebEngineChromiumSecurityPatchVersion()) + assert security >= base + + def test_chromium_security_version_dict(self, qapp): + """Check if we infer the QtWebEngine security version properly. + + Note this test mostly tests that our overview in version.py (also + intended for human readers) is accurate. The code we call here is never + going to be called in real-life situations, as the API is available. + """ + try: + from qutebrowser.qt.webenginecore import ( + qWebEngineVersion, + qWebEngineChromiumSecurityPatchVersion, + ) + except ImportError: + pytest.skip("Requires QtWebEngine 6.3+") + + inferred = version.WebEngineVersions.from_webengine( + qWebEngineVersion(), source="API") + assert inferred.chromium_security == qWebEngineChromiumSecurityPatchVersion() + class FakeQSslSocket: @@ -1294,7 +1359,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub): else: monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) patches['objects.backend'] = usertypes.Backend.QtWebEngine - substitutions['backend'] = 'QtWebEngine 1.2.3 (from faked)' + substitutions['backend'] = 'QtWebEngine 1.2.3\n (source: faked)' if params.known_distribution: patches['distribution'] = lambda: version.DistributionInfo( diff --git a/tests/unit/utils/usertypes/test_timer.py b/tests/unit/utils/usertypes/test_timer.py index c02f160b6..6aabc8c04 100644 --- a/tests/unit/utils/usertypes/test_timer.py +++ b/tests/unit/utils/usertypes/test_timer.py @@ -4,6 +4,9 @@ """Tests for Timer.""" +import logging +import fnmatch + import pytest from qutebrowser.qt.core import QObject @@ -65,3 +68,63 @@ def test_timeout_set_interval(qtbot): with qtbot.wait_signal(t.timeout, timeout=3000): t.setInterval(200) t.start() + + +@pytest.mark.parametrize( + "elapsed_ms, expected", + [ + (0, False), + (1, False), + (600, True), + (999, True), + (1000, True), + ], +) +def test_early_timeout_check(qtbot, mocker, elapsed_ms, expected): + time_mock = mocker.patch("time.monotonic", autospec=True) + + t = usertypes.Timer() + t.setInterval(1000) # anything long enough to not actually fire + time_mock.return_value = 0 # assigned to _start_time in start() + t.start() + time_mock.return_value = elapsed_ms / 1000 # used for `elapsed` + + assert t.check_timeout_validity() is expected + + t.stop() + + +def test_early_timeout_handler(qtbot, mocker, caplog): + time_mock = mocker.patch("time.monotonic", autospec=True) + + t = usertypes.Timer(name="t") + t.setInterval(3) + t.setSingleShot(True) + time_mock.return_value = 0 + with caplog.at_level(logging.WARNING): + with qtbot.wait_signal(t.timeout, timeout=10): + t.start() + time_mock.return_value = 1 / 1000 + + assert len(caplog.messages) == 1 + assert fnmatch.fnmatch( + caplog.messages[-1], + "Timer t (id *) triggered too early: interval 3 but only 0.001s passed", + ) + + +def test_early_manual_fire(qtbot, mocker, caplog): + """Same as above but start() never gets called.""" + time_mock = mocker.patch("time.monotonic", autospec=True) + + t = usertypes.Timer(name="t") + t.setInterval(3) + t.setSingleShot(True) + time_mock.return_value = 0 + with caplog.at_level(logging.WARNING): + with qtbot.wait_signal(t.timeout, timeout=10): + t.timeout.emit() + time_mock.return_value = 1 / 1000 + + assert len(caplog.messages) == 0 + assert t.check_timeout_validity() |