diff options
author | Axel Dahlberg <git@valleymnt.com> | 2021-03-03 05:07:00 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-03 05:07:00 -0800 |
commit | 7db764f0950f04fe044d5187713738d116fbf702 (patch) | |
tree | e97ba568d8bd3d71d830cd97c6fe581a582434ed /tests | |
parent | 82ba01647b9000b5422f6f77e874acdf4f5a511d (diff) | |
parent | 9909bf0b113b1357bb19c678046a14762b2b6901 (diff) | |
download | qutebrowser-7db764f0950f04fe044d5187713738d116fbf702.tar.gz qutebrowser-7db764f0950f04fe044d5187713738d116fbf702.zip |
Merge branch 'master' into feature/6109-file-picker-stdout
Diffstat (limited to 'tests')
78 files changed, 1724 insertions, 601 deletions
diff --git a/tests/conftest.py b/tests/conftest.py index 16cd39656..ea7381a2f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,7 +36,7 @@ from helpers import logfail from helpers.logfail import fail_on_logging from helpers.messagemock import message_mock from helpers.fixtures import * # noqa: F403 -from helpers import utils as testutils +from helpers import testutils from qutebrowser.utils import qtutils, standarddir, usertypes, utils, version from qutebrowser.misc import objects, earlyinit from qutebrowser.qt import sip diff --git a/tests/end2end/data/darkmode/blank.html b/tests/end2end/data/darkmode/blank.html new file mode 100644 index 000000000..4d7b9c379 --- /dev/null +++ b/tests/end2end/data/darkmode/blank.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Blank page</title> + </head> + <body> + </body> +</html> diff --git a/tests/end2end/data/darkmode/prefers-color-scheme.html b/tests/end2end/data/darkmode/prefers-color-scheme.html new file mode 100644 index 000000000..b1feb84d7 --- /dev/null +++ b/tests/end2end/data/darkmode/prefers-color-scheme.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Prefers colorscheme test</title> + <style> +body { + background: #aa0000; +} +#dark-text { + display: none; +} +#light-text { + display: none; +} +#no-preference-text { + display: none; +} + +@media (prefers-color-scheme: dark) { + body { + background: #222222; + color: #ffffff; + } + #dark-text { + display: inline; + } + #missing-support-text { + display: none; + } +} + +@media (prefers-color-scheme: light) { + body { + background: #dddddd; + } + #light-text { + display: inline; + } + #missing-support-text { + display: none; + } +} + +@media (prefers-color-scheme: no-preference) { + body { + background: #00aa00; + } + #no-preference-text { + display: inline; + } + #missing-support-text { + display: none; + } +} + </style> + </head> + <body> + <p id="dark-text">Dark preference detected.</p> + <p id="light-text">Light preference detected.</p> + <p id="no-preference-text">No preference detected.</p> + <p id="missing-support-text">Preference support missing.</p> + </body> +</html> diff --git a/tests/end2end/data/darkmode/yellow.html b/tests/end2end/data/darkmode/yellow.html new file mode 100644 index 000000000..bfb1d82ba --- /dev/null +++ b/tests/end2end/data/darkmode/yellow.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<!-- Is it yellow? Is it green? Who knows! --> +<html> + <head> + <meta charset="utf-8"> + <title>Yellow page</title> + </head> + <body bgcolor="#ffff99"> + </body> +</html> diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 2862cc5d6..ce5fc3a01 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -37,7 +37,7 @@ import pytest_bdd as bdd import qutebrowser from qutebrowser.utils import log, utils, docutils from qutebrowser.browser import pdfjs -from helpers import utils as testutils +from helpers import testutils def _get_echo_exe_path(): @@ -294,7 +294,7 @@ def run_command(quteproc, server, tmpdir, command): @bdd.when(bdd.parsers.parse("I reload")) def reload(qtbot, server, quteproc, command): """Reload and wait until a new request is received.""" - with qtbot.waitSignal(server.new_request): + with qtbot.wait_signal(server.new_request): quteproc.send_cmd(':reload') diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 226e99ffa..1433f4c0a 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -611,7 +611,7 @@ Feature: Downloading things from a website. When the unwritable dir is unwritable And I set downloads.location.prompt to false And I run :download http://localhost:(port)/data/downloads/download.bin --dest (tmpdir)/downloads/unwritable - Then the error "Download error: Permission denied" should be shown + Then the error "Download error: *" should be shown Scenario: Downloading 20MB file When I set downloads.location.prompt to false diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature index cb12360bc..47cb1230a 100644 --- a/tests/end2end/features/editor.feature +++ b/tests/end2end/features/editor.feature @@ -228,3 +228,14 @@ Feature: Opening external editors And I open data/fileselect.html And I run :click-element id multiple_files Then the javascript message "Files: 1.txt, 2.txt" should be logged + + ## No temporary file created + + Scenario: File selector deleting temporary file + When I set fileselect.handler to external + And I set fileselect.single_file.command to ['rm', '{}'] + And I open data/fileselect.html + And I run :click-element id single_file + Then the javascript message "Files: 1.txt" should not be logged + And the error "Failed to open tempfile *" should be shown + And "Failed to delete tempfile *" should be logged with level error diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index fb04db01c..cf35c5356 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -443,7 +443,8 @@ Feature: Using hints ### hints.leave_on_load Scenario: Leaving hint mode on reload - When I open data/hints/html/wrapped.html + When I set hints.leave_on_load to true + And I open data/hints/html/wrapped.html And I hint with args "all" And I run :reload Then "Leaving mode KeyMode.hint (reason: load started)" should be logged diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index 3666d609d..79b9e7d01 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -167,7 +167,7 @@ Feature: Javascript stuff Then "Showing error page for* 500" should be logged And "Load error: *500" should be logged - @flaky @windows_skip + @skip # Too flaky Scenario: Using JS after window.open When I open data/hello.txt And I set content.javascript.can_open_tabs_automatically to true diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index f2e098bc7..351135fab 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -468,6 +468,18 @@ Feature: Various utility commands. And I run :command-accept Then the message "blah" should be shown + Scenario: Command starting with space and calling previous command + When I run :set-cmd-text :message-info first + And I run :command-accept + And I wait for "first" in the log + When I run :set-cmd-text : message-info second + And I run :command-accept + And I wait for "second" in the log + And I run :set-cmd-text : + And I run :command-history-prev + And I run :command-accept + Then the message "first" should be shown + Scenario: Calling previous command with :completion-item-focus When I run :set-cmd-text :message-info blah And I wait for "Entering mode KeyMode.command (reason: *)" in the log diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index b1fee1603..286f8f80a 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -54,7 +54,7 @@ Feature: Special qute:// pages And I run :tab-only And I open qute:help without waiting And I wait for "Changing title for idx 0 to 'qutebrowser help'" in the log - And I hint with args "links normal" and follow a + And I hint with args "links normal" and follow ls Then qute://help/quickstart.html should be loaded Scenario: Opening a link with qute://help @@ -62,7 +62,7 @@ Feature: Special qute:// pages And I run :tab-only And I open qute://help without waiting And I wait until qute://help/ is loaded - And I hint with args "links normal" and follow a + And I hint with args "links normal" and follow ls Then qute://help/quickstart.html should be loaded Scenario: Opening a link with qute://help/index.html/.. diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 0f0c015e0..e48947cbd 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -395,9 +395,10 @@ Feature: Saving and loading sessions And I run :session-load -c pin_session And I wait until data/numbers/3.txt is loaded And I run :tab-focus 2 - And I run :open hello world - Then the message "Tab is pinned!" should be shown + And I open data/numbers/4.txt + Then the message "Tab is pinned! Opening in new tab." should be shown And the following tabs should be open: - data/numbers/1.txt - data/numbers/2.txt (active) (pinned) + - data/numbers/4.txt - data/numbers/3.txt diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index ca0efefc4..7db054573 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1534,10 +1534,11 @@ Feature: Tab management Scenario: :tab-pin open url When I open data/numbers/1.txt And I run :tab-pin - And I open data/numbers/2.txt without waiting - Then the message "Tab is pinned!" should be shown + And I open data/numbers/2.txt + Then the message "Tab is pinned! Opening in new tab." should be shown And the following tabs should be open: - data/numbers/1.txt (active) (pinned) + - data/numbers/2.txt Scenario: :tab-pin open url with tabs.pinned.frozen = false When I set tabs.pinned.frozen to false diff --git a/tests/end2end/features/test_urlmarks_bdd.py b/tests/end2end/features/test_urlmarks_bdd.py index 2d4dcb5b5..8aea592c3 100644 --- a/tests/end2end/features/test_urlmarks_bdd.py +++ b/tests/end2end/features/test_urlmarks_bdd.py @@ -21,7 +21,7 @@ import os.path import pytest_bdd as bdd -from helpers import utils +from helpers import testutils bdd.scenarios('urlmarks.feature') @@ -48,7 +48,7 @@ def _check_marks(quteproc, quickmarks, expected, contains): lines = f.readlines() matched_line = any( - utils.pattern_match(pattern=expected, value=line.rstrip('\n')) + testutils.pattern_match(pattern=expected, value=line.rstrip('\n')) for line in lines) assert matched_line == contains, lines diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 4945dbc77..9ef338768 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -33,11 +33,12 @@ import json import yaml import pytest -from PyQt5.QtCore import pyqtSignal, QUrl +from PyQt5.QtCore import pyqtSignal, QUrl, QPoint +from PyQt5.QtGui import QImage, QColor from qutebrowser.misc import ipc from qutebrowser.utils import log, utils, javascript -from helpers import utils as testutils +from helpers import testutils from end2end.fixtures import testprocess @@ -337,8 +338,11 @@ def is_ignored_chromium_message(line): # Windows N: # https://github.com/microsoft/playwright/issues/2901 - (r'DXVAVDA fatal error: could not LoadLibrary: .*: The specified ' - r'module could not be found. \(0x7E\)'), + ('DXVAVDA fatal error: could not LoadLibrary: *: The specified ' + 'module could not be found. (0x7E)'), + + # Qt 5.15.3 dev build + r'Duplicate id found. Reassigning from * to *', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) @@ -884,6 +888,40 @@ class QuteProc(testprocess.Process): with open(path, 'r', encoding='utf-8') as f: return f.read() + def get_screenshot( + self, + *, + probe_pos: QPoint = None, + probe_color: QColor = testutils.Color(0, 0, 0), + ) -> QImage: + """Get a screenshot of the current page. + + Arguments: + probe: If given, only continue if the pixel at the given position isn't + black (or whatever is specified by probe_color). + """ + for _ in range(5): + tmp_path = self.request.getfixturevalue('tmp_path') + path = tmp_path / 'screenshot.png' + self.send_cmd(f':screenshot --force {path}') + self.wait_for(message=f'Screenshot saved to {path}') + + img = QImage(str(path)) + assert not img.isNull() + + if probe_pos is None: + return img + + probed_color = testutils.Color(img.pixelColor(probe_pos)) + if probed_color == probe_color: + return img + + # Rendering might not be completed yet... + time.sleep(0.5) + + raise ValueError( + f"Pixel probing for {probe_color} failed (got {probed_color} on last try)") + def press_keys(self, keys): """Press the given keys using :fake-key.""" self.send_cmd(':fake-key -g "{}"'.format(keys)) diff --git a/tests/end2end/fixtures/test_quteprocess.py b/tests/end2end/fixtures/test_quteprocess.py index 81dd1d13b..c4b226972 100644 --- a/tests/end2end/fixtures/test_quteprocess.py +++ b/tests/end2end/fixtures/test_quteprocess.py @@ -104,7 +104,7 @@ def request_mock(quteproc, monkeypatch, server): ]) def test_quteproc_error_message(qtbot, quteproc, cmd, request_mock): """Make sure the test fails with an unexpected error message.""" - with qtbot.waitSignal(quteproc.got_error): + with qtbot.wait_signal(quteproc.got_error): quteproc.send_cmd(cmd) # Usually we wouldn't call this from inside a test, but here we force the # error to occur during the test rather than at teardown time. @@ -115,7 +115,7 @@ def test_quteproc_error_message(qtbot, quteproc, cmd, request_mock): def test_quteproc_error_message_did_fail(qtbot, quteproc, request_mock): """Make sure the test does not fail on teardown if the main test failed.""" request_mock.node.rep_call.failed = True - with qtbot.waitSignal(quteproc.got_error): + with qtbot.wait_signal(quteproc.got_error): quteproc.send_cmd(':message-error test') # Usually we wouldn't call this from inside a test, but here we force the # error to occur during the test rather than at teardown time. @@ -142,13 +142,13 @@ def test_quteproc_skip_and_wait_for(qtbot, quteproc): def test_qt_log_ignore(qtbot, quteproc): """Make sure the test passes when logging a qt_log_ignore message.""" - with qtbot.waitSignal(quteproc.got_error): + with qtbot.wait_signal(quteproc.got_error): quteproc.send_cmd(':message-error "SpellCheck: test"') def test_quteprocess_quitting(qtbot, quteproc_process): """When qutebrowser quits, after_test should fail.""" - with qtbot.waitSignal(quteproc_process.proc.finished, timeout=15000): + with qtbot.wait_signal(quteproc_process.proc.finished, timeout=15000): quteproc_process.send_cmd(':quit') with pytest.raises(testprocess.ProcessExited): quteproc_process.after_test() diff --git a/tests/end2end/fixtures/test_testprocess.py b/tests/end2end/fixtures/test_testprocess.py index 73fcf8b05..aa6f19c67 100644 --- a/tests/end2end/fixtures/test_testprocess.py +++ b/tests/end2end/fixtures/test_testprocess.py @@ -131,7 +131,7 @@ def test_no_ready_python_process(noready_pyproc): def test_quitting_process(qtbot, quit_pyproc): - with qtbot.waitSignal(quit_pyproc.proc.finished): + with qtbot.wait_signal(quit_pyproc.proc.finished): quit_pyproc.start() with pytest.raises(testprocess.ProcessExited): quit_pyproc.after_test() @@ -139,7 +139,7 @@ def test_quitting_process(qtbot, quit_pyproc): def test_quitting_process_expected(qtbot, quit_pyproc): quit_pyproc.exit_expected = True - with qtbot.waitSignal(quit_pyproc.proc.finished): + with qtbot.wait_signal(quit_pyproc.proc.finished): quit_pyproc.start() quit_pyproc.after_test() diff --git a/tests/end2end/fixtures/test_webserver.py b/tests/end2end/fixtures/test_webserver.py index 4ad9108ca..3c825e5bc 100644 --- a/tests/end2end/fixtures/test_webserver.py +++ b/tests/end2end/fixtures/test_webserver.py @@ -34,7 +34,7 @@ import pytest ('/data/hello.txt', 'Hello World!', True), ]) def test_server(server, qtbot, path, content, expected): - with qtbot.waitSignal(server.new_request, timeout=100): + with qtbot.wait_signal(server.new_request, timeout=100): url = 'http://localhost:{}{}'.format(server.port, path) try: response = urllib.request.urlopen(url) diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index 75bab0bf3..33b154e9a 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer, QProcessEnvironment) from PyQt5.QtTest import QSignalSpy -from helpers import utils +from helpers import testutils from qutebrowser.utils import utils as quteutils @@ -77,13 +77,13 @@ def _render_log(data, *, verbose, threshold=100): if (len(data) > threshold and not verbose and not is_exception and - not utils.ON_CI): + not testutils.ON_CI): msg = '[{} lines suppressed, use -v to show]'.format( len(data) - threshold) data = [msg] + data[-threshold:] - if utils.ON_CI: - data = [utils.gha_group_begin('Log')] + data + [utils.gha_group_end()] + if testutils.ON_CI: + data = [testutils.gha_group_begin('Log')] + data + [testutils.gha_group_end()] return '\n'.join(data) @@ -233,7 +233,7 @@ class Process(QObject): self._started = True verbose = self.request.config.getoption('--verbose') - timeout = 60 if utils.ON_CI else 20 + timeout = 60 if testutils.ON_CI else 20 for _ in range(timeout): with self._wait_signal(self.ready, timeout=1000, raising=False) as blocker: @@ -350,7 +350,7 @@ class Process(QObject): elif isinstance(expected, regex_type): return expected.search(value) elif isinstance(value, (bytes, str)): - return utils.pattern_match(pattern=expected, value=value) + return testutils.pattern_match(pattern=expected, value=value) else: return value == expected @@ -475,7 +475,7 @@ class Process(QObject): if timeout is None: if do_skip: timeout = 2000 - elif utils.ON_CI: + elif testutils.ON_CI: timeout = 15000 else: timeout = 5000 diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 4677415f1..81a864c8e 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -75,6 +75,7 @@ class Request(testprocess.Line): '/absolute-redirect': [HTTPStatus.FOUND], '/cookies/set': [HTTPStatus.FOUND], + '/cookies/set-custom': [HTTPStatus.FOUND], '/500-inline': [HTTPStatus.INTERNAL_SERVER_ERROR], '/500': [HTTPStatus.INTERNAL_SERVER_ERROR], diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py index b7999148f..a4f54e19c 100644 --- a/tests/end2end/fixtures/webserver_sub.py +++ b/tests/end2end/fixtures/webserver_sub.py @@ -199,6 +199,16 @@ def set_cookies(): return r +@app.route('/cookies/set-custom') +def set_custom_cookie(): + """Set a cookie with a custom max_age/expires.""" + r = app.make_response(flask.redirect(flask.url_for('view_cookies'))) + max_age = flask.request.args.get('max_age') + r.set_cookie(key='cookie', value='value', + max_age=int(max_age) if max_age else None) + return r + + @app.route('/basic-auth/<user>/<passwd>') def basic_auth(user='user', passwd='passwd'): """Prompt the user for authorization using HTTP Basic Auth.""" diff --git a/tests/end2end/test_dirbrowser.py b/tests/end2end/test_dirbrowser.py index 8a9f895a2..3efbfc14e 100644 --- a/tests/end2end/test_dirbrowser.py +++ b/tests/end2end/test_dirbrowser.py @@ -30,7 +30,7 @@ import bs4 from PyQt5.QtCore import QUrl from qutebrowser.utils import urlutils -from helpers import utils as testutils +from helpers import testutils pytestmark = pytest.mark.qtwebengine_skip("Title is empty when parsing for " diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index b509e355b..f3d74d1f0 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -23,13 +23,15 @@ import configparser import subprocess import sys import logging +import importlib import re import json import pytest -from PyQt5.QtCore import QProcess +from PyQt5.QtCore import QProcess, QPoint -from helpers import utils +from helpers import testutils +from qutebrowser.utils import qtutils, utils ascii_locale = pytest.mark.skipif(sys.hexversion >= 0x03070000, @@ -46,7 +48,7 @@ def _base_args(config): args += ['--backend', 'webkit'] if config.webengine: - args += utils.seccomp_args(qt_flag=True) + args += testutils.seccomp_args(qt_flag=True) args.append('about:blank') return args @@ -424,24 +426,116 @@ def test_referrer(quteproc_new, server, server2, request, value, expected): assert headers.get('Referer') == expected +def test_preferred_colorscheme_unsupported(request, quteproc_new): + """Test versions without preferred-color-scheme support.""" + if request.config.webengine and qtutils.version_check('5.14'): + pytest.skip("preferred-color-scheme is supported") + + args = _base_args(request.config) + ['--temp-basedir'] + quteproc_new.start(args) + quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') + content = quteproc_new.get_content() + assert content == "Preference support missing." + + @pytest.mark.qtwebkit_skip -@utils.qt514 -def test_preferred_colorscheme(request, quteproc_new): +@testutils.qt514 +@pytest.mark.parametrize('value', ["dark", "light", "auto", None]) +def test_preferred_colorscheme(request, quteproc_new, value): """Make sure the the preferred colorscheme is set.""" + if not request.config.webengine: + pytest.skip("Skipped with QtWebKit") + + args = _base_args(request.config) + ['--temp-basedir'] + if value is not None: + args += ['-s', 'colors.webpage.preferred_color_scheme', value] + quteproc_new.start(args) + + dark_text = "Dark preference detected." + light_text = "Light preference detected." + + expected_values = { + "dark": [dark_text], + "light": [light_text], + + # Depends on the environment the test is running in. + "auto": [dark_text, light_text], + None: [dark_text, light_text], + } + xfail = False + if not qtutils.version_check('5.15.2', compiled=False): + # On older versions, "light" is not supported, so the result will depend on the + # environment. + expected_values["light"].append(dark_text) + elif qtutils.version_check('5.15.2', exact=True, compiled=False): + # Test the WORKAROUND https://bugreports.qt.io/browse/QTBUG-89753 + # With that workaround, we should always get the light preference. + for key in ["auto", None]: + expected_values[key].remove(dark_text) + xfail = value in ["auto", None] + + quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') + content = quteproc_new.get_content() + assert content in expected_values[value] + + if xfail: + # Unsatisfactory result, but expected based on a Qt bug. + pytest.xfail("QTBUG-89753") + + +@testutils.qt514 +def test_preferred_colorscheme_with_dark_mode( + request, quteproc_new, webengine_versions): + """Test interaction between preferred-color-scheme and dark mode.""" + if not request.config.webengine: + pytest.skip("Skipped with QtWebKit") + args = _base_args(request.config) + [ '--temp-basedir', - '-s', 'colors.webpage.prefers_color_scheme_dark', 'true', + '-s', 'colors.webpage.preferred_color_scheme', 'dark', + '-s', 'colors.webpage.darkmode.enabled', 'true', + '-s', 'colors.webpage.darkmode.algorithm', 'brightness-rgb', ] quteproc_new.start(args) - quteproc_new.send_cmd(':jseval matchMedia("(prefers-color-scheme: dark)").matches') - quteproc_new.wait_for(message='True') + quteproc_new.open_path('data/darkmode/prefers-color-scheme.html') + content = quteproc_new.get_content() + + if webengine_versions.webengine == utils.VersionNumber(5, 15, 3): + # https://bugs.chromium.org/p/chromium/issues/detail?id=1177973 + # No workaround known. + expected_text = 'Light preference detected.' + # light website color, inverted by darkmode + expected_color = testutils.Color(127, 127, 127) + xfail = True + elif webengine_versions.webengine == utils.VersionNumber(5, 15, 2): + # Our workaround breaks when dark mode is enabled... + # Also, for some reason, dark mode doesn't work on that page either! + expected_text = 'No preference detected.' + expected_color = testutils.Color(0, 170, 0) # green + xfail = True + else: + # Qt 5.14 and 5.15.0/.1 work correctly. + # Hopefully, so does Qt 6.x in the future? + expected_text = 'Dark preference detected.' + expected_color = testutils.Color(34, 34, 34) # dark website color + xfail = False + + pos = QPoint(0, 0) + img = quteproc_new.get_screenshot(probe_pos=pos, probe_color=expected_color) + color = testutils.Color(img.pixelColor(pos)) + + assert content == expected_text + assert color == expected_color + if xfail: + # We still do some checks, but we want to mark the test outcome as xfail. + pytest.xfail("QTBUG-89753") @pytest.mark.qtwebkit_skip @pytest.mark.parametrize('reason', [ 'Explicitly enabled', - pytest.param('Qt 5.14', marks=utils.qt514), + pytest.param('Qt 5.14', marks=testutils.qt514), 'Qt version changed', None, ]) @@ -490,3 +584,131 @@ def test_service_worker_workaround( quteproc_new.ensure_not_logged(message='Removing service workers at *') else: assert not service_worker_dir.exists() + + +@testutils.qt513 # Qt 5.12 doesn't store cookies immediately +@pytest.mark.parametrize('store', [True, False]) +def test_cookies_store(quteproc_new, request, short_tmpdir, store): + # Start test process + args = _base_args(request.config) + [ + '--basedir', str(short_tmpdir), + '-s', 'content.cookies.store', str(store), + ] + quteproc_new.start(args) + + # Set cookie and ensure it's set + quteproc_new.open_path('cookies/set-custom?max_age=30', wait=False) + quteproc_new.wait_for_load_finished('cookies') + content = quteproc_new.get_content() + data = json.loads(content) + assert data == {'cookies': {'cookie': 'value'}} + + # Restart + quteproc_new.send_cmd(':quit') + quteproc_new.wait_for_quit() + quteproc_new.start(args) + + # Check cookies + quteproc_new.open_path('cookies') + content = quteproc_new.get_content() + data = json.loads(content) + expected_cookies = {'cookie': 'value'} if store else {} + assert data == {'cookies': expected_cookies} + + quteproc_new.send_cmd(':quit') + quteproc_new.wait_for_quit() + + +@pytest.mark.parametrize('filename, algorithm, colors', [ + ( + 'blank', + 'lightness-cielab', + { + '5.15': testutils.Color(18, 18, 18), + '5.14': testutils.Color(27, 27, 27), + None: testutils.Color(0, 0, 0), + } + ), + ('blank', 'lightness-hsl', {None: testutils.Color(0, 0, 0)}), + ('blank', 'brightness-rgb', {None: testutils.Color(0, 0, 0)}), + + ( + 'yellow', + 'lightness-cielab', + { + '5.15': testutils.Color(35, 34, 0), + '5.14': testutils.Color(35, 34, 0), + None: testutils.Color(204, 204, 0), + } + ), + ('yellow', 'lightness-hsl', {None: testutils.Color(204, 204, 0)}), + ('yellow', 'brightness-rgb', {None: testutils.Color(0, 0, 204)}), +]) +def test_dark_mode(webengine_versions, quteproc_new, request, + filename, algorithm, colors): + if not request.config.webengine: + pytest.skip("Skipped with QtWebKit") + + args = _base_args(request.config) + [ + '--temp-basedir', + '-s', 'colors.webpage.darkmode.enabled', 'true', + '-s', 'colors.webpage.darkmode.algorithm', algorithm, + ] + quteproc_new.start(args) + + ver = webengine_versions.webengine + minor_version = f'{ver.majorVersion()}.{ver.minorVersion()}' + expected = colors.get(minor_version, colors[None]) + + quteproc_new.open_path(f'data/darkmode/{filename}.html') + + # Position chosen by fair dice roll. + # https://xkcd.com/221/ + pos = QPoint(4, 4) + img = quteproc_new.get_screenshot(probe_pos=pos, probe_color=expected) + + color = testutils.Color(img.pixelColor(pos)) + # For pytest debug output + assert color == expected + + +def test_unavailable_backend(request, quteproc_new): + """Test starting with a backend which isn't available. + + If we use --qute-bdd-webengine, we test with QtWebKit here; otherwise we test with + QtWebEngine. If both are available, the test is skipped. + + This ensures that we don't accidentally use backend-specific code before checking + that the chosen backend is actually available - i.e., that the error message is + properly printed, rather than an unhandled exception. + """ + qtwe_module = "PyQt5.QtWebEngineWidgets" + qtwk_module = "PyQt5.QtWebKitWidgets" + # Note we want to try the *opposite* backend here. + if request.config.webengine: + pytest.importorskip(qtwe_module) + module = qtwk_module + backend = 'webkit' + else: + pytest.importorskip(qtwk_module) + module = qtwe_module + backend = 'webengine' + + try: + importlib.import_module(module) + except ImportError: + pass + else: + pytest.skip(f"{module} is available") + + args = [ + '--debug', '--json-logging', '--no-err-windows', + '--backend', backend, + '--temp-basedir' + ] + quteproc_new.exit_expected = True + quteproc_new.start(args) + line = quteproc_new.wait_for( + message=('*qutebrowser tried to start with the Qt* backend but failed ' + 'because * could not be imported.*')) + line.expected = True diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 547e11dba..4e0204741 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -45,7 +45,7 @@ import helpers.stubs as stubsmod from qutebrowser.config import (config, configdata, configtypes, configexc, configfiles, configcache, stylesheet) from qutebrowser.api import config as configapi -from qutebrowser.utils import objreg, standarddir, utils, usertypes +from qutebrowser.utils import objreg, standarddir, utils, usertypes, version from qutebrowser.browser import greasemonkey, history, qutescheme from qutebrowser.browser.webkit import cookies, cache from qutebrowser.misc import savemanager, sql, objects, sessions @@ -73,7 +73,7 @@ class WidgetContainer(QWidget): self._widget = widget def expose(self): - with self._qtbot.waitExposed(self): + with self._qtbot.wait_exposed(self): self.show() self._widget.setFocus() @@ -407,7 +407,7 @@ def status_command_stub(stubs, qtbot, win_registry): """Fixture which provides a fake status-command object.""" cmd = stubs.StatusBarCommandStub() objreg.register('status-command', cmd, scope='window', window=0) - qtbot.addWidget(cmd) + qtbot.add_widget(cmd) yield cmd objreg.delete('status-command', scope='window', window=0) @@ -725,3 +725,14 @@ def unwritable_tmp_path(tmp_path): # Make sure pytest can clean up the tmp_path tmp_path.chmod(0o755) + + +@pytest.fixture +def webengine_versions(testdata_scheme): + """Get QtWebEngine version numbers. + + Calling qtwebengine_versions() initializes QtWebEngine, so we depend on + testdata_scheme here, to make sure that happens before. + """ + pytest.importorskip('PyQt5.QtWebEngineWidgets') + return version.qtwebengine_versions() diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index c1ccdcf8b..4c56cf76c 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# pylint: disable=invalid-name,abstract-method +# pylint: disable=abstract-method """Fake objects/stubs.""" @@ -26,6 +26,9 @@ from unittest import mock import contextlib import shutil import dataclasses +import builtins +import importlib +import types from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject, QUrl from PyQt5.QtGui import QIcon @@ -623,10 +626,11 @@ class FakeHistoryProgress: """Fake for a WebHistoryProgress object.""" - def __init__(self): + def __init__(self, *, raise_on_tick=False): self._started = False self._finished = False self._value = 0 + self._raise_on_tick = raise_on_tick def start(self, _text): self._started = True @@ -635,6 +639,8 @@ class FakeHistoryProgress: pass def tick(self): + if self._raise_on_tick: + raise Exception('tick-tock') self._value += 1 def finish(self): @@ -676,3 +682,66 @@ class FakeCookieStore: def setCookieFilter(self, func): self.cookie_filter = func + + +class ImportFake: + + """A fake for __import__ which is used by the import_fake fixture. + + Attributes: + modules: A dict mapping module names to bools. If True, the import will + succeed. Otherwise, it'll fail with ImportError. + version_attribute: The name to use in the fake modules for the version + attribute. + version: The version to use for the modules. + _real_import: Saving the real __import__ builtin so the imports can be + done normally for modules not in self. modules. + """ + + def __init__(self, modules, monkeypatch): + self._monkeypatch = monkeypatch + self.modules = modules + self.version_attribute = '__version__' + self.version = '1.2.3' + self._real_import = builtins.__import__ + self._real_importlib_import = importlib.import_module + + def patch(self): + """Patch import functions.""" + self._monkeypatch.setattr(builtins, '__import__', self.fake_import) + self._monkeypatch.setattr( + importlib, 'import_module', self.fake_importlib_import) + + def _do_import(self, name): + """Helper for fake_import and fake_importlib_import to do the work. + + Return: + The imported fake module, or None if normal importing should be + used. + """ + if name not in self.modules: + # Not one of the modules to test -> use real import + return None + elif self.modules[name]: + ns = types.SimpleNamespace() + if self.version_attribute is not None: + setattr(ns, self.version_attribute, self.version) + return ns + else: + raise ImportError("Fake ImportError for {}.".format(name)) + + def fake_import(self, name, *args, **kwargs): + """Fake for the builtin __import__.""" + module = self._do_import(name) + if module is not None: + return module + else: + return self._real_import(name, *args, **kwargs) + + def fake_importlib_import(self, name): + """Fake for importlib.import_module.""" + module = self._do_import(name) + if module is not None: + return module + else: + return self._real_importlib_import(name) diff --git a/tests/helpers/test_helper_utils.py b/tests/helpers/test_helper_utils.py index 2f4822df9..5d723429b 100644 --- a/tests/helpers/test_helper_utils.py +++ b/tests/helpers/test_helper_utils.py @@ -20,7 +20,7 @@ import pytest -from helpers import utils +from helpers import testutils @pytest.mark.parametrize('val1, val2', [ @@ -32,7 +32,7 @@ from helpers import utils ("foobarbaz", "foo*baz"), ]) def test_partial_compare_equal(val1, val2): - assert utils.partial_compare(val1, val2) + assert testutils.partial_compare(val1, val2) @pytest.mark.parametrize('val1, val2, error', [ @@ -48,9 +48,9 @@ def test_partial_compare_equal(val1, val2): (23.42, 13.37, "23.42 != 13.37 (float comparison)"), ]) def test_partial_compare_not_equal(val1, val2, error): - outcome = utils.partial_compare(val1, val2) + outcome = testutils.partial_compare(val1, val2) assert not outcome - assert isinstance(outcome, utils.PartialCompareOutcome) + assert isinstance(outcome, testutils.PartialCompareOutcome) assert outcome.error == error @@ -72,9 +72,9 @@ def test_partial_compare_not_equal(val1, val2, error): ('foo?ar', 'foo?ar', True), ]) def test_pattern_match(pattern, value, expected): - assert utils.pattern_match(pattern=pattern, value=value) == expected + assert testutils.pattern_match(pattern=pattern, value=value) == expected def test_nop_contextmanager(): - with utils.nop_contextmanager(): + with testutils.nop_contextmanager(): pass diff --git a/tests/helpers/utils.py b/tests/helpers/testutils.py index 41da08331..8bb622133 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/testutils.py @@ -32,19 +32,32 @@ import importlib.machinery import pytest from PyQt5.QtCore import qVersion +from PyQt5.QtGui import QColor try: from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION_STR except ImportError: PYQT_WEBENGINE_VERSION_STR = None -from qutebrowser.utils import qtutils, log +from qutebrowser.utils import qtutils, log, utils ON_CI = 'CI' in os.environ +qt513 = pytest.mark.skipif( + not qtutils.version_check('5.13'), reason="Needs Qt 5.13 or newer") qt514 = pytest.mark.skipif( not qtutils.version_check('5.14'), reason="Needs Qt 5.14 or newer") +class Color(QColor): + + """A QColor with a nicer repr().""" + + def __repr__(self): + return utils.get_repr(self, constructor=True, red=self.red(), + green=self.green(), blue=self.blue(), + alpha=self.alpha()) + + class PartialCompareOutcome: """Storage for a partial_compare error. diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index ad5425b8e..288471ea0 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -107,7 +107,7 @@ def test_selection_callback_wrong_mode(qtbot, caplog, async callback was happening, so we don't want to mess with the status bar. """ assert mode_manager.mode == usertypes.KeyMode.normal - with qtbot.assertNotEmitted(webengine_tab.caret.selection_toggled): + with qtbot.assert_not_emitted(webengine_tab.caret.selection_toggled): webengine_tab.caret._toggle_sel_translate('normal') msg = 'Ignoring caret selection callback in KeyMode.normal' diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index 9e3bd0519..9b08de30d 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -290,7 +290,7 @@ class TestHistoryInterface: def test_history_interface(self, qtbot, webview, hist_interface): html = b"<a href='about:blank'>foo</a>" url = urlutils.data_url('text/html', html) - with qtbot.waitSignal(webview.loadFinished): + with qtbot.wait_signal(webview.loadFinished): webview.load(url) @@ -392,6 +392,25 @@ class TestRebuild: ('example.com/1', '', 1), ('example.com/2', '', 2), ] + assert not hist3.metainfo['force_rebuild'] + + def test_force_rebuild(self, web_history, stubs): + """Ensure that completion is regenerated if we force a rebuild.""" + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.completion.delete('url', 'example.com/2') + + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + # User version always changes, so this won't work + # assert list(hist2.completion) == [('example.com/1', '', 1)] + hist2.metainfo['force_rebuild'] = True + + hist3 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist3.completion) == [ + ('example.com/1', '', 1), + ('example.com/2', '', 2), + ] + assert not hist3.metainfo['force_rebuild'] def test_exclude(self, config_stub, web_history, stubs): """Ensure that patterns in completion.web_history.exclude are ignored. @@ -443,6 +462,23 @@ class TestRebuild: assert progress._started assert progress._finished + def test_interrupted(self, stubs, web_history, monkeypatch): + """If we interrupt the rebuilding process, force_rebuild should still be set.""" + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + progress = stubs.FakeHistoryProgress(raise_on_tick=True) + + # Trigger a completion rebuild + monkeypatch.setattr(sql, 'user_version_changed', lambda: True) + + with pytest.raises(Exception, match='tick-tock'): + history.WebHistory(progress=progress) + + assert web_history.metainfo['force_rebuild'] + + # If we now try again, we should get another rebuild. But due to user_version + # always changing, we can't test this at the moment (see the FIXME in the + # docstring for details) + class TestCompletionMetaInfo: @@ -466,12 +502,6 @@ class TestCompletionMetaInfo: def test_contains(self, metainfo): assert 'excluded_patterns' in metainfo - def test_delete_old_key(self, monkeypatch, metainfo): - metainfo.insert({'key': 'force_rebuild', 'value': False}) - info2 = history.CompletionMetaInfo() - monkeypatch.setitem(info2.KEYS, 'force_rebuild', False) - assert 'force_rebuild' not in info2 - def test_modify(self, metainfo): assert not metainfo['excluded_patterns'] value = 'https://example.com/' diff --git a/tests/unit/browser/test_inspector.py b/tests/unit/browser/test_inspector.py index cb76a18a6..f7f532050 100644 --- a/tests/unit/browser/test_inspector.py +++ b/tests/unit/browser/test_inspector.py @@ -146,9 +146,9 @@ def test_detach_after_toggling(hidden_again, needs_recreate, if needs_recreate: fake_inspector.needs_recreate = True - with qtbot.waitSignal(fake_inspector.recreate): + with qtbot.wait_signal(fake_inspector.recreate): fake_inspector.set_position(inspector.Position.window) else: - with qtbot.assertNotEmitted(fake_inspector.recreate): + with qtbot.assert_not_emitted(fake_inspector.recreate): fake_inspector.set_position(inspector.Position.window) assert fake_inspector.isVisible() and fake_inspector.isWindow() diff --git a/tests/unit/browser/webengine/test_darkmode.py b/tests/unit/browser/webengine/test_darkmode.py index 743f2ab1a..3f5272dab 100644 --- a/tests/unit/browser/webengine/test_darkmode.py +++ b/tests/unit/browser/webengine/test_darkmode.py @@ -21,10 +21,10 @@ import logging import pytest from qutebrowser.config import configdata -from qutebrowser.utils import usertypes, version +from qutebrowser.utils import usertypes, version, utils from qutebrowser.browser.webengine import darkmode from qutebrowser.misc import objects -from helpers import utils +from helpers import testutils @pytest.fixture(autouse=True) @@ -32,24 +32,63 @@ def patch_backend(monkeypatch): monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) -@pytest.mark.parametrize('qversion, enabled, expected', [ - # Disabled or nothing set - ("5.14", False, []), - ("5.15.0", False, []), - ("5.15.1", False, []), - ("5.15.2", False, []), - - # Enabled in configuration - ("5.14", True, []), - ("5.15.0", True, []), - ("5.15.1", True, []), - ("5.15.2", True, [("preferredColorScheme", "1")]), +@pytest.fixture +def gentoo_versions(): + return version.WebEngineVersions( + webengine=utils.VersionNumber(5, 15, 2), + chromium='87.0.4280.144', + source='faked', + ) + + +@pytest.mark.parametrize('value, webengine_version, expected', [ + # Auto + ("auto", "5.14", []), + ("auto", "5.15.0", []), + ("auto", "5.15.1", []), + ("auto", "5.15.2", [("preferredColorScheme", "2")]), # QTBUG-89753 + ("auto", "5.15.3", []), + ("auto", "6.0.0", []), + + # Unset + (None, "5.14", []), + (None, "5.15.0", []), + (None, "5.15.1", []), + (None, "5.15.2", [("preferredColorScheme", "2")]), # QTBUG-89753 + (None, "5.15.3", []), + (None, "6.0.0", []), + + # Dark + ("dark", "5.14", []), + ("dark", "5.15.0", []), + ("dark", "5.15.1", []), + ("dark", "5.15.2", [("preferredColorScheme", "1")]), + ("dark", "5.15.3", [("preferredColorScheme", "0")]), + ("dark", "6.0.0", [("preferredColorScheme", "0")]), + + # Light + ("light", "5.14", []), + ("light", "5.15.0", []), + ("light", "5.15.1", []), + ("light", "5.15.2", [("preferredColorScheme", "2")]), + ("light", "5.15.3", [("preferredColorScheme", "1")]), + ("light", "6.0.0", [("preferredColorScheme", "1")]), ]) -@utils.qt514 -def test_colorscheme(config_stub, monkeypatch, qversion, enabled, expected): - monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion) - config_stub.val.colors.webpage.prefers_color_scheme_dark = enabled - assert list(darkmode.settings()) == expected +@testutils.qt514 +def test_colorscheme(config_stub, value, webengine_version, expected): + versions = version.WebEngineVersions.from_pyqt(webengine_version) + if value is not None: + config_stub.val.colors.webpage.preferred_color_scheme = value + + darkmode_settings = darkmode.settings(versions=versions, special_flags=[]) + assert darkmode_settings['blink-settings'] == expected + + +@testutils.qt514 +def test_colorscheme_gentoo_workaround(config_stub, gentoo_versions): + config_stub.val.colors.webpage.preferred_color_scheme = "dark" + darkmode_settings = darkmode.settings(versions=gentoo_versions, special_flags=[]) + assert darkmode_settings['blink-settings'] == [("preferredColorScheme", "0")] @pytest.mark.parametrize('settings, expected', [ @@ -57,57 +96,69 @@ def test_colorscheme(config_stub, monkeypatch, qversion, enabled, expected): ({}, []), # Enabled without customization - ({'enabled': True}, [('forceDarkModeEnabled', 'true')]), + ({'enabled': True}, [('darkModeEnabled', 'true')]), # Algorithm ( {'enabled': True, 'algorithm': 'brightness-rgb'}, [ - ('forceDarkModeEnabled', 'true'), - ('forceDarkModeInversionAlgorithm', '2') + ('darkModeEnabled', 'true'), + ('darkModeInversionAlgorithm', '2') ], ), ]) -def test_basics(config_stub, monkeypatch, settings, expected): +def test_basics(config_stub, settings, expected): for k, v in settings.items(): config_stub.set_obj('colors.webpage.darkmode.' + k, v) - monkeypatch.setattr(darkmode, '_variant', - lambda: darkmode.Variant.qt_515_2) if expected: - expected.append(('forceDarkModeImagePolicy', '2')) + expected.append(('darkModeImagePolicy', '2')) - assert list(darkmode.settings()) == expected + # Using Qt 5.15.1 because it has the least special cases. + versions = version.WebEngineVersions.from_pyqt('5.15.1') + darkmode_settings = darkmode.settings(versions=versions, special_flags=[]) + assert darkmode_settings['blink-settings'] == expected -QT_514_SETTINGS = [ +QT_514_SETTINGS = {'blink-settings': [ ('darkMode', '2'), ('darkModeImagePolicy', '2'), ('darkModeGrayscale', 'true'), -] +]} -QT_515_0_SETTINGS = [ +QT_515_0_SETTINGS = {'blink-settings': [ ('darkModeEnabled', 'true'), ('darkModeInversionAlgorithm', '2'), ('darkModeGrayscale', 'true'), -] +]} -QT_515_1_SETTINGS = [ +QT_515_1_SETTINGS = {'blink-settings': [ ('darkModeEnabled', 'true'), ('darkModeInversionAlgorithm', '2'), ('darkModeImagePolicy', '2'), ('darkModeGrayscale', 'true'), -] +]} -QT_515_2_SETTINGS = [ +QT_515_2_SETTINGS = {'blink-settings': [ + ('preferredColorScheme', '2'), # QTBUG-89753 ('forceDarkModeEnabled', 'true'), ('forceDarkModeInversionAlgorithm', '2'), ('forceDarkModeImagePolicy', '2'), ('forceDarkModeGrayscale', 'true'), -] +]} + + +QT_515_3_SETTINGS = { + 'blink-settings': [('forceDarkModeEnabled', 'true')], + 'dark-mode-settings': [ + ('InversionAlgorithm', '1'), + ('ImagePolicy', '2'), + ('IsGrayScale', 'true'), + ], +} @pytest.mark.parametrize('qversion, expected', [ @@ -119,16 +170,9 @@ QT_515_2_SETTINGS = [ ('5.15.1', QT_515_1_SETTINGS), ('5.15.2', QT_515_2_SETTINGS), + ('5.15.3', QT_515_3_SETTINGS), ]) -def test_qt_version_differences(config_stub, monkeypatch, qversion, expected): - monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion) - - major, minor, patch = [int(part) for part in qversion.split('.')] - hexversion = major << 16 | minor << 8 | patch - if major > 5 or minor >= 13: - # Added in Qt 5.13 - monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', hexversion) - +def test_qt_version_differences(config_stub, qversion, expected): settings = { 'enabled': True, 'algorithm': 'brightness-rgb', @@ -137,10 +181,12 @@ def test_qt_version_differences(config_stub, monkeypatch, qversion, expected): for k, v in settings.items(): config_stub.set_obj('colors.webpage.darkmode.' + k, v) - assert list(darkmode.settings()) == expected + versions = version.WebEngineVersions.from_pyqt(qversion) + darkmode_settings = darkmode.settings(versions=versions, special_flags=[]) + assert darkmode_settings == expected -@utils.qt514 +@testutils.qt514 @pytest.mark.parametrize('setting, value, exp_key, exp_val', [ ('contrast', -0.5, 'Contrast', '-0.5'), @@ -157,36 +203,37 @@ def test_qt_version_differences(config_stub, monkeypatch, qversion, expected): ('grayscale.images', 0.5, 'ImageGrayscale', '0.5'), ]) -def test_customization(config_stub, monkeypatch, setting, value, exp_key, exp_val): +def test_customization(config_stub, setting, value, exp_key, exp_val): config_stub.val.colors.webpage.darkmode.enabled = True config_stub.set_obj('colors.webpage.darkmode.' + setting, value) - monkeypatch.setattr(darkmode, '_variant', lambda: darkmode.Variant.qt_515_2) expected = [] - expected.append(('forceDarkModeEnabled', 'true')) + expected.append(('darkModeEnabled', 'true')) if exp_key != 'ImagePolicy': - expected.append(('forceDarkModeImagePolicy', '2')) - expected.append(('forceDarkMode' + exp_key, exp_val)) - - assert list(darkmode.settings()) == expected - + expected.append(('darkModeImagePolicy', '2')) + expected.append(('darkMode' + exp_key, exp_val)) + + versions = version.WebEngineVersions.from_pyqt('5.15.1') + darkmode_settings = darkmode.settings(versions=versions, special_flags=[]) + assert darkmode_settings['blink-settings'] == expected + + +@pytest.mark.parametrize('webengine_version, expected', [ + ('5.13.0', darkmode.Variant.qt_511_to_513), + ('5.14.0', darkmode.Variant.qt_514), + ('5.15.0', darkmode.Variant.qt_515_0), + ('5.15.1', darkmode.Variant.qt_515_1), + ('5.15.2', darkmode.Variant.qt_515_2), + ('5.15.3', darkmode.Variant.qt_515_3), + ('6.0.0', darkmode.Variant.qt_515_3), +]) +def test_variant(webengine_version, expected): + versions = version.WebEngineVersions.from_pyqt(webengine_version) + assert darkmode._variant(versions) == expected -@pytest.mark.parametrize('qversion, webengine_version, expected', [ - # Without PYQT_WEBENGINE_VERSION - ('5.12.9', None, darkmode.Variant.qt_511_to_513), - # With PYQT_WEBENGINE_VERSION - (None, 0x050d00, darkmode.Variant.qt_511_to_513), - (None, 0x050e00, darkmode.Variant.qt_514), - (None, 0x050f00, darkmode.Variant.qt_515_0), - (None, 0x050f01, darkmode.Variant.qt_515_1), - (None, 0x050f02, darkmode.Variant.qt_515_2), - (None, 0x060000, darkmode.Variant.qt_515_2), # Qt 6 -]) -def test_variant(monkeypatch, qversion, webengine_version, expected): - monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion) - monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', webengine_version) - assert darkmode._variant() == expected +def test_variant_gentoo_workaround(gentoo_versions): + assert darkmode._variant(gentoo_versions) == darkmode.Variant.qt_515_3 @pytest.mark.parametrize('value, is_valid, expected', [ @@ -194,24 +241,23 @@ def test_variant(monkeypatch, qversion, webengine_version, expected): ('qt_515_2', True, darkmode.Variant.qt_515_2), ]) def test_variant_override(monkeypatch, caplog, value, is_valid, expected): - monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: None) - monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00) + versions = version.WebEngineVersions.from_pyqt('5.15.0') monkeypatch.setenv('QUTE_DARKMODE_VARIANT', value) with caplog.at_level(logging.WARNING): - assert darkmode._variant() == expected + assert darkmode._variant(versions) == expected log_msg = 'Ignoring invalid QUTE_DARKMODE_VARIANT=invalid_value' assert (log_msg in caplog.messages) != is_valid -def test_broken_smart_images_policy(config_stub, monkeypatch, caplog): +def test_broken_smart_images_policy(config_stub, caplog): config_stub.val.colors.webpage.darkmode.enabled = True config_stub.val.colors.webpage.darkmode.policy.images = 'smart' - monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00) + versions = version.WebEngineVersions.from_pyqt('5.15.0') with caplog.at_level(logging.WARNING): - settings = list(darkmode.settings()) + darkmode_settings = darkmode.settings(versions=versions, special_flags=[]) assert caplog.messages[-1] == ( 'Ignoring colors.webpage.darkmode.policy.images = smart because of ' @@ -221,28 +267,25 @@ def test_broken_smart_images_policy(config_stub, monkeypatch, caplog): [('darkModeEnabled', 'true')], # Qt 5.15 [('darkMode', '4')], # Qt 5.14 ] - assert settings in expected - - -def test_new_chromium(): - """Fail if we encounter an unknown Chromium version. - - Dark mode in Chromium (or rather, the underlying Blink) is being changed with - almost every Chromium release. - - Make this test fail deliberately with newer Chromium versions, so that - we can test whether dark mode still works manually, and adjust if not. - """ - assert version._chromium_version() in [ - 'unavailable', # QtWebKit - '61.0.3163.140', # Qt 5.10 - '65.0.3325.230', # Qt 5.11 - '69.0.3497.128', # Qt 5.12 - '73.0.3683.105', # Qt 5.13 - '77.0.3865.129', # Qt 5.14 - '80.0.3987.163', # Qt 5.15.0 - '83.0.4103.122', # Qt 5.15.2 + assert darkmode_settings['blink-settings'] in expected + + +@pytest.mark.parametrize('flag, expected', [ + ('--blink-settings=key=value', [('key', 'value')]), + ('--blink-settings=key=equal=rights', [('key', 'equal=rights')]), + ('--blink-settings=one=1,two=2', [('one', '1'), ('two', '2')]), + ('--enable-features=feat', []), +]) +def test_pass_through_existing_settings(config_stub, flag, expected): + config_stub.val.colors.webpage.darkmode.enabled = True + versions = version.WebEngineVersions.from_pyqt('5.15.1') + settings = darkmode.settings(versions=versions, special_flags=[flag]) + + dark_mode_expected = [ + ('darkModeEnabled', 'true'), + ('darkModeImagePolicy', '2'), ] + assert settings['blink-settings'] == expected + dark_mode_expected def test_options(configdata_init): diff --git a/tests/unit/browser/webengine/test_webenginedownloads.py b/tests/unit/browser/webengine/test_webenginedownloads.py index f3fb13f1b..877af3c9a 100644 --- a/tests/unit/browser/webengine/test_webenginedownloads.py +++ b/tests/unit/browser/webengine/test_webenginedownloads.py @@ -19,12 +19,13 @@ import os.path import base64 +import dataclasses import pytest pytest.importorskip('PyQt5.QtWebEngineWidgets') from PyQt5.QtWebEngineWidgets import QWebEngineProfile -from qutebrowser.utils import urlutils, usertypes +from qutebrowser.utils import urlutils, usertypes, utils from qutebrowser.browser.webengine import webenginedownloads @@ -41,6 +42,15 @@ def test_strip_suffix(path, expected): assert webenginedownloads._strip_suffix(path) == expected +@dataclasses.dataclass +class _ExpectedNames: + + """The filenames used in the tests.""" + + before: str + after: str + + class TestDataUrlWorkaround: """With data URLs, we get rather weird base64 filenames back from QtWebEngine. @@ -71,6 +81,28 @@ class TestDataUrlWorkaround: return urlutils.data_url('application/pdf', pdf_bytes) @pytest.fixture + def expected_names(self, webengine_versions, pdf_bytes): + """Get the expected filenames before/after the workaround. + + With QtWebEngine 5.15.3, this is handled correctly inside QtWebEngine + and we get a qwe_download.pdf instead. + """ + if webengine_versions.webengine >= utils.VersionNumber(5, 15, 3): + return _ExpectedNames(before='qwe_download.pdf', after='qwe_download.pdf') + + with_slash = b'% ?' in pdf_bytes + base64_data = base64.b64encode(pdf_bytes).decode('ascii') + + if with_slash: + assert '/' in base64_data + before = base64_data.split('/')[1] + else: + assert '/' not in base64_data + before = 'pdf' # from the mimetype + + return _ExpectedNames(before=before, after='download.pdf') + + @pytest.fixture def webengine_profile(self, qapp): profile = QWebEngineProfile.defaultProfile() profile.setParent(qapp) @@ -86,13 +118,13 @@ class TestDataUrlWorkaround: webengine_profile.downloadRequested.disconnect() def test_workaround(self, webengine_tab, message_mock, qtbot, - pdf_url, download_manager): + pdf_url, download_manager, expected_names): """Verify our workaround works properly.""" - with qtbot.waitSignal(message_mock.got_question): + with qtbot.wait_signal(message_mock.got_question): webengine_tab.load_url(pdf_url) question = message_mock.get_question() - assert question.default == 'download.pdf' + assert question.default == expected_names.after def test_explicit_filename(self, webengine_tab, message_mock, qtbot, pdf_url, download_manager): @@ -100,10 +132,10 @@ class TestDataUrlWorkaround: pdf_url_str = pdf_url.toDisplayString() html = f'<a href="{pdf_url_str}" download="filename.pdf" id="link">' - with qtbot.waitSignal(webengine_tab.load_finished): + with qtbot.wait_signal(webengine_tab.load_finished): webengine_tab.set_html(html) - with qtbot.waitSignal(message_mock.got_question): + with qtbot.wait_signal(message_mock.got_question): webengine_tab.elements.find_id( "link", lambda elem: elem.click(usertypes.ClickTarget.normal), @@ -112,20 +144,8 @@ class TestDataUrlWorkaround: question = message_mock.get_question() assert question.default == 'filename.pdf' - @pytest.fixture - def expected_wrong_filename(self, pdf_bytes): - with_slash = b'% ?' in pdf_bytes - base64_data = base64.b64encode(pdf_bytes).decode('ascii') - - if with_slash: - assert '/' in base64_data - return base64_data.split('/')[1] - else: - assert '/' not in base64_data - return 'pdf' # from the mimetype - def test_workaround_needed(self, qtbot, webengineview, - pdf_url, expected_wrong_filename, webengine_profile): + pdf_url, expected_names, webengine_profile): """Verify that our workaround for this is still needed. In other words, check whether we get those base64-filenames rather than a @@ -134,9 +154,9 @@ class TestDataUrlWorkaround: def check_item(item): assert item.mimeType() == 'application/pdf' assert item.url().scheme() == 'data' - assert os.path.basename(item.path()) == expected_wrong_filename + assert os.path.basename(item.path()) == expected_names.before return True - with qtbot.waitSignal(webengine_profile.downloadRequested, + with qtbot.wait_signal(webengine_profile.downloadRequested, check_params_cb=check_item): webengineview.load(pdf_url) diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py index 51ab4472c..12cacb5a2 100644 --- a/tests/unit/browser/webkit/network/test_filescheme.py +++ b/tests/unit/browser/webkit/network/test_filescheme.py @@ -29,7 +29,7 @@ from PyQt5.QtNetwork import QNetworkRequest from qutebrowser.browser.webkit.network import filescheme from qutebrowser.utils import urlutils, utils -from helpers import utils as testutils +from helpers import testutils @pytest.mark.parametrize('create_file, create_dir, filterfunc, expected', [ diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py index b01fe6c5b..3cffb2fd7 100644 --- a/tests/unit/browser/webkit/network/test_networkreply.py +++ b/tests/unit/browser/webkit/network/test_networkreply.py @@ -52,7 +52,7 @@ class TestFixedDataNetworkReply: b'Hello World! This is a test.']) def test_data(self, qtbot, req, data): reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo') - with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead, + with qtbot.wait_signals([reply.metaDataChanged, reply.readyRead, reply.finished], order='strict'): pass @@ -78,7 +78,7 @@ def test_error_network_reply(qtbot, req): reply = networkreply.ErrorNetworkReply( req, "This is an error", QNetworkReply.UnknownNetworkError) - with qtbot.waitSignals([reply.error, reply.finished], order='strict'): + with qtbot.wait_signals([reply.error, reply.finished], order='strict'): pass reply.abort() # shouldn't do anything diff --git a/tests/unit/browser/webkit/test_cookies.py b/tests/unit/browser/webkit/test_cookies.py index 0c30c44df..81da561ce 100644 --- a/tests/unit/browser/webkit/test_cookies.py +++ b/tests/unit/browser/webkit/test_cookies.py @@ -85,7 +85,7 @@ class TestSetCookies: """Test setCookiesFromUrl with cookies enabled.""" config_stub.val.content.cookies.accept = 'all' - with qtbot.waitSignal(ram_jar.changed): + with qtbot.wait_signal(ram_jar.changed): assert ram_jar.setCookiesFromUrl([cookie], url) # assert the cookies are added correctly @@ -100,7 +100,7 @@ class TestSetCookies: """Test setCookiesFromUrl when cookies are not accepted.""" config_stub.val.content.cookies.accept = 'never' - with qtbot.assertNotEmitted(ram_jar.changed): + with qtbot.assert_not_emitted(ram_jar.changed): assert not ram_jar.setCookiesFromUrl([cookie], url) assert not ram_jar.cookiesForUrl(url) @@ -112,11 +112,11 @@ class TestSetCookies: org_url = QUrl('http://example.org/') - with qtbot.waitSignal(ram_jar.changed): + with qtbot.wait_signal(ram_jar.changed): assert ram_jar.setCookiesFromUrl([cookie], org_url) assert ram_jar.cookiesForUrl(org_url) - with qtbot.assertNotEmitted(ram_jar.changed): + with qtbot.assert_not_emitted(ram_jar.changed): assert not ram_jar.setCookiesFromUrl([cookie], url) assert not ram_jar.cookiesForUrl(url) @@ -175,7 +175,7 @@ def test_cookies_changed_emit(config_stub, fake_save_manager, monkeypatch.setattr(lineparser, 'LineParser', LineparserSaveStub) jar = cookies.CookieJar() - with qtbot.waitSignal(jar.changed): + with qtbot.wait_signal(jar.changed): config_stub.val.content.cookies.store = False diff --git a/tests/unit/commands/test_userscripts.py b/tests/unit/commands/test_userscripts.py index 6758bdd08..48bc31c32 100644 --- a/tests/unit/commands/test_userscripts.py +++ b/tests/unit/commands/test_userscripts.py @@ -44,7 +44,7 @@ class TestQtFIFOReader: def test_single_line(self, reader, qtbot): """Test QSocketNotifier with a single line of data.""" - with qtbot.waitSignal(reader.got_line) as blocker: + with qtbot.wait_signal(reader.got_line) as blocker: with open(reader._filepath, 'w', encoding='utf-8') as f: f.write('foobar\n') @@ -74,8 +74,8 @@ def test_command(qtbot, py_proc, runner): with open(os.environ['QUTE_FIFO'], 'w') as f: f.write('foo\n') """) - with qtbot.waitSignal(runner.finished, timeout=10000): - with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: + with qtbot.wait_signal(runner.finished, timeout=10000): + with qtbot.wait_signal(runner.got_cmd, timeout=10000) as blocker: runner.prepare_run(cmd, *args) runner.store_html('') runner.store_text('') @@ -97,8 +97,8 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner): f.write('\n') """) - with qtbot.waitSignal(runner.finished, timeout=10000): - with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: + with qtbot.wait_signal(runner.finished, timeout=10000): + with qtbot.wait_signal(runner.got_cmd, timeout=10000) as blocker: runner.prepare_run(cmd, *args, env=env) runner.store_html('') runner.store_text('') @@ -131,8 +131,8 @@ def test_source(qtbot, py_proc, runner): f.write('\n') """) - with qtbot.waitSignal(runner.finished, timeout=10000): - with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: + with qtbot.wait_signal(runner.finished, timeout=10000): + with qtbot.wait_signal(runner.got_cmd, timeout=10000) as blocker: runner.prepare_run(cmd, *args) runner.store_html('This is HTML') runner.store_text('This is text') @@ -158,8 +158,8 @@ def test_command_with_error(qtbot, py_proc, runner, caplog): """) with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(runner.finished, timeout=10000): - with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: + with qtbot.wait_signal(runner.finished, timeout=10000): + with qtbot.wait_signal(runner.got_cmd, timeout=10000) as blocker: runner.prepare_run(cmd, *args) runner.store_text('Hello World') runner.store_html('') @@ -195,7 +195,7 @@ def test_killed_command(qtbot, tmpdir, py_proc, runner, caplog): """) args.append(str(data_file)) - with qtbot.waitSignal(watcher.directoryChanged, timeout=10000): + with qtbot.wait_signal(watcher.directoryChanged, timeout=10000): runner.prepare_run(cmd, *args) runner.store_text('Hello World') runner.store_html('') @@ -206,7 +206,7 @@ def test_killed_command(qtbot, tmpdir, py_proc, runner, caplog): data = json.load(data_file) with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(runner.finished): + with qtbot.wait_signal(runner.finished): os.kill(int(data['pid']), signal.SIGTERM) assert not os.path.exists(data['text_file']) @@ -220,7 +220,7 @@ def test_temporary_files_failed_cleanup(caplog, qtbot, py_proc, runner): """) with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(runner.finished, timeout=10000): + with qtbot.wait_signal(runner.finished, timeout=10000): runner.prepare_run(cmd, *args) runner.store_text('') runner.store_html('') @@ -237,7 +237,7 @@ def test_unicode_error(caplog, qtbot, py_proc, runner): f.write(b'\x80') """) with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(runner.finished, timeout=10000): + with qtbot.wait_signal(runner.finished, timeout=10000): runner.prepare_run(cmd, *args) runner.store_text('') runner.store_html('') diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 5b2e519ea..ad081ccbf 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -92,7 +92,7 @@ def test_highlighted(qtbot): # Needed so the highlighting actually works. edit = QTextEdit() - qtbot.addWidget(edit) + qtbot.add_widget(edit) edit.setDocument(doc) colors = [f.foreground().color() for f in doc.allFormats()] diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py index e2d55082e..2130f1f1c 100644 --- a/tests/unit/completion/test_completionmodel.py +++ b/tests/unit/completion/test_completionmodel.py @@ -77,7 +77,7 @@ def test_set_pattern(pat, qtbot): for c in cats: c.set_pattern = mock.Mock(spec=[]) model.add_category(c) - with qtbot.waitSignals([model.layoutAboutToBeChanged, model.layoutChanged], + with qtbot.wait_signals([model.layoutAboutToBeChanged, model.layoutChanged], order='strict'): model.set_pattern(pat) for c in cats: diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 494531ff4..89390cbf1 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -39,7 +39,7 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry, 'qutebrowser.completion.completiondelegate.CompletionItemDelegate', new=lambda *_: None) view = completionwidget.CompletionView(cmd=status_command_stub, win_id=0) - qtbot.addWidget(view) + qtbot.add_widget(view) return view @@ -73,10 +73,10 @@ def test_set_pattern_no_model(completionview): def test_maybe_update_geometry(completionview, config_stub, qtbot): """Ensure completion is resized only if shrink is True.""" - with qtbot.assertNotEmitted(completionview.update_geometry): + with qtbot.assert_not_emitted(completionview.update_geometry): completionview._maybe_update_geometry() config_stub.val.completion.shrink = True - with qtbot.waitSignal(completionview.update_geometry): + with qtbot.wait_signal(completionview.update_geometry): completionview._maybe_update_geometry() @@ -137,10 +137,10 @@ def test_completion_item_focus(which, tree, expected, completionview, model, qtb completionview.set_model(model) for entry in expected: if entry is None: - with qtbot.assertNotEmitted(completionview.selection_changed): + with qtbot.assert_not_emitted(completionview.selection_changed): completionview.completion_item_focus(which) else: - with qtbot.waitSignal(completionview.selection_changed) as sig: + with qtbot.wait_signal(completionview.selection_changed) as sig: completionview.completion_item_focus(which) assert sig.args == [entry] @@ -153,11 +153,11 @@ def test_completion_item_focus_no_model(which, completionview, model, qtbot): Validates #1812: help completion repeatedly completes """ - with qtbot.assertNotEmitted(completionview.selection_changed): + with qtbot.assert_not_emitted(completionview.selection_changed): completionview.completion_item_focus(which) completionview.set_model(model) completionview.set_model(None) - with qtbot.assertNotEmitted(completionview.selection_changed): + with qtbot.assert_not_emitted(completionview.selection_changed): completionview.completion_item_focus(which) @@ -214,7 +214,7 @@ class TestCompletionItemFocusPage: cat = listcategory.ListCategory('Test', items) model.add_category(cat) completionview.set_model(model) - with qtbot.waitSignal(completionview.selection_changed) as blocker: + with qtbot.wait_signal(completionview.selection_changed) as blocker: completionview.completion_item_focus(which) assert blocker.args == [expected] @@ -259,7 +259,7 @@ class TestCompletionItemFocusPage: for move, item in steps: print('{:9} -> expecting {}'.format(move, item)) - with qtbot.waitSignal(completionview.selection_changed) as blocker: + with qtbot.wait_signal(completionview.selection_changed) as blocker: completionview.completion_item_focus(move) assert blocker.args == [item] @@ -273,7 +273,7 @@ class TestCompletionItemFocusPage: completionview.set_model(model) for move, item in [('next', 'Item 1'), ('next-page', 'Target item')]: - with qtbot.waitSignal(completionview.selection_changed) as blocker: + with qtbot.wait_signal(completionview.selection_changed) as blocker: completionview.completion_item_focus(move) assert blocker.args == [item] diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 8a6b24557..22e9c6490 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -28,7 +28,7 @@ from datetime import datetime from unittest import mock import hypothesis -import hypothesis.strategies +import hypothesis.strategies as hst import pytest from PyQt5.QtCore import QUrl, QDateTime try: @@ -459,9 +459,10 @@ def test_filesystem_completion_model_interface(info, local_files_path): @hypothesis.given( - as_uri=hypothesis.strategies.booleans(), - add_sep=hypothesis.strategies.booleans(), - text=hypothesis.strategies.text(), + as_uri=hst.booleans(), + add_sep=hst.booleans(), + text=hst.text(alphabet=hst.characters( + blacklist_categories=['Cc'], blacklist_characters='\x00')), ) def test_filesystem_completion_hypothesis(info, as_uri, add_sep, text): if as_uri: @@ -1445,7 +1446,7 @@ def undo_completion_retains_sort_order(tabbed_browser_stubs, info): _check_completions(model, {"Closed tabs": expected}) -@hypothesis.given(text=hypothesis.strategies.text()) +@hypothesis.given(text=hst.text()) def test_listcategory_hypothesis(text): """Make sure we can't produce invalid patterns.""" cat = listcategory.ListCategory("test", []) diff --git a/tests/unit/components/test_blockutils.py b/tests/unit/components/test_blockutils.py index 205dd7fc1..f206f3b6a 100644 --- a/tests/unit/components/test_blockutils.py +++ b/tests/unit/components/test_blockutils.py @@ -76,7 +76,7 @@ def test_blocklist_dl(qtbot, pretend_blocklists): dl = blockutils.BlocklistDownloads(list_qurls) dl.single_download_finished.connect(on_single_download) - with qtbot.waitSignal(dl.all_downloads_finished) as blocker: + with qtbot.wait_signal(dl.all_downloads_finished) as blocker: dl.initiate() assert blocker.args == [total_expected] diff --git a/tests/unit/components/test_braveadblock.py b/tests/unit/components/test_braveadblock.py index 0afb7499e..3b99fbd38 100644 --- a/tests/unit/components/test_braveadblock.py +++ b/tests/unit/components/test_braveadblock.py @@ -30,7 +30,7 @@ import pytest from qutebrowser.api.interceptor import ResourceType from qutebrowser.components import braveadblock from qutebrowser.components.utils import blockutils -from helpers import utils +from helpers import testutils pytestmark = pytest.mark.usefixtures("qapp") @@ -107,7 +107,7 @@ def run_function_on_dataset(given_function): contains tuples of (url, source_url, type) in each line. We give these to values to the given function, row by row. """ - dataset = utils.adblock_dataset_tsv() + dataset = testutils.adblock_dataset_tsv() reader = csv.DictReader(dataset, delimiter="\t") for row in reader: url = QUrl(row["url"]) @@ -144,8 +144,8 @@ def easylist_easyprivacy_both(tmpdir): bl_dst_dir.mkdir() urls = [] for blocklist, filename in [ - (utils.easylist_txt(), "easylist.txt"), - (utils.easyprivacy_txt(), "easyprivacy.txt"), + (testutils.easylist_txt(), "easylist.txt"), + (testutils.easyprivacy_txt(), "easyprivacy.txt"), ]: bl_dst_path = bl_dst_dir / filename with open(bl_dst_path, "w", encoding="utf-8") as f: diff --git a/tests/unit/components/test_hostblock.py b/tests/unit/components/test_hostblock.py index 3869aba66..876be1c53 100644 --- a/tests/unit/components/test_hostblock.py +++ b/tests/unit/components/test_hostblock.py @@ -29,7 +29,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.components import hostblock from qutebrowser.utils import urlmatch -from helpers import utils +from helpers import testutils pytestmark = pytest.mark.usefixtures("qapp") @@ -557,7 +557,7 @@ def test_add_directory(config_stub, tmpdir, host_blocker_factory): def test_adblock_benchmark(data_tmpdir, benchmark, host_blocker_factory): blocked_hosts = data_tmpdir / "blocked-hosts" - blocked_hosts.write_text("\n".join(utils.blocked_hosts()), encoding="utf-8") + blocked_hosts.write_text("\n".join(testutils.blocked_hosts()), encoding="utf-8") url = QUrl("https://www.example.org/") blocker = host_blocker_factory() diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 7f35af024..8a9d8154d 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -432,7 +432,7 @@ class TestConfig: assert conf.get_obj(name1) == 'never' assert conf.get_obj(name2) is True - with qtbot.waitSignals([conf.changed, conf.changed]) as blocker: + with qtbot.wait_signals([conf.changed, conf.changed]) as blocker: conf.clear(save_yaml=save_yaml) options = {e.args[0] for e in blocker.all_signals_and_args} diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index fdd12d308..255ea8acc 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -617,6 +617,24 @@ class TestYamlMigrations: def test_title_format(self, migration_test, setting, old, new): migration_test(setting, old, new) + @pytest.mark.parametrize('setting', [ + 'colors.webpage.force_dark_color_scheme', + 'colors.webpage.prefers_color_scheme_dark', + ]) + @pytest.mark.parametrize('old, new', [ + (True, 'dark'), + (False, 'auto'), + ]) + def test_preferred_color_scheme(self, autoconfig, yaml, setting, old, new): + autoconfig.write({setting: {'global': old}}) + + yaml.load() + yaml._save() + + data = autoconfig.read() + assert setting not in data + assert data['colors.webpage.preferred_color_scheme']['global'] == new + @pytest.mark.parametrize('old, new', [ (None, ('Mozilla/5.0 ({os_info}) ' 'AppleWebKit/{webkit_version} (KHTML, like Gecko) ' @@ -729,22 +747,28 @@ class ConfPy: def __init__(self, tmpdir, filename: str = "config.py"): self._file = tmpdir / filename self.filename = str(self._file) - config.instance.warn_autoconfig = False def write(self, *lines): text = '\n'.join(lines) self._file.write_text(text, 'utf-8', ensure=True) - def read(self, error=False): + def read(self, error=False, warn_autoconfig=False): """Read the config.py via configfiles and check for errors.""" if error: with pytest.raises(configexc.ConfigFileErrors) as excinfo: - configfiles.read_config_py(self.filename) + configfiles.read_config_py( + self.filename, + warn_autoconfig=warn_autoconfig, + ) errors = excinfo.value.errors assert len(errors) == 1 return errors[0] else: - configfiles.read_config_py(self.filename, raising=True) + configfiles.read_config_py( + self.filename, + raising=True, + warn_autoconfig=warn_autoconfig, + ) return None def write_qbmodule(self): @@ -1025,9 +1049,8 @@ class TestConfigPy: def test_load_autoconfig_warning(self, confpy): confpy.write('') - config.instance.warn_autoconfig = True with pytest.raises(configexc.ConfigFileErrors) as excinfo: - configfiles.read_config_py(confpy.filename) + configfiles.read_config_py(confpy.filename, warn_autoconfig=True) assert len(excinfo.value.errors) == 1 error = excinfo.value.errors[0] assert error.text == "autoconfig loading not specified" @@ -1194,6 +1217,21 @@ class TestConfigPy: assert error.text == "Error while reading doesnotexist.py" assert isinstance(error.exception, FileNotFoundError) + @pytest.mark.parametrize('reverse', [True, False]) + def test_source_warn_autoconfig(self, tmpdir, confpy, reverse): + subfile = tmpdir / 'config' / 'subfile.py' + subfile.write_text("c.content.javascript.enabled = False", + encoding='utf-8') + lines = [ + "config.source('subfile.py')", + "config.load_autoconfig(False)", + ] + if reverse: + lines.reverse() + + confpy.write(*lines) + confpy.read(warn_autoconfig=True) + class TestConfigPyWriter: diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 4a7788874..1945f7f8a 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -204,6 +204,19 @@ class TestEarlyInit: assert dump == '\n'.join(expected) + def test_autoconfig_warning(self, init_patch, args, config_tmpdir, caplog): + """Test the warning shown for missing autoconfig loading.""" + config_py_file = config_tmpdir / 'config.py' + config_py_file.ensure() + + with caplog.at_level(logging.ERROR): + configinit.early_init(args) + + # Check error messages + assert len(configinit._init_errors.errors) == 1 + error = configinit._init_errors.errors[0] + assert str(error).startswith("autoconfig loading not specified") + @pytest.mark.parametrize('byte', [ b'\x00', # configparser.Error b'\xda', # UnicodeDecodeError diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 493cf0ace..1a0a9cb43 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -39,7 +39,7 @@ from qutebrowser.config import configtypes, configexc from qutebrowser.utils import debug, utils, qtutils, urlmatch, usertypes from qutebrowser.browser.network import pac from qutebrowser.keyinput import keyutils -from helpers import utils as testutils +from helpers import testutils class Font(QFont): diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py index f7cefec31..4d3082a92 100644 --- a/tests/unit/config/test_configutils.py +++ b/tests/unit/config/test_configutils.py @@ -25,7 +25,7 @@ from PyQt5.QtWidgets import QLabel from qutebrowser.config import configutils, configdata, configtypes, configexc from qutebrowser.utils import urlmatch, usertypes, qtutils -from tests.helpers import utils +from tests.helpers import testutils @pytest.fixture @@ -277,7 +277,7 @@ def test_no_pattern_support(func, opt, pattern): def test_add_url_benchmark(values, benchmark): - blocked_hosts = list(utils.blocked_hosts()) + blocked_hosts = list(testutils.blocked_hosts()) def _add_blocked(): for line in blocked_hosts: @@ -294,7 +294,7 @@ def test_add_url_benchmark(values, benchmark): def test_domain_lookup_sparse_benchmark(url, values, benchmark): url = QUrl(url) values.add(False, urlmatch.UrlPattern("*.foo.bar.baz")) - for line in utils.blocked_hosts(): + for line in testutils.blocked_hosts(): values.add(False, urlmatch.UrlPattern(line)) benchmark(lambda: values.get_for_url(url)) @@ -382,7 +382,7 @@ class TestFontFamilies: print(stylesheet) label.setStyleSheet(stylesheet) - with qtbot.waitExposed(label): + with qtbot.wait_exposed(label): # Needed so the font gets calculated label.show() info = label.fontInfo() @@ -396,7 +396,7 @@ class TestFontFamilies: qtbot.add_widget(label) fallback_label.setText("fallback") - with qtbot.waitExposed(fallback_label): + with qtbot.wait_exposed(fallback_label): # Needed so the font gets calculated fallback_label.show() diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index be27274e5..e7dbd5d95 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -24,27 +24,42 @@ import pytest from qutebrowser import qutebrowser from qutebrowser.config import qtargs -from qutebrowser.utils import usertypes -from helpers import utils +from qutebrowser.utils import usertypes, version +from helpers import testutils -class TestQtArgs: +@pytest.fixture +def parser(mocker): + """Fixture to provide an argparser. - @pytest.fixture - def parser(self, mocker): - """Fixture to provide an argparser. + Monkey-patches .exit() of the argparser so it doesn't exit on errors. + """ + parser = qutebrowser.get_argparser() + mocker.patch.object(parser, 'exit', side_effect=Exception) + return parser - Monkey-patches .exit() of the argparser so it doesn't exit on errors. - """ - parser = qutebrowser.get_argparser() - mocker.patch.object(parser, 'exit', side_effect=Exception) - return parser - @pytest.fixture(autouse=True) - def reduce_args(self, monkeypatch, config_stub): - """Make sure no --disable-shared-workers/referer argument get added.""" - monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: '5.15.0') - config_stub.val.content.headers.referer = 'always' +@pytest.fixture +def version_patcher(monkeypatch): + """Get a patching function to patch the QtWebEngine version.""" + def run(ver): + versions = version.WebEngineVersions.from_pyqt(ver) + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) + monkeypatch.setattr(version, 'qtwebengine_versions', + lambda avoid_init: versions) + + return run + + +@pytest.fixture +def reduce_args(config_stub, version_patcher): + """Make sure no --disable-shared-workers/referer argument get added.""" + version_patcher('5.15.0') + config_stub.val.content.headers.referer = 'always' + + +@pytest.mark.usefixtures('reduce_args') +class TestQtArgs: @pytest.mark.parametrize('args, expected', [ # No Qt arguments @@ -89,35 +104,65 @@ class TestQtArgs: for arg in ['--foo', '--bar']: assert arg in args - @pytest.mark.parametrize('backend, expected', [ - (usertypes.Backend.QtWebEngine, True), - (usertypes.Backend.QtWebKit, False), + +def test_no_webengine_available(monkeypatch, config_stub, parser, stubs): + """Test that we don't fail if QtWebEngine is requested but unavailable. + + Note this is not inside TestQtArgs because we don't want the reduce_args patching + here. + """ + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) + monkeypatch.setattr(qtargs.version, 'webenginesettings', None) + + fake = stubs.ImportFake({'qutebrowser.browser.webengine': False}, monkeypatch) + fake.patch() + + parsed = parser.parse_args([]) + args = qtargs.qt_args(parsed) + assert args == [sys.argv[0]] + + +@pytest.mark.usefixtures('reduce_args') +class TestWebEngineArgs: + + @pytest.fixture(autouse=True) + def ensure_webengine(self): + """Skip all tests if QtWebEngine is unavailable.""" + pytest.importorskip("PyQt5.QtWebEngine") + + @pytest.mark.parametrize('backend, qt_version, expected', [ + (usertypes.Backend.QtWebEngine, '5.13.0', False), + (usertypes.Backend.QtWebEngine, '5.14.0', True), + (usertypes.Backend.QtWebEngine, '5.14.1', True), + (usertypes.Backend.QtWebEngine, '5.15.0', False), + (usertypes.Backend.QtWebEngine, '5.15.1', False), + + (usertypes.Backend.QtWebKit, '5.14.0', False), ]) - def test_shared_workers(self, config_stub, monkeypatch, parser, - backend, expected): - monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: '5.14.0') + def test_shared_workers(self, config_stub, version_patcher, monkeypatch, parser, + qt_version, backend, expected): + version_patcher(qt_version) monkeypatch.setattr(qtargs.objects, 'backend', backend) parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) assert ('--disable-shared-workers' in args) == expected - @pytest.mark.parametrize('backend, version_check, debug_flag, expected', [ + @pytest.mark.parametrize('backend, qt_version, debug_flag, expected', [ # Qt >= 5.12.3: Enable with -D stack, do nothing without it. - (usertypes.Backend.QtWebEngine, True, True, True), - (usertypes.Backend.QtWebEngine, True, False, None), + (usertypes.Backend.QtWebEngine, '5.12.3', True, True), + (usertypes.Backend.QtWebEngine, '5.12.3', False, None), # Qt < 5.12.3: Do nothing with -D stack, disable without it. - (usertypes.Backend.QtWebEngine, False, True, None), - (usertypes.Backend.QtWebEngine, False, False, False), + (usertypes.Backend.QtWebEngine, '5.12.2', True, None), + (usertypes.Backend.QtWebEngine, '5.12.2', False, False), # QtWebKit: Do nothing - (usertypes.Backend.QtWebKit, True, True, None), - (usertypes.Backend.QtWebKit, True, False, None), - (usertypes.Backend.QtWebKit, False, True, None), - (usertypes.Backend.QtWebKit, False, False, None), + (usertypes.Backend.QtWebKit, '5.12.3', True, None), + (usertypes.Backend.QtWebKit, '5.12.3', False, None), + (usertypes.Backend.QtWebKit, '5.12.2', True, None), + (usertypes.Backend.QtWebKit, '5.12.2', False, None), ]) - def test_in_process_stack_traces(self, monkeypatch, parser, backend, - version_check, debug_flag, expected): - monkeypatch.setattr(qtargs.qtutils, 'version_check', - lambda version, compiled=False, exact=False: version_check) + def test_in_process_stack_traces(self, monkeypatch, parser, backend, version_patcher, + qt_version, debug_flag, expected): + version_patcher(qt_version) monkeypatch.setattr(qtargs.objects, 'backend', backend) parsed = parser.parse_args(['--debug-flag', 'stack'] if debug_flag else []) @@ -139,8 +184,7 @@ class TestQtArgs: (['--debug-flag', 'wait-renderer-process'], ['--renderer-startup-dialog']), ]) def test_chromium_flags(self, monkeypatch, parser, flags, args): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) parsed = parser.parse_args(flags) args = qtargs.qt_args(parsed) @@ -158,10 +202,8 @@ class TestQtArgs: ('software-opengl', False), ('chromium', True), ]) - def test_disable_gpu(self, config, added, - config_stub, monkeypatch, parser): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + def test_disable_gpu(self, config, added, config_stub, monkeypatch, parser): + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.qt.force_software_rendering = config parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -182,10 +224,8 @@ class TestQtArgs: '--force-webrtc-ip-handling-policy=' 'disable_non_proxied_udp'), ]) - def test_webrtc(self, config_stub, monkeypatch, parser, - policy, arg): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + def test_webrtc(self, config_stub, monkeypatch, parser, policy, arg): + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.content.webrtc_ip_handling_policy = policy parsed = parser.parse_args([]) @@ -203,8 +243,7 @@ class TestQtArgs: ]) def test_canvas_reading(self, config_stub, monkeypatch, parser, canvas_reading, added): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.content.canvas_reading = canvas_reading parsed = parser.parse_args([]) @@ -218,8 +257,7 @@ class TestQtArgs: ]) def test_process_model(self, config_stub, monkeypatch, parser, process_model, added): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.qt.process_model = process_model parsed = parser.parse_args([]) @@ -240,8 +278,7 @@ class TestQtArgs: ]) def test_low_end_device_mode(self, config_stub, monkeypatch, parser, low_end_device_mode, arg): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.qt.low_end_device_mode = low_end_device_mode parsed = parser.parse_args([]) @@ -270,9 +307,10 @@ class TestQtArgs: ('5.14.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'), ('5.15.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'), ]) - def test_referer(self, config_stub, monkeypatch, parser, qt_version, referer, arg): + def test_referer(self, config_stub, monkeypatch, version_patcher, parser, + qt_version, referer, arg): monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) - monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version) + version_patcher(qt_version) # Avoid WebRTC pipewire feature monkeypatch.setattr(qtargs.utils, 'is_linux', False) @@ -290,27 +328,40 @@ class TestQtArgs: else: assert arg in args - @pytest.mark.parametrize('dark, qt_version, added', [ - (True, "5.13", False), # not supported - (True, "5.14", True), - (True, "5.15.0", True), - (True, "5.15.1", True), - (True, "5.15.2", False), # handled via blink setting - - (False, "5.13", False), - (False, "5.14", False), - (False, "5.15.0", False), - (False, "5.15.1", False), - (False, "5.15.2", False), + @pytest.mark.parametrize('value, qt_version, added', [ + ("dark", "5.13", False), # not supported + ("dark", "5.14", True), + ("dark", "5.15.0", True), + ("dark", "5.15.1", True), + # handled via blink setting + ("dark", "5.15.2", False), + ("dark", "5.15.3", False), + ("dark", "6.0.0", False), + + ("light", "5.13", False), + ("light", "5.14", False), + ("light", "5.15.0", False), + ("light", "5.15.1", False), + ("light", "5.15.2", False), + ("light", "5.15.2", False), + ("light", "5.15.3", False), + ("light", "6.0.0", False), + + ("auto", "5.13", False), + ("auto", "5.14", False), + ("auto", "5.15.0", False), + ("auto", "5.15.1", False), + ("auto", "5.15.2", False), + ("auto", "5.15.2", False), + ("auto", "5.15.3", False), + ("auto", "6.0.0", False), ]) - @utils.qt514 - def test_prefers_color_scheme_dark(self, config_stub, monkeypatch, parser, - dark, qt_version, added): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) - monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version) + @testutils.qt514 + def test_preferred_color_scheme( + self, config_stub, version_patcher, parser, value, qt_version, added): + version_patcher(qt_version) - config_stub.val.colors.webpage.prefers_color_scheme_dark = dark + config_stub.val.colors.webpage.preferred_color_scheme = value parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -329,8 +380,7 @@ class TestQtArgs: ]) def test_overlay_scrollbar(self, config_stub, monkeypatch, parser, bar, is_mac, added): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) monkeypatch.setattr(qtargs.utils, 'is_mac', is_mac) # Avoid WebRTC pipewire feature monkeypatch.setattr(qtargs.utils, 'is_linux', False) @@ -343,13 +393,10 @@ class TestQtArgs: assert ('--enable-features=OverlayScrollbar' in args) == added @pytest.fixture - def feature_flag_patch(self, monkeypatch): + def feature_flag_patch(self, monkeypatch, config_stub, version_patcher): """Patch away things affecting feature flags.""" - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) - monkeypatch.setattr(qtargs.qtutils, 'version_check', - lambda version, exact=False, compiled=True: - True) + config_stub.val.scrolling.bar = 'never' + version_patcher('5.15.3') monkeypatch.setattr(qtargs.utils, 'is_mac', False) # Avoid WebRTC pipewire feature monkeypatch.setattr(qtargs.utils, 'is_linux', False) @@ -409,10 +456,21 @@ class TestQtArgs: arg for arg in args if arg.startswith(qtargs._DISABLE_FEATURES) ] - assert len(disable_features_args) == 1 - features = set(disable_features_args[0].split('=')[1].split(',')) - features -= {'InstalledApp'} - assert features == set(passed_features) + assert disable_features_args == [flag] + + def test_blink_settings_passthrough(self, parser, config_stub, feature_flag_patch): + config_stub.val.colors.webpage.darkmode.enabled = True + + flag = qtargs._BLINK_SETTINGS + 'foo=bar' + parsed = parser.parse_args(['--qt-flag', flag.lstrip('-')]) + args = qtargs.qt_args(parsed) + + blink_settings_args = [ + arg for arg in args + if arg.startswith(qtargs._BLINK_SETTINGS) + ] + assert len(blink_settings_args) == 1 + assert blink_settings_args[0].startswith('--blink-settings=foo=bar,') @pytest.mark.parametrize('qt_version, has_workaround', [ ('5.14.0', False), @@ -421,10 +479,8 @@ class TestQtArgs: ('5.15.3', False), ('6.0.0', False), ]) - def test_installedapp_workaround(self, parser, monkeypatch, qt_version, has_workaround): - monkeypatch.setattr(qtargs.qtutils, 'qVersion', lambda: qt_version) - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) + def test_installedapp_workaround(self, parser, version_patcher, qt_version, has_workaround): + version_patcher(qt_version) parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -436,22 +492,43 @@ class TestQtArgs: expected = ['--disable-features=InstalledApp'] if has_workaround else [] assert disable_features_args == expected - def test_blink_settings(self, config_stub, monkeypatch, parser): + @pytest.mark.parametrize('variant, expected', [ + ( + 'qt_515_1', + ['--blink-settings=darkModeEnabled=true,darkModeImagePolicy=2'], + ), + ( + 'qt_515_2', + [ + ( + '--blink-settings=preferredColorScheme=2,' + 'forceDarkModeEnabled=true,' + 'forceDarkModeImagePolicy=2' + ) + ], + ), + ( + 'qt_515_3', + [ + '--blink-settings=forceDarkModeEnabled=true', + '--dark-mode-settings=ImagePolicy=2', + ] + ), + ]) + def test_dark_mode_settings(self, config_stub, monkeypatch, parser, + variant, expected): from qutebrowser.browser.webengine import darkmode - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) - monkeypatch.setattr(darkmode, '_variant', - lambda: darkmode.Variant.qt_515_2) + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) + monkeypatch.setattr( + darkmode, '_variant', lambda _versions: darkmode.Variant[variant]) config_stub.val.colors.webpage.darkmode.enabled = True parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) - expected = ('--blink-settings=forceDarkModeEnabled=true,' - 'forceDarkModeImagePolicy=2') - - assert expected in args + for arg in expected: + assert arg in args class TestEnvVars: diff --git a/tests/unit/config/test_stylesheet.py b/tests/unit/config/test_stylesheet.py index 935280b01..4ffa107ed 100644 --- a/tests/unit/config/test_stylesheet.py +++ b/tests/unit/config/test_stylesheet.py @@ -28,7 +28,7 @@ class StyleObj(QObject): def __init__(self, stylesheet=None, parent=None): super().__init__(parent) if stylesheet is not None: - self.STYLESHEET = stylesheet # noqa: N801,N806 pylint: disable=invalid-name + self.STYLESHEET = stylesheet self.rendered_stylesheet = None def setStyleSheet(self, stylesheet): @@ -45,8 +45,9 @@ def test_get_stylesheet(config_stub): @pytest.mark.parametrize('delete', [True, False]) @pytest.mark.parametrize('stylesheet_param', [True, False]) @pytest.mark.parametrize('update', [True, False]) -def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot, - config_stub, caplog): +@pytest.mark.parametrize('changed_option', ['colors.hints.fg', 'colors.hints.bg']) +def test_set_register_stylesheet(delete, stylesheet_param, update, changed_option, + qtbot, config_stub, caplog): config_stub.val.colors.hints.fg = 'magenta' qss = "{{ conf.colors.hints.fg }}" @@ -63,10 +64,11 @@ def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot, assert obj.rendered_stylesheet == 'magenta' if delete: - with qtbot.waitSignal(obj.destroyed): + with qtbot.wait_signal(obj.destroyed): obj.deleteLater() - config_stub.val.colors.hints.fg = 'yellow' + config_stub.set_obj(changed_option, 'yellow') - expected = 'magenta' if delete or not update else 'yellow' + expected = ('magenta' if delete or not update or changed_option != 'colors.hints.fg' + else 'yellow') assert obj.rendered_stylesheet == expected diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 793ec25d7..47884687d 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -53,27 +53,28 @@ class JSTester: 'warning': 'error' } - def load(self, path, **kwargs): + def load(self, path, base_url=QUrl(), **kwargs): """Load and display the given jinja test data. Args: path: The path to the test file, relative to the javascript/ folder. + base_url: The url to pass to set_html. **kwargs: Passed to jinja's template.render(). """ template = self._jinja_env.get_template(path) try: - with self.qtbot.waitSignal(self.tab.load_finished, + with self.qtbot.wait_signal(self.tab.load_finished, timeout=2000) as blocker: - self.tab.set_html(template.render(**kwargs)) + self.tab.set_html(template.render(**kwargs), base_url=base_url) except self.qtbot.TimeoutError: # Sometimes this fails for some odd reason on macOS, let's just try # again. print("Trying to load page again...") - with self.qtbot.waitSignal(self.tab.load_finished, + with self.qtbot.wait_signal(self.tab.load_finished, timeout=2000) as blocker: - self.tab.set_html(template.render(**kwargs)) + self.tab.set_html(template.render(**kwargs), base_url=base_url) assert blocker.args == [True] @@ -94,7 +95,7 @@ class JSTester: url: The QUrl to load. force: Whether to force loading even if the file is invalid. """ - with self.qtbot.waitSignal(self.tab.load_finished, + with self.qtbot.wait_signal(self.tab.load_finished, timeout=2000) as blocker: self.tab.load_url(url) if not force: diff --git a/tests/unit/javascript/test_js_quirks.py b/tests/unit/javascript/test_js_quirks.py new file mode 100644 index 000000000..b906aa17c --- /dev/null +++ b/tests/unit/javascript/test_js_quirks.py @@ -0,0 +1,68 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. + +"""Tests for QtWebEngine JavaScript quirks. + +This tests JS functionality which is missing in older QtWebEngine releases, but we have +polyfills for. They should either pass because the polyfill is active, or pass because +the native functionality exists. +""" + +import pytest + +from PyQt5.QtCore import QUrl +from qutebrowser.utils import usertypes + + +@pytest.mark.parametrize('base_url, source, expected', [ + pytest.param( + QUrl(), + '"This is a test".replaceAll("test", "fest")', + "This is a fest", + id='replace-all', + ), + pytest.param( + QUrl(), + '"This is a test".replaceAll(/[tr]est/g, "fest")', + "This is a fest", + id='replace-all-regex', + ), + pytest.param( + QUrl(), + '"This is a [test[".replaceAll("[", "<")', + "This is a <test<", + id='replace-all-reserved-string', + ), + pytest.param( + QUrl('https://test.qutebrowser.org/test'), + 'typeof globalThis.setTimeout === "function"', + True, + id='global-this', + ), + pytest.param( + QUrl(), + 'Object.fromEntries([["0", "a"], ["1", "b"]])', + {'0': 'a', '1': 'b'}, + id='object-fromentries', + ), +]) +def test_js_quirks(js_tester_webengine, base_url, source, expected): + js_tester_webengine.tab._scripts._inject_site_specific_quirks() + js_tester_webengine.load('base.html', base_url=base_url) + js_tester_webengine.run(source, expected, world=usertypes.JsWorld.main) diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 42fa2aeed..30ee36301 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import Qt import pytest from qutebrowser.keyinput import basekeyparser, keyutils -from qutebrowser.utils import usertypes +from qutebrowser.utils import utils, usertypes # Alias because we need this a lot in here. @@ -119,9 +119,11 @@ def test_read_config(keyparser, key_config_stub, changed_mode, expected): class TestHandle: def test_valid_key(self, prompt_keyparser, handle_text): + modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier + infos = [ - keyutils.KeyInfo(Qt.Key_A, Qt.ControlModifier), - keyutils.KeyInfo(Qt.Key_X, Qt.ControlModifier), + keyutils.KeyInfo(Qt.Key_A, modifier), + keyutils.KeyInfo(Qt.Key_X, modifier), ] for info in infos: prompt_keyparser.handle(info.to_event()) @@ -131,9 +133,11 @@ class TestHandle: assert not prompt_keyparser._sequence def test_valid_key_count(self, prompt_keyparser): + modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier + infos = [ keyutils.KeyInfo(Qt.Key_5, Qt.NoModifier), - keyutils.KeyInfo(Qt.Key_A, Qt.ControlModifier), + keyutils.KeyInfo(Qt.Key_A, modifier), ] for info in infos: prompt_keyparser.handle(info.to_event()) @@ -308,7 +312,7 @@ class TestCount: def test_count_keystring_update(self, qtbot, handle_text, prompt_keyparser): """Make sure the keystring is updated correctly when entering count.""" - with qtbot.waitSignals([ + with qtbot.wait_signals([ prompt_keyparser.keystring_updated, prompt_keyparser.keystring_updated]) as blocker: handle_text(prompt_keyparser, Qt.Key_4, Qt.Key_2) @@ -331,7 +335,7 @@ def test_clear_keystring(qtbot, keyparser): """Test that the keystring is cleared and the signal is emitted.""" keyparser._sequence = keyseq('test') keyparser._count = '23' - with qtbot.waitSignal(keyparser.keystring_updated): + with qtbot.wait_signal(keyparser.keystring_updated): keyparser.clear_keystring() assert not keyparser._sequence assert not keyparser._count diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 6ac01ead5..8ab2ab147 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -28,6 +28,7 @@ from PyQt5.QtWidgets import QWidget from unit.keyinput import key_data from qutebrowser.keyinput import keyutils +from qutebrowser.utils import utils @pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute) @@ -421,10 +422,13 @@ class TestKeySequence: ('', Qt.Key_Colon, Qt.AltModifier | Qt.ShiftModifier, ':', '<Alt+Shift+:>'), - # Modifiers - ('', Qt.Key_A, Qt.ControlModifier, '', '<Ctrl+A>'), - ('', Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, '', '<Ctrl+Shift+A>'), - ('', Qt.Key_A, Qt.MetaModifier, '', '<Meta+A>'), + # Swapping Control/Meta on macOS + ('', Qt.Key_A, Qt.ControlModifier, '', + '<Meta+A>' if utils.is_mac else '<Ctrl+A>'), + ('', Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, '', + '<Meta+Shift+A>' if utils.is_mac else '<Ctrl+Shift+A>'), + ('', Qt.Key_A, Qt.MetaModifier, '', + '<Ctrl+A>' if utils.is_mac else '<Meta+A>'), # Handling of Backtab ('', Qt.Key_Backtab, Qt.NoModifier, '', '<Backtab>'), @@ -441,6 +445,27 @@ class TestKeySequence: new = seq.append_event(event) assert new == keyutils.KeySequence.parse(expected) + @pytest.mark.fake_os('mac') + @pytest.mark.parametrize('modifiers, expected', [ + (Qt.ControlModifier, + Qt.MetaModifier), + (Qt.MetaModifier, + Qt.ControlModifier), + (Qt.ControlModifier | Qt.MetaModifier, + Qt.ControlModifier | Qt.MetaModifier), + (Qt.ControlModifier | Qt.ShiftModifier, + Qt.MetaModifier | Qt.ShiftModifier), + (Qt.MetaModifier | Qt.ShiftModifier, + Qt.ControlModifier | Qt.ShiftModifier), + (Qt.ShiftModifier, Qt.ShiftModifier), + ]) + def test_fake_mac(self, modifiers, expected): + """Make sure Control/Meta are swapped with a simulated Mac.""" + seq = keyutils.KeySequence() + info = keyutils.KeyInfo(key=Qt.Key_A, modifiers=modifiers) + new = seq.append_event(info.to_event()) + assert new[0] == keyutils.KeyInfo(Qt.Key_A, expected) + @pytest.mark.parametrize('key', [Qt.Key_unknown, 0x0]) def test_append_event_invalid(self, key): seq = keyutils.KeySequence() diff --git a/tests/unit/mainwindow/statusbar/test_textbase.py b/tests/unit/mainwindow/statusbar/test_textbase.py index e77b11671..631c6ce44 100644 --- a/tests/unit/mainwindow/statusbar/test_textbase.py +++ b/tests/unit/mainwindow/statusbar/test_textbase.py @@ -64,7 +64,7 @@ def test_resize(qtbot): long_string = 'Hello world! ' * 20 label.setText(long_string) - with qtbot.waitExposed(label): + with qtbot.wait_exposed(label): label.show() text_1 = label._elided_text diff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py index 03b8c9879..82328ffea 100644 --- a/tests/unit/mainwindow/test_messageview.py +++ b/tests/unit/mainwindow/test_messageview.py @@ -37,14 +37,14 @@ def view(qtbot, config_stub): usertypes.MessageLevel.error]) @pytest.mark.flaky # on macOS def test_single_message(qtbot, view, level): - with qtbot.waitExposed(view, timeout=5000): + with qtbot.wait_exposed(view, timeout=5000): view.show_message(level, 'test') assert view._messages[0].isVisible() def test_message_hiding(qtbot, view): """Messages should be hidden after the timer times out.""" - with qtbot.waitSignal(view._clear_timer.timeout): + with qtbot.wait_signal(view._clear_timer.timeout): view.show_message(usertypes.MessageLevel.info, 'test') assert not view._messages @@ -61,7 +61,7 @@ def test_size_hint(view): def test_word_wrap(view, qtbot): """A long message should be wrapped.""" - with qtbot.waitSignal(view._clear_timer.timeout): + with qtbot.wait_signal(view._clear_timer.timeout): view.show_message(usertypes.MessageLevel.info, 'short') height1 = view.sizeHint().height() assert height1 > 0 @@ -88,7 +88,7 @@ def test_show_message_twice(view): def test_show_message_twice_after_first_disappears(qtbot, view): """Show the same message twice after the first is gone.""" - with qtbot.waitSignal(view._clear_timer.timeout): + with qtbot.wait_signal(view._clear_timer.timeout): view.show_message(usertypes.MessageLevel.info, 'test') # Just a sanity check assert not view._messages @@ -101,7 +101,7 @@ def test_changing_timer_with_messages_shown(qtbot, view, config_stub): """When we change messages.timeout, the timer should be restarted.""" config_stub.val.messages.timeout = 900000 # 15s view.show_message(usertypes.MessageLevel.info, 'test') - with qtbot.waitSignal(view._clear_timer.timeout): + with qtbot.wait_signal(view._clear_timer.timeout): config_stub.val.messages.timeout = 100 diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index 3f9e0292d..564ca1b38 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -36,7 +36,7 @@ class TestTabWidget: @pytest.fixture def widget(self, qtbot, monkeypatch, config_stub): w = tabwidget.TabWidget(0) - qtbot.addWidget(w) + qtbot.add_widget(w) monkeypatch.setattr(tabwidget.objects, 'backend', usertypes.Backend.QtWebKit) w.show() @@ -53,7 +53,7 @@ class TestTabWidget: tab = fake_web_tab() widget.addTab(tab, icon, 'foobar') - with qtbot.waitExposed(widget): + with qtbot.wait_exposed(widget): widget.show() # Sizing tests @@ -118,7 +118,7 @@ class TestTabWidget: for i in range(num_tabs): widget.addTab(fake_web_tab(), 'foobar' + str(i)) - with qtbot.waitExposed(widget): + with qtbot.wait_exposed(widget): widget.show() benchmark(widget.update_tab_titles) diff --git a/tests/unit/misc/test_autoupdate.py b/tests/unit/misc/test_autoupdate.py index f86b9b0ff..f7cf78248 100644 --- a/tests/unit/misc/test_autoupdate.py +++ b/tests/unit/misc/test_autoupdate.py @@ -64,8 +64,8 @@ def test_get_version_success(qtbot): http_stub = HTTPGetStub(success=True) client = autoupdate.PyPIVersionClient(client=http_stub) - with qtbot.assertNotEmitted(client.error): - with qtbot.waitSignal(client.success): + with qtbot.assert_not_emitted(client.error): + with qtbot.wait_signal(client.success): client.get_version('test') assert http_stub.url == QUrl(client.API_URL.format('test')) @@ -76,8 +76,8 @@ def test_get_version_error(qtbot): http_stub = HTTPGetStub(success=False) client = autoupdate.PyPIVersionClient(client=http_stub) - with qtbot.assertNotEmitted(client.success): - with qtbot.waitSignal(client.error): + with qtbot.assert_not_emitted(client.success): + with qtbot.wait_signal(client.error): client.get_version('test') @@ -88,6 +88,6 @@ def test_invalid_json(qtbot, json): client = autoupdate.PyPIVersionClient(client=http_stub) client.get_version('test') - with qtbot.assertNotEmitted(client.success): - with qtbot.waitSignal(client.error): + with qtbot.assert_not_emitted(client.success): + with qtbot.wait_signal(client.error): client.get_version('test') diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index d27c2f885..ee167e8bb 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -223,12 +223,13 @@ def _update_file(filename, contents): This might write the file multiple times, but different systems have different mtime's, so we can't be sure how long to wait otherwise. """ - old_mtime = new_mtime = os.stat(filename).st_mtime + file_path = pathlib.Path(filename) + old_mtime = new_mtime = file_path.stat().st_mtime while old_mtime == new_mtime: time.sleep(0.1) with open(filename, 'w', encoding='utf-8') as f: f.write(contents) - new_mtime = os.stat(filename).st_mtime + new_mtime = file_path.stat().st_mtime def test_modify_watch(qtbot): diff --git a/tests/unit/misc/test_elf.py b/tests/unit/misc/test_elf.py new file mode 100644 index 000000000..d5d5b0ac5 --- /dev/null +++ b/tests/unit/misc/test_elf.py @@ -0,0 +1,88 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2021 Florian Bruhin (The-Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. + +import io +import struct + +import pytest +import hypothesis +from hypothesis import strategies as hst + +from qutebrowser.misc import elf +from qutebrowser.utils import utils + + +@pytest.mark.parametrize('fmt, expected', [ + (elf.Ident._FORMAT, 0x10), + + (elf.Header._FORMATS[elf.Bitness.x64], 0x30), + (elf.Header._FORMATS[elf.Bitness.x32], 0x24), + + (elf.SectionHeader._FORMATS[elf.Bitness.x64], 0x40), + (elf.SectionHeader._FORMATS[elf.Bitness.x32], 0x28), +]) +def test_format_sizes(fmt, expected): + """Ensure the struct format have the expected sizes. + + See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + and https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#Section_header + """ + assert struct.calcsize(fmt) == expected + + +@pytest.mark.skipif(not utils.is_linux, reason="Needs Linux") +def test_result(qapp, caplog): + """Test the real result of ELF parsing. + + NOTE: If you're a distribution packager (or contributor) and see this test failing, + I'd like your help with making either the code or the test more reliable! The + underlying code is susceptible to changes in the environment, and while it's been + tested in various environments (Archlinux, Ubuntu), might break in yours. + + If that happens, please report a bug about it! + """ + pytest.importorskip('PyQt5.QtWebEngineCore') + + versions = elf.parse_webenginecore() + assert versions is not None + + # No failing mmap + assert len(caplog.messages) == 1 + assert caplog.messages[0].startswith('Got versions from ELF:') + + from qutebrowser.browser.webengine import webenginesettings + webenginesettings.init_user_agent() + ua = webenginesettings.parsed_user_agent + + assert ua.qt_version == versions.webengine + assert ua.upstream_browser_version == versions.chromium + + +@hypothesis.given(data=hst.builds( + lambda *a: b''.join(a), + hst.sampled_from([b'', b'\x7fELF', b'\x7fELF\x02\x01\x01']), + hst.binary(min_size=0x70), +)) +def test_hypothesis(data): + """Fuzz ELF parsing and make sure no crashes happen.""" + fobj = io.BytesIO(data) + try: + elf._parse_from_file(fobj) + except elf.ParseError as e: + print(e) diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index 7f6a11ddd..9e1b3916c 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -36,7 +36,7 @@ def proc(qtbot, caplog): yield p if p._proc.state() == QProcess.Running: with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(p.finished, timeout=10000, + with qtbot.wait_signal(p.finished, timeout=10000, raising=False) as blocker: p._proc.terminate() if not blocker.signal_triggered: @@ -54,7 +54,7 @@ def fake_proc(monkeypatch, stubs): def test_start(proc, qtbot, message_mock, py_proc): """Test simply starting a process.""" - with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): argv = py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) @@ -70,7 +70,7 @@ def test_start_verbose(proc, qtbot, message_mock, py_proc): """Test starting a process verbosely.""" proc.verbose = True - with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): argv = py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) @@ -99,7 +99,7 @@ def test_start_output_message(proc, qtbot, caplog, message_mock, py_proc, code.append("sys.exit(0)") with caplog.at_level(logging.ERROR, 'message'): - with qtbot.waitSignals([proc.started, proc.finished], + with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): argv = py_proc(';'.join(code)) @@ -149,7 +149,7 @@ def test_start_env(monkeypatch, qtbot, py_proc): sys.exit(0) """) - with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): proc.start(*argv) @@ -180,7 +180,7 @@ def test_start_detached_error(fake_proc, message_mock, caplog): def test_double_start(qtbot, proc, py_proc): """Test starting a GUIProcess twice.""" - with qtbot.waitSignal(proc.started, timeout=10000): + with qtbot.wait_signal(proc.started, timeout=10000): argv = py_proc("import time; time.sleep(10)") proc.start(*argv) with pytest.raises(ValueError): @@ -189,11 +189,11 @@ def test_double_start(qtbot, proc, py_proc): def test_double_start_finished(qtbot, proc, py_proc): """Test starting a GUIProcess twice (with the first call finished).""" - with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): argv = py_proc("import sys; sys.exit(0)") proc.start(*argv) - with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): argv = py_proc("import sys; sys.exit(0)") proc.start(*argv) @@ -222,7 +222,7 @@ def test_start_logging(fake_proc, caplog): def test_error(qtbot, proc, caplog, message_mock): """Test the process emitting an error.""" with caplog.at_level(logging.ERROR, 'message'): - with qtbot.waitSignal(proc.error, timeout=5000): + with qtbot.wait_signal(proc.error, timeout=5000): proc.start('this_does_not_exist_either', []) msg = message_mock.getmsg(usertypes.MessageLevel.error) @@ -231,7 +231,7 @@ def test_error(qtbot, proc, caplog, message_mock): def test_exit_unsuccessful(qtbot, proc, message_mock, py_proc, caplog): with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.wait_signal(proc.finished, timeout=10000): proc.start(*py_proc('import sys; sys.exit(1)')) msg = message_mock.getmsg(usertypes.MessageLevel.error) @@ -241,7 +241,7 @@ def test_exit_unsuccessful(qtbot, proc, message_mock, py_proc, caplog): def test_exit_crash(qtbot, proc, message_mock, py_proc, caplog): with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.wait_signal(proc.finished, timeout=10000): proc.start(*py_proc(""" import os, signal os.kill(os.getpid(), signal.SIGSEGV) @@ -259,7 +259,7 @@ def test_exit_crash(qtbot, proc, message_mock, py_proc, caplog): def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream): """When a process fails, its output should be logged.""" with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.wait_signal(proc.finished, timeout=10000): proc.start(*py_proc(""" import sys print("test", file=sys.{}) @@ -275,7 +275,7 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream): The test doesn't actually check the log as it'd fail because of the error logging. """ - with qtbot.waitSignal(proc.finished, timeout=10000): + with qtbot.wait_signal(proc.finished, timeout=10000): proc.start(*py_proc(""" import sys print("test", file=sys.{}) @@ -285,7 +285,7 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream): def test_stdout_not_decodable(proc, qtbot, message_mock, py_proc): """Test handling malformed utf-8 in stdout.""" - with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + with qtbot.wait_signals([proc.started, proc.finished], timeout=10000, order='strict'): argv = py_proc(r""" import sys diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 1ea70afe7..c19d0bc42 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -37,7 +37,7 @@ from PyQt5.QtTest import QSignalSpy import qutebrowser from qutebrowser.misc import ipc from qutebrowser.utils import standarddir, utils -from helpers import stubs, utils as testutils +from helpers import stubs, testutils pytestmark = pytest.mark.usefixtures('qapp') @@ -57,7 +57,7 @@ def ipc_server(qapp, qtbot): yield server if (server._socket is not None and server._socket.state() != QLocalSocket.UnconnectedState): - with qtbot.waitSignal(server._socket.disconnected, raising=False): + with qtbot.wait_signal(server._socket.disconnected, raising=False): server._socket.abort() try: server.shutdown() @@ -306,10 +306,10 @@ class TestListen: @pytest.mark.posix def test_permissions_posix(self, ipc_server): ipc_server.listen() - sockfile = ipc_server._server.fullServerName() - sockdir = pathlib.Path(sockfile).parent + sockfile_path = pathlib.Path(ipc_server._server.fullServerName()) + sockdir = sockfile_path.parent - file_stat = os.stat(sockfile) + file_stat = sockfile_path.stat() dir_stat = sockdir.stat() # pylint: disable=no-member,useless-suppression @@ -322,7 +322,7 @@ class TestListen: print('sockdir: {} / owner {} / mode {:o}'.format( sockdir, dir_stat.st_uid, dir_stat.st_mode)) print('sockfile: {} / owner {} / mode {:o}'.format( - sockfile, file_stat.st_uid, file_stat.st_mode)) + sockfile_path, file_stat.st_uid, file_stat.st_mode)) assert file_owner_ok or dir_owner_ok assert file_mode_ok or dir_mode_ok @@ -331,16 +331,18 @@ class TestListen: def test_atime_update(self, qtbot, ipc_server): ipc_server._atime_timer.setInterval(500) # We don't want to wait ipc_server.listen() - old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns - with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000): + sockfile_path = pathlib.Path(ipc_server._server.fullServerName()) + old_atime = sockfile_path.stat().st_atime_ns + + with qtbot.wait_signal(ipc_server._atime_timer.timeout, timeout=2000): pass # Make sure the timer is not singleShot - with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000): + with qtbot.wait_signal(ipc_server._atime_timer.timeout, timeout=2000): pass - new_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns + new_atime = sockfile_path.stat().st_atime_ns assert old_atime != new_atime @@ -366,7 +368,7 @@ class TestListen: sockfile.unlink() with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(ipc_server._atime_timer.timeout, + with qtbot.wait_signal(ipc_server._atime_timer.timeout, timeout=2000): pass @@ -443,7 +445,7 @@ class TestHandleConnection: ipc_server._server = FakeServer(socket) - with qtbot.waitSignal(ipc_server.got_args) as blocker: + with qtbot.wait_signal(ipc_server.got_args) as blocker: ipc_server.handle_connection() assert blocker.args == [['foo'], 'tab', ''] @@ -456,7 +458,7 @@ def connected_socket(qtbot, qlocalsocket, ipc_server): pytest.skip("Skipping connected_socket test - " "https://github.com/qutebrowser/qutebrowser/issues/1045") ipc_server.listen() - with qtbot.waitSignal(ipc_server._server.newConnection): + with qtbot.wait_signal(ipc_server._server.newConnection): qlocalsocket.connectToServer('qute-test') yield qlocalsocket qlocalsocket.disconnectFromServer() @@ -496,8 +498,8 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8') def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg): signals = [ipc_server.got_invalid_data, connected_socket.disconnected] with caplog.at_level(logging.ERROR): - with qtbot.assertNotEmitted(ipc_server.got_args): - with qtbot.waitSignals(signals, order='strict'): + with qtbot.assert_not_emitted(ipc_server.got_args): + with qtbot.wait_signals(signals, order='strict'): connected_socket.write(data) invalid_msg = 'Ignoring invalid IPC data from socket ' @@ -514,8 +516,8 @@ def test_multiline(qtbot, ipc_server, connected_socket): ' "protocol_version": {version}}}\n'.format( version=ipc.PROTOCOL_VERSION)) - with qtbot.assertNotEmitted(ipc_server.got_invalid_data): - with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args], + with qtbot.assert_not_emitted(ipc_server.got_invalid_data): + with qtbot.wait_signals([ipc_server.got_args, ipc_server.got_args], order='strict'): connected_socket.write(data.encode('utf-8')) @@ -536,10 +538,10 @@ class TestSendToRunningInstance: def test_normal(self, qtbot, tmp_path, ipc_server, mocker, has_cwd): ipc_server.listen() - with qtbot.assertNotEmitted(ipc_server.got_invalid_data): - with qtbot.waitSignal(ipc_server.got_args, + with qtbot.assert_not_emitted(ipc_server.got_invalid_data): + with qtbot.wait_signal(ipc_server.got_args, timeout=5000) as blocker: - with qtbot.waitSignal(ipc_server.got_raw, + with qtbot.wait_signal(ipc_server.got_raw, timeout=5000) as raw_blocker: with testutils.change_cwd(tmp_path): if not has_cwd: @@ -588,11 +590,11 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server): ipc_server._timer.setInterval(100) ipc_server.listen() - with qtbot.waitSignal(ipc_server._server.newConnection): + with qtbot.wait_signal(ipc_server._server.newConnection): qlocalsocket.connectToServer('qute-test') with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(qlocalsocket.disconnected, timeout=5000): + with qtbot.wait_signal(qlocalsocket.disconnected, timeout=5000): pass assert caplog.messages[-1].startswith("IPC connection timed out") @@ -654,7 +656,7 @@ class TestSendOrListen: assert "Starting IPC server..." in caplog.messages assert ret_server is ipc.server - with qtbot.waitSignal(ret_server.got_args): + with qtbot.wait_signal(ret_server.got_args): ret_client = ipc.send_or_listen(args) assert ret_client is None diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 580869b45..8d9c53c93 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -55,8 +55,8 @@ def keyhint(qtbot, config_stub, key_config_stub): def test_show_and_hide(qtbot, keyhint): - with qtbot.waitSignal(keyhint.update_geometry): - with qtbot.waitExposed(keyhint): + with qtbot.wait_signal(keyhint.update_geometry): + with qtbot.wait_exposed(keyhint): keyhint.show() keyhint.update_keyhint(usertypes.KeyMode.normal, '') assert not keyhint.isVisible() diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py index 572a5cbc9..6e1919ec3 100644 --- a/tests/unit/misc/test_miscwidgets.py +++ b/tests/unit/misc/test_miscwidgets.py @@ -127,7 +127,7 @@ class TestFullscreenNotification: def test_timeout(self, qtbot, key_config_stub): w = miscwidgets.FullscreenNotification() qtbot.add_widget(w) - with qtbot.waitSignal(w.destroyed): + with qtbot.wait_signal(w.destroyed): w.set_timeout(1) diff --git a/tests/unit/misc/test_msgbox.py b/tests/unit/misc/test_msgbox.py index 893f317d7..c5c23639a 100644 --- a/tests/unit/misc/test_msgbox.py +++ b/tests/unit/misc/test_msgbox.py @@ -77,7 +77,7 @@ def test_finished_signal(qtbot): qtbot.add_widget(box) - with qtbot.waitSignal(box.finished): + with qtbot.wait_signal(box.finished): box.accept() assert signal_triggered @@ -92,10 +92,8 @@ def test_information(qtbot): assert box.icon() == QMessageBox.Information -def test_no_err_windows(fake_args, capsys): +def test_no_err_windows(fake_args, caplog): fake_args.no_err_windows = True box = msgbox.information(parent=None, title='foo', text='bar') box.exec() # should do nothing - out, err = capsys.readouterr() - assert not out - assert err == 'Message box: foo; bar\n' + assert caplog.messages == ['foo\n\nbar'] diff --git a/tests/unit/misc/test_pastebin.py b/tests/unit/misc/test_pastebin.py index e750bb299..7a47e723c 100644 --- a/tests/unit/misc/test_pastebin.py +++ b/tests/unit/misc/test_pastebin.py @@ -99,8 +99,8 @@ def test_paste_private(pbclient): "http://paste.the-compiler.org/view/3gjnwg4" ]) def test_on_client_success(http, pbclient, qtbot): - with qtbot.assertNotEmitted(pbclient.error): - with qtbot.waitSignal(pbclient.success): + with qtbot.assert_not_emitted(pbclient.error): + with qtbot.wait_signal(pbclient.success): pbclient._client.success.emit(http) @@ -110,12 +110,12 @@ def test_on_client_success(http, pbclient, qtbot): "http//invalid.com" ]) def test_client_success_invalid_http(http, pbclient, qtbot): - with qtbot.assertNotEmitted(pbclient.success): - with qtbot.waitSignal(pbclient.error): + with qtbot.assert_not_emitted(pbclient.success): + with qtbot.wait_signal(pbclient.error): pbclient._client.success.emit(http) def test_client_error(pbclient, qtbot): - with qtbot.assertNotEmitted(pbclient.success): - with qtbot.waitSignal(pbclient.error): + with qtbot.assert_not_emitted(pbclient.success): + with qtbot.wait_signal(pbclient.error): pbclient._client.error.emit("msg") diff --git a/tests/unit/misc/test_sql.py b/tests/unit/misc/test_sql.py index 211280a6b..f6fa68869 100644 --- a/tests/unit/misc/test_sql.py +++ b/tests/unit/misc/test_sql.py @@ -128,18 +128,18 @@ def test_init(): def test_insert(qtbot): table = sql.SqlTable('Foo', ['name', 'val', 'lucky']) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.insert({'name': 'one', 'val': 1, 'lucky': False}) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.insert({'name': 'wan', 'val': 1, 'lucky': False}) def test_insert_replace(qtbot): table = sql.SqlTable('Foo', ['name', 'val', 'lucky'], constraints={'name': 'PRIMARY KEY'}) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.insert({'name': 'one', 'val': 1, 'lucky': False}, replace=True) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=True) assert list(table) == [('one', 11, True)] @@ -150,7 +150,7 @@ def test_insert_replace(qtbot): def test_insert_batch(qtbot): table = sql.SqlTable('Foo', ['name', 'val', 'lucky']) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.insert_batch({'name': ['one', 'nine', 'thirteen'], 'val': [1, 9, 13], 'lucky': [False, False, True]}) @@ -164,12 +164,12 @@ def test_insert_batch_replace(qtbot): table = sql.SqlTable('Foo', ['name', 'val', 'lucky'], constraints={'name': 'PRIMARY KEY'}) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.insert_batch({'name': ['one', 'nine', 'thirteen'], 'val': [1, 9, 13], 'lucky': [False, False, True]}) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.insert_batch({'name': ['one', 'nine'], 'val': [11, 19], 'lucky': [True, True]}, @@ -219,19 +219,14 @@ def test_delete(qtbot): table.insert({'name': 'thirteen', 'val': 13, 'lucky': True}) with pytest.raises(KeyError): table.delete('name', 'nope') - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.delete('name', 'thirteen') assert list(table) == [('one', 1, False), ('nine', 9, False)] - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.delete('lucky', False) assert not list(table) -def test_delete_optional(qtbot): - table = sql.SqlTable('Foo', ['name', 'val']) - table.delete('name', 'doesnotexist', optional=True) - - def test_len(): table = sql.SqlTable('Foo', ['name', 'val', 'lucky']) assert len(table) == 0 @@ -289,7 +284,7 @@ def test_delete_all(qtbot): table.insert({'name': 'one', 'val': 1, 'lucky': False}) table.insert({'name': 'nine', 'val': 9, 'lucky': False}) table.insert({'name': 'thirteen', 'val': 13, 'lucky': True}) - with qtbot.waitSignal(table.changed): + with qtbot.wait_signal(table.changed): table.delete_all() assert list(table) == [] diff --git a/tests/unit/misc/test_throttle.py b/tests/unit/misc/test_throttle.py index 48148a6a6..33e9949f2 100644 --- a/tests/unit/misc/test_throttle.py +++ b/tests/unit/misc/test_throttle.py @@ -25,11 +25,11 @@ import sip import pytest from PyQt5.QtCore import QObject -from helpers import utils +from helpers import testutils from qutebrowser.misc import throttle -DELAY = 500 if utils.ON_CI else 100 +DELAY = 500 if testutils.ON_CI else 100 @pytest.fixture diff --git a/tests/unit/misc/userscripts/test_qute_lastpass.py b/tests/unit/misc/userscripts/test_qute_lastpass.py index 7e2c7dfbd..3846028ee 100644 --- a/tests/unit/misc/userscripts/test_qute_lastpass.py +++ b/tests/unit/misc/userscripts/test_qute_lastpass.py @@ -26,9 +26,9 @@ from unittest.mock import ANY, call import pytest -from helpers import utils +from helpers import testutils -qute_lastpass = utils.import_userscript('qute-lastpass') +qute_lastpass = testutils.import_userscript('qute-lastpass') default_lpass_match = [ { diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index 5a7551b1c..9708c6283 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -177,7 +177,7 @@ def test_untested_floats(covtest): @pytest.mark.skipif( - sys.version_info[:4] == (3, 10, 0, 'alpha') and sys.version_info.serial < 5, + sys.version_info[:4] == (3, 10, 0, 'alpha'), reason='Different results, see https://github.com/nedbat/coveragepy/issues/1106') def test_untested_branches(covtest): covtest.makefile(""" diff --git a/tests/unit/scripts/test_run_vulture.py b/tests/unit/scripts/test_run_vulture.py index f1e5a261a..0ef5afcee 100644 --- a/tests/unit/scripts/test_run_vulture.py +++ b/tests/unit/scripts/test_run_vulture.py @@ -23,7 +23,7 @@ import textwrap import pytest -from tests.helpers import utils +from tests.helpers import testutils try: from scripts.dev import run_vulture @@ -53,7 +53,7 @@ class VultureDir: """Run vulture over all generated files and return the output.""" names = [p.name for p in self._tmp_path.glob('*')] assert names - with utils.change_cwd(self._tmp_path): + with testutils.change_cwd(self._tmp_path): return run_vulture.run(names) def makepyfile(self, **kwargs): @@ -86,7 +86,7 @@ def test_unused_func(vultdir): msg = "*test_unused_func*foo.py:2: unused function 'foo' (60% confidence)" msgs = vultdir.run() assert len(msgs) == 1 - assert utils.pattern_match(pattern=msg, value=msgs[0]) + assert testutils.pattern_match(pattern=msg, value=msgs[0]) def test_unused_method_camelcase(vultdir): diff --git a/tests/unit/test_qutebrowser.py b/tests/unit/test_qutebrowser.py index c553606c4..d9275631d 100644 --- a/tests/unit/test_qutebrowser.py +++ b/tests/unit/test_qutebrowser.py @@ -60,3 +60,18 @@ class TestLogFilter: _out, err = capsys.readouterr() print(err) assert 'Invalid log category invalid - valid categories' in err + + +class TestJsonArgs: + + def test_partial(self, parser): + """Make sure we can provide a subset of all arguments. + + This ensures that it's possible to restart into an older version of qutebrowser + when a new argument was added. + """ + args = parser.parse_args(['--json-args', '{"debug": true}']) + args = qutebrowser._unpack_json_args(args) + # pylint: disable=no-member + assert args.debug + assert not args.temp_basedir diff --git a/tests/unit/utils/test_javascript.py b/tests/unit/utils/test_javascript.py index e8acc30fb..7a97ef6d1 100644 --- a/tests/unit/utils/test_javascript.py +++ b/tests/unit/utils/test_javascript.py @@ -73,7 +73,7 @@ class TestStringEscape: """Test conversion by running JS in a tab.""" escaped = javascript.string_escape(text) - with qtbot.waitCallback() as cb: + with qtbot.wait_callback() as cb: web_tab.run_js_async('"{}";'.format(escaped), cb) cb.assert_called_with(text) diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index eb1233332..c686e4f74 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -34,6 +34,7 @@ from PyQt5.QtGui import QColor from qutebrowser.utils import qtutils, utils, usertypes import overflow_test_cases +from helpers import testutils if utils.is_linux: # Those are not run on macOS because that seems to cause a hang sometimes. @@ -941,38 +942,28 @@ class TestEventLoop: assert not self.loop._executing -class Color(QColor): - - """A QColor with a nicer repr().""" - - def __repr__(self): - return utils.get_repr(self, constructor=True, red=self.red(), - green=self.green(), blue=self.blue(), - alpha=self.alpha()) - - class TestInterpolateColor: @dataclasses.dataclass class Colors: - white: Color - black: Color + white: testutils.Color + black: testutils.Color @pytest.fixture def colors(self): """Example colors to be used.""" - return self.Colors(Color('white'), Color('black')) + return self.Colors(testutils.Color('white'), testutils.Color('black')) def test_invalid_start(self, colors): """Test an invalid start color.""" with pytest.raises(qtutils.QtValueError): - qtutils.interpolate_color(Color(), colors.white, 0) + qtutils.interpolate_color(testutils.Color(), colors.white, 0) def test_invalid_end(self, colors): """Test an invalid end color.""" with pytest.raises(qtutils.QtValueError): - qtutils.interpolate_color(colors.white, Color(), 0) + qtutils.interpolate_color(colors.white, testutils.Color(), 0) @pytest.mark.parametrize('perc', [-1, 101]) def test_invalid_percentage(self, colors, perc): @@ -985,52 +976,50 @@ class TestInterpolateColor: with pytest.raises(ValueError): qtutils.interpolate_color(colors.white, colors.black, 10, QColor.Cmyk) - @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, - QColor.Hsl]) + @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, QColor.Hsl]) def test_0_100(self, colors, colorspace): """Test 0% and 100% in different colorspaces.""" white = qtutils.interpolate_color(colors.white, colors.black, 0, colorspace) black = qtutils.interpolate_color(colors.white, colors.black, 100, colorspace) - assert Color(white) == colors.white - assert Color(black) == colors.black + assert testutils.Color(white) == colors.white + assert testutils.Color(black) == colors.black def test_interpolation_rgb(self): """Test an interpolation in the RGB colorspace.""" color = qtutils.interpolate_color( - Color(0, 40, 100), Color(0, 20, 200), 50, QColor.Rgb) - assert Color(color) == Color(0, 30, 150) + testutils.Color(0, 40, 100), testutils.Color(0, 20, 200), 50, QColor.Rgb) + assert testutils.Color(color) == testutils.Color(0, 30, 150) def test_interpolation_hsv(self): """Test an interpolation in the HSV colorspace.""" - start = Color() - stop = Color() + start = testutils.Color() + stop = testutils.Color() start.setHsv(0, 40, 100) stop.setHsv(0, 20, 200) color = qtutils.interpolate_color(start, stop, 50, QColor.Hsv) - expected = Color() + expected = testutils.Color() expected.setHsv(0, 30, 150) - assert Color(color) == expected + assert testutils.Color(color) == expected def test_interpolation_hsl(self): """Test an interpolation in the HSL colorspace.""" - start = Color() - stop = Color() + start = testutils.Color() + stop = testutils.Color() start.setHsl(0, 40, 100) stop.setHsl(0, 20, 200) color = qtutils.interpolate_color(start, stop, 50, QColor.Hsl) - expected = Color() + expected = testutils.Color() expected.setHsl(0, 30, 150) - assert Color(color) == expected + assert testutils.Color(color) == expected - @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, - QColor.Hsl]) + @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, QColor.Hsl]) def test_interpolation_alpha(self, colorspace): """Test interpolation of colorspace's alpha.""" - start = Color(0, 0, 0, 30) - stop = Color(0, 0, 0, 100) + start = testutils.Color(0, 0, 0, 30) + stop = testutils.Color(0, 0, 0, 100) color = qtutils.interpolate_color(start, stop, 50, colorspace) - expected = Color(0, 0, 0, 65) - assert Color(color) == expected + expected = testutils.Color(0, 0, 0, 65) + assert testutils.Color(color) == expected @pytest.mark.parametrize('percentage, expected', [ (0, (0, 0, 0)), @@ -1040,6 +1029,6 @@ class TestInterpolateColor: def test_interpolation_none(self, percentage, expected): """Test an interpolation with a gradient turned off.""" color = qtutils.interpolate_color( - Color(0, 0, 0), Color(255, 255, 255), percentage, None) + testutils.Color(0, 0, 0), testutils.Color(255, 255, 255), percentage, None) assert isinstance(color, QColor) - assert Color(color) == Color(*expected) + assert testutils.Color(color) == testutils.Color(*expected) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index f6ac8039d..4cf60943c 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -30,7 +30,7 @@ import shlex import math import zipfile -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import QUrl, QRect from PyQt5.QtGui import QClipboard import pytest import hypothesis @@ -42,6 +42,22 @@ import qutebrowser.utils # for test_qualname from qutebrowser.utils import utils, version, usertypes +class TestVersionNumber: + + @pytest.mark.parametrize('args, expected', [ + ([5, 15, 2], 'VersionNumber(5, 15, 2)'), + ([5, 15], 'VersionNumber(5, 15)'), + ([5], 'VersionNumber(5)'), + ]) + def test_repr(self, args, expected): + num = utils.VersionNumber(*args) + assert repr(num) == expected + + def test_not_normalized(self): + with pytest.raises(ValueError, match='Refusing to construct'): + utils.VersionNumber(5, 15, 0) + + ELLIPSIS = '\u2026' @@ -993,3 +1009,53 @@ class TestCleanupFileContext: pass assert len(caplog.messages) == 1 assert caplog.messages[0].startswith("Failed to delete tempfile") + + +class TestParseRect: + + @pytest.mark.parametrize('value, expected', [ + ('1x1+0+0', QRect(0, 0, 1, 1)), + ('123x789+12+34', QRect(12, 34, 123, 789)), + ]) + def test_valid(self, value, expected): + assert utils.parse_rect(value) == expected + + @pytest.mark.parametrize('value, message', [ + ('0x0+1+1', "Invalid rectangle"), + ('1x1-1+1', "String 1x1-1+1 does not match WxH+X+Y"), + ('1x1+1-1', "String 1x1+1-1 does not match WxH+X+Y"), + ('1x1', "String 1x1 does not match WxH+X+Y"), + ('+1+2', "String +1+2 does not match WxH+X+Y"), + ('1e0x1+0+0', "String 1e0x1+0+0 does not match WxH+X+Y"), + ('¹x1+0+0', "String ¹x1+0+0 does not match WxH+X+Y"), + ]) + def test_invalid(self, value, message): + with pytest.raises(ValueError) as excinfo: + utils.parse_rect(value) + assert str(excinfo.value) == message + + @hypothesis.given(strategies.text()) + def test_hypothesis_text(self, s): + try: + utils.parse_rect(s) + except ValueError as e: + print(e) + + @hypothesis.given(strategies.tuples( + strategies.integers(), + strategies.integers(), + strategies.integers(), + strategies.integers(), + ).map(lambda tpl: '{}x{}+{}+{}'.format(*tpl))) + def test_hypothesis_sophisticated(self, s): + try: + utils.parse_rect(s) + except ValueError as e: + print(e) + + @hypothesis.given(strategies.from_regex(utils._RECT_PATTERN)) + def test_hypothesis_regex(self, s): + try: + utils.parse_rect(s) + except ValueError as e: + print(e) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 9a8e6d075..f846c91ac 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -21,13 +21,9 @@ import io import sys -import collections import os.path import subprocess import contextlib -import builtins -import types -import importlib import logging import textwrap import datetime @@ -36,11 +32,12 @@ import dataclasses import pytest import hypothesis import hypothesis.strategies +from PyQt5.QtCore import PYQT_VERSION_STR import qutebrowser -from qutebrowser.config import config +from qutebrowser.config import config, websettings from qutebrowser.utils import version, usertypes, utils, standarddir -from qutebrowser.misc import pastebin, objects +from qutebrowser.misc import pastebin, objects, elf from qutebrowser.browser import pdfjs @@ -76,7 +73,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='ubuntu', parsed=version.Distribution.ubuntu, - version=utils.parse_version('14.4'), + version=utils.VersionNumber(14, 4, 5), pretty='Ubuntu 14.04.5 LTS')), # Ubuntu 17.04 (""" @@ -89,7 +86,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='ubuntu', parsed=version.Distribution.ubuntu, - version=utils.parse_version('17.4'), + version=utils.VersionNumber(17, 4), pretty='Ubuntu 17.04')), # Debian Jessie (""" @@ -101,7 +98,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='debian', parsed=version.Distribution.debian, - version=utils.parse_version('8'), + version=utils.VersionNumber(8), pretty='Debian GNU/Linux 8 (jessie)')), # Void Linux (""" @@ -132,7 +129,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='fedora', parsed=version.Distribution.fedora, - version=utils.parse_version('25'), + version=utils.VersionNumber(25), pretty='Fedora 25 (Twenty Five)')), # OpenSUSE (""" @@ -145,7 +142,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='opensuse', parsed=version.Distribution.opensuse, - version=utils.parse_version('42.2'), + version=utils.VersionNumber(42, 2), pretty='openSUSE Leap 42.2')), # Linux Mint (""" @@ -158,7 +155,7 @@ from qutebrowser.browser import pdfjs """, version.DistributionInfo( id='linuxmint', parsed=version.Distribution.linuxmint, - version=utils.parse_version('18.1'), + version=utils.VersionNumber(18, 1), pretty='Linux Mint 18.1')), # Manjaro (""" @@ -178,6 +175,97 @@ from qutebrowser.browser import pdfjs version.DistributionInfo( id='funtoo', parsed=version.Distribution.gentoo, version=None, pretty='Funtoo GNU/Linux')), + # KDE neon + (""" + NAME="KDE neon" + VERSION="5.20" + ID=neon + ID_LIKE="ubuntu debian" + PRETTY_NAME="KDE neon User Edition 5.20" + VARIANT="User Edition" + VERSION_ID="20.04" + """, + version.DistributionInfo( + id='neon', parsed=version.Distribution.neon, + version=utils.VersionNumber(5, 20), pretty='KDE neon User Edition 5.20')), + # Archlinux ARM + (""" + NAME="Arch Linux ARM" + PRETTY_NAME="Arch Linux ARM" + ID=archarm + ID_LIKE=arch + """, + version.DistributionInfo( + id='archarm', parsed=version.Distribution.arch, + version=None, pretty='Arch Linux ARM')), + # Alpine + (""" + NAME="Alpine Linux" + ID=alpine + VERSION_ID=3.12_alpha20200122 + PRETTY_NAME="Alpine Linux edge" + """, + version.DistributionInfo( + id='alpine', parsed=version.Distribution.alpine, + version=utils.VersionNumber(3, 12), pretty='Alpine Linux edge')), + # EndeavourOS + (""" + NAME="EndeavourOS" + PRETTY_NAME="EndeavourOS" + ID=endeavouros + ID_LIKE=arch + BUILD_ID=rolling + DOCUMENTATION_URL="https://endeavouros.com/wiki/" + LOGO=endeavouros + """, + version.DistributionInfo( + id='endeavouros', parsed=version.Distribution.arch, + version=None, pretty='EndeavourOS')), + # Manjaro ARM + (""" + NAME="Manjaro-ARM" + ID=manjaro-arm + ID_LIKE=manjaro arch + PRETTY_NAME="Manjaro ARM" + """, + version.DistributionInfo( + id='manjaro-arm', parsed=version.Distribution.manjaro, + version=None, pretty='Manjaro ARM')), + # Artix Linux + (""" + NAME="Artix Linux" + PRETTY_NAME="Artix Linux" + ID=artix + """, + version.DistributionInfo( + id='artix', parsed=version.Distribution.arch, + version=None, pretty='Artix Linux')), + # NixOS + (""" + NAME=NixOS + ID=nixos + VERSION="21.03pre268206.536fe36e23a (Okapi)" + VERSION_CODENAME=okapi + VERSION_ID="21.03pre268206.536fe36e23a" + PRETTY_NAME="NixOS 21.03 (Okapi)" + """, + version.DistributionInfo( + id='nixos', parsed=version.Distribution.nixos, + version=utils.VersionNumber(21, 3), + pretty='NixOS 21.03 (Okapi)')), + # SolusOS + (""" + NAME="Solus" + VERSION="4.2" + ID="solus" + VERSION_CODENAME=fortitude + VERSION_ID="4.2" + PRETTY_NAME="Solus 4.2 Fortitude" + """, + version.DistributionInfo( + id='solus', parsed=version.Distribution.solus, + version=utils.VersionNumber(4, 2), + pretty='Solus 4.2 Fortitude')), # KDE Platform (""" NAME=KDE @@ -185,27 +273,27 @@ from qutebrowser.browser import pdfjs VERSION_ID="5.12" ID=org.kde.Platform """, - version.DistributionInfo( - id='org.kde.Platform', parsed=version.Distribution.kde_flatpak, - version=utils.parse_version('5.12'), - pretty='KDE')), + version.DistributionInfo( + id='org.kde.Platform', parsed=version.Distribution.kde_flatpak, + version=utils.VersionNumber(5, 12), + pretty='KDE')), # No PRETTY_NAME (""" NAME="Tux" ID=tux - """, - version.DistributionInfo( - id='tux', parsed=version.Distribution.unknown, - version=None, pretty='Tux')), + """, + version.DistributionInfo( + id='tux', parsed=version.Distribution.unknown, + version=None, pretty='Tux')), # Invalid multi-line value (""" ID=tux PRETTY_NAME="Multiline Text" - """, - version.DistributionInfo( - id='tux', parsed=version.Distribution.unknown, - version=None, pretty='Multiline')), + """, + version.DistributionInfo( + id='tux', parsed=version.Distribution.unknown, + version=None, pretty='Multiline')), ]) def test_distribution(tmpdir, monkeypatch, os_release, expected): os_release_file = tmpdir / 'os-release' @@ -220,7 +308,7 @@ def test_distribution(tmpdir, monkeypatch, os_release, expected): (None, False), (version.DistributionInfo( id='org.kde.Platform', parsed=version.Distribution.kde_flatpak, - version=utils.parse_version('5.12'), + version=utils.VersionNumber(5, 12), pretty='Unknown'), True), (version.DistributionInfo( id='arch', parsed=version.Distribution.arch, version=None, @@ -538,70 +626,11 @@ def test_path_info(monkeypatch, equal): assert pathinfo['system data'] == 'SYSTEM DATA PATH' -class ImportFake: - - """A fake for __import__ which is used by the import_fake fixture. - - Attributes: - modules: A dict mapping module names to bools. If True, the import will - succeed. Otherwise, it'll fail with ImportError. - version_attribute: The name to use in the fake modules for the version - attribute. - version: The version to use for the modules. - _real_import: Saving the real __import__ builtin so the imports can be - done normally for modules not in self. modules. - """ - - def __init__(self): - self.modules = collections.OrderedDict( - [(mod, True) for mod in version.MODULE_INFO]) - self.version_attribute = '__version__' - self.version = '1.2.3' - self._real_import = builtins.__import__ - self._real_importlib_import = importlib.import_module - - def _do_import(self, name): - """Helper for fake_import and fake_importlib_import to do the work. - - Return: - The imported fake module, or None if normal importing should be - used. - """ - if name not in self.modules: - # Not one of the modules to test -> use real import - return None - elif self.modules[name]: - ns = types.SimpleNamespace() - if self.version_attribute is not None: - setattr(ns, self.version_attribute, self.version) - return ns - else: - raise ImportError("Fake ImportError for {}.".format(name)) - - def fake_import(self, name, *args, **kwargs): - """Fake for the builtin __import__.""" - module = self._do_import(name) - if module is not None: - return module - else: - return self._real_import(name, *args, **kwargs) - - def fake_importlib_import(self, name): - """Fake for importlib.import_module.""" - module = self._do_import(name) - if module is not None: - return module - else: - return self._real_importlib_import(name) - - @pytest.fixture -def import_fake(monkeypatch): +def import_fake(stubs, monkeypatch): """Fixture to patch imports using ImportFake.""" - fake = ImportFake() - monkeypatch.setattr(builtins, '__import__', fake.fake_import) - monkeypatch.setattr(version.importlib, 'import_module', - fake.fake_importlib_import) + fake = stubs.ImportFake({mod: True for mod in version.MODULE_INFO}, monkeypatch) + fake.patch() return fake @@ -869,6 +898,103 @@ class TestPDFJSVersion: assert ver.split()[0] not in ['no', 'unknown'], ver +class TestWebEngineVersions: + + @pytest.mark.parametrize('version, expected', [ + ( + version.WebEngineVersions( + webengine=utils.VersionNumber(5, 15, 2), + chromium=None, + source='UA'), + "QtWebEngine 5.15.2", + ), + ( + version.WebEngineVersions( + webengine=utils.VersionNumber(5, 15, 2), + chromium='87.0.4280.144', + source='UA'), + "QtWebEngine 5.15.2, Chromium 87.0.4280.144", + ), + ( + version.WebEngineVersions( + webengine=utils.VersionNumber(5, 15, 2), + chromium='87.0.4280.144', + source='faked'), + "QtWebEngine 5.15.2, Chromium 87.0.4280.144 (from faked)", + ), + ]) + def test_str(self, version, expected): + assert str(version) == expected + + def test_from_ua(self): + ua = websettings.UserAgent( + os_info='X11; Linux x86_64', + webkit_version='537.36', + upstream_browser_key='Chrome', + upstream_browser_version='83.0.4103.122', + qt_key='QtWebEngine', + qt_version='5.15.2', + ) + expected = version.WebEngineVersions( + webengine=utils.VersionNumber(5, 15, 2), + chromium='83.0.4103.122', + source='UA', + ) + assert version.WebEngineVersions.from_ua(ua) == expected + + def test_from_elf(self): + elf_version = elf.Versions(webengine='5.15.2', chromium='83.0.4103.122') + expected = version.WebEngineVersions( + webengine=utils.VersionNumber(5, 15, 2), + chromium='83.0.4103.122', + source='ELF', + ) + assert version.WebEngineVersions.from_elf(elf_version) == expected + + @pytest.mark.parametrize('qt_version, chromium_version', [ + ('5.12.10', '69.0.3497.128'), + ('5.14.2', '77.0.3865.129'), + ('5.15.1', '80.0.3987.163'), + ('5.15.2', '83.0.4103.122'), + ]) + def test_from_pyqt(self, qt_version, chromium_version): + expected = version.WebEngineVersions( + webengine=utils.parse_version(qt_version), + chromium=chromium_version, + source='PyQt', + ) + assert version.WebEngineVersions.from_pyqt(qt_version) == expected + + def test_real_chromium_version(self, qapp): + """Compare the inferred Chromium version with the real one.""" + if '.dev' in PYQT_VERSION_STR: + pytest.skip("dev version of PyQt5") + + try: + from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION_STR + except ImportError as e: + # QtWebKit or QtWebEngine < 5.13 + pytest.skip(str(e)) + + pyqt_webengine_version = version._get_pyqt_webengine_qt_version() + if pyqt_webengine_version is None: + pyqt_webengine_version = PYQT_WEBENGINE_VERSION_STR + + versions = version.WebEngineVersions.from_pyqt(pyqt_webengine_version) + + if pyqt_webengine_version == '5.15.3': + # Transient situation - we expect to get QtWebEngine 5.15.3 soon, + # so this will line up again. + assert versions.chromium == '87.0.4280.144' + pytest.xfail("Transient situation") + else: + from qutebrowser.browser.webengine import webenginesettings + webenginesettings.init_user_agent() + expected = webenginesettings.parsed_user_agent.upstream_browser_version + + assert versions.chromium == expected + + class FakeQSslSocket: """Fake for the QSslSocket Qt class. @@ -902,25 +1028,19 @@ class TestChromiumVersion: @pytest.fixture(autouse=True) def clear_parsed_ua(self, monkeypatch): + pytest.importorskip('PyQt5.QtWebEngineWidgets') if version.webenginesettings is not None: # Not available with QtWebKit monkeypatch.setattr(version.webenginesettings, 'parsed_user_agent', None) def test_fake_ua(self, monkeypatch, caplog): - pytest.importorskip('PyQt5.QtWebEngineWidgets') - ver = '77.0.3865.98' version.webenginesettings._init_user_agent_str( _QTWE_USER_AGENT.format(ver)) - assert version._chromium_version() == ver - - def test_no_webengine(self, monkeypatch): - monkeypatch.setattr(version, 'webenginesettings', None) - assert version._chromium_version() == 'unavailable' + assert version.qtwebengine_versions().chromium == ver def test_prefers_saved_user_agent(self, monkeypatch): - pytest.importorskip('PyQt5.QtWebEngineWidgets') version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT) class FakeProfile: @@ -930,17 +1050,69 @@ class TestChromiumVersion: monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile', FakeProfile()) - version._chromium_version() + version.qtwebengine_versions() def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub): - pytest.importorskip('PyQt5.QtWebEngineWidgets') - unexpected = ['', 'unknown', 'unavailable', 'avoided'] - assert version._chromium_version() not in unexpected + assert version.qtwebengine_versions().chromium is not None def test_avoided(self, monkeypatch): - pytest.importorskip('PyQt5.QtWebEngineWidgets') - monkeypatch.setattr(objects, 'debug_flags', ['avoid-chromium-init']) - assert version._chromium_version() == 'avoided' + versions = version.qtwebengine_versions(avoid_init=True) + assert versions.source in ['ELF', 'importlib', 'PyQt', 'Qt'] + + @pytest.fixture + def patch_elf_fail(self, monkeypatch): + """Simulate parsing the version from ELF to fail.""" + monkeypatch.setattr(elf, 'parse_webenginecore', lambda: None) + + @pytest.fixture + def patch_old_pyqt(self, monkeypatch): + """Simulate an old PyQt without PYQT_WEBENGINE_VERSION_STR.""" + monkeypatch.setattr(version, 'PYQT_WEBENGINE_VERSION_STR', None) + + @pytest.fixture + def patch_no_importlib(self, monkeypatch, stubs): + """Simulate missing importlib modules.""" + import_fake = stubs.ImportFake({ + 'importlib_metadata': False, + 'importlib.metadata': False, + }, monkeypatch) + import_fake.patch() + + @pytest.fixture + def patch_importlib_no_package(self, monkeypatch): + """Simulate importlib not finding PyQtWebEngine-Qt.""" + try: + import importlib.metadata as importlib_metadata + except ImportError: + importlib_metadata = pytest.importorskip("importlib_metadata") + + def _fake_version(name): + assert name == 'PyQtWebEngine-Qt' + raise importlib_metadata.PackageNotFoundError(name) + + monkeypatch.setattr(importlib_metadata, 'version', _fake_version) + + @pytest.mark.parametrize('patches, sources', [ + (['elf_fail'], ['importlib', 'PyQt', 'Qt']), + (['elf_fail', 'old_pyqt'], ['importlib', 'Qt']), + (['elf_fail', 'no_importlib'], ['PyQt', 'Qt']), + (['elf_fail', 'no_importlib', 'old_pyqt'], ['Qt']), + (['elf_fail', 'importlib_no_package'], ['PyQt', 'Qt']), + (['elf_fail', 'importlib_no_package', 'old_pyqt'], ['Qt']), + ], ids=','.join) + def test_simulated(self, request, patches, sources): + """Test various simulated error conditions. + + This dynamically gets a list of fixtures (above) to do the patching. It then + checks whether the version it got is from one of the expected sources. Depending + on the environment this test is run in, some sources might fail "naturally", + i.e. without any patching related to them. + """ + for patch in patches: + request.getfixturevalue(f'patch_{patch}') + + versions = version.qtwebengine_versions(avoid_init=True) + assert versions.source in sources @dataclasses.dataclass @@ -1014,11 +1186,13 @@ def test_version_info(params, stubs, monkeypatch, config_stub): 'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no", } - ua = _QTWE_USER_AGENT.format('CHROMIUMVERSION') - if version.webenginesettings is None: - patches['_chromium_version'] = lambda: 'CHROMIUMVERSION' - else: - version.webenginesettings._init_user_agent_str(ua) + patches['qtwebengine_versions'] = ( + lambda avoid_init: version.WebEngineVersions( + webengine=utils.VersionNumber(1, 2, 3), + chromium=None, + source='faked', + ) + ) if params.config_py_loaded: substitutions["config_py_loaded"] = "{} has been loaded".format( @@ -1034,7 +1208,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 (Chromium CHROMIUMVERSION)' + substitutions['backend'] = 'QtWebEngine 1.2.3 (from faked)' if params.known_distribution: patches['distribution'] = lambda: version.DistributionInfo( diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py index dbb34c47f..5e3731109 100644 --- a/tests/unit/utils/usertypes/test_question.py +++ b/tests/unit/utils/usertypes/test_question.py @@ -53,14 +53,14 @@ def test_done(mode, answer, signal_names, question, qtbot): question.mode = mode question.answer = answer signals = [getattr(question, name) for name in signal_names] - with qtbot.waitSignals(signals, order='strict'): + with qtbot.wait_signals(signals, order='strict'): question.done() assert not question.is_aborted def test_cancel(question, qtbot): """Test Question.cancel().""" - with qtbot.waitSignals([question.cancelled, question.completed], + with qtbot.wait_signals([question.cancelled, question.completed], order='strict'): question.cancel() assert not question.is_aborted @@ -68,7 +68,7 @@ def test_cancel(question, qtbot): def test_abort(question, qtbot): """Test Question.abort().""" - with qtbot.waitSignals([question.aborted, question.completed], + with qtbot.wait_signals([question.aborted, question.completed], order='strict'): question.abort() assert question.is_aborted diff --git a/tests/unit/utils/usertypes/test_timer.py b/tests/unit/utils/usertypes/test_timer.py index 37c56b653..4dc85b06f 100644 --- a/tests/unit/utils/usertypes/test_timer.py +++ b/tests/unit/utils/usertypes/test_timer.py @@ -70,13 +70,13 @@ def test_start_overflow(): def test_timeout_start(qtbot): """Make sure the timer works with start().""" t = usertypes.Timer() - with qtbot.waitSignal(t.timeout, timeout=3000): + with qtbot.wait_signal(t.timeout, timeout=3000): t.start(200) def test_timeout_set_interval(qtbot): """Make sure the timer works with setInterval().""" t = usertypes.Timer() - with qtbot.waitSignal(t.timeout, timeout=3000): + with qtbot.wait_signal(t.timeout, timeout=3000): t.setInterval(200) t.start() |