summaryrefslogtreecommitdiff
path: root/tests/conftest.py
blob: ddacc3db10b27734ec511d39cbf6ec3c3e23cee2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""The qutebrowser test suite conftest file."""

import os
import pathlib
import sys
import ssl

import pytest
import hypothesis

pytest.register_assert_rewrite('helpers')

# pylint: disable=wildcard-import,unused-import,unused-wildcard-import
from helpers import logfail
from helpers.logfail import fail_on_logging
from helpers.messagemock import message_mock
from helpers.fixtures import *  # noqa: F403
# pylint: enable=wildcard-import,unused-import,unused-wildcard-import
from helpers import testutils
from qutebrowser.utils import usertypes, utils, version
from qutebrowser.misc import objects, earlyinit

from qutebrowser.qt import machinery
# To register commands
import qutebrowser.app  # pylint: disable=unused-import


_qute_scheme_handler = None


# Set hypothesis settings
hypothesis.settings.register_profile(
    'default', hypothesis.settings(
        deadline=600,
        suppress_health_check=[hypothesis.HealthCheck.function_scoped_fixture],
    )
)
hypothesis.settings.register_profile(
    'ci', hypothesis.settings(
        deadline=None,
        suppress_health_check=[
            hypothesis.HealthCheck.function_scoped_fixture,
            hypothesis.HealthCheck.too_slow
        ]
    )
)
hypothesis.settings.load_profile('ci' if testutils.ON_CI else 'default')


def _apply_platform_markers(config, item):
    """Apply a skip marker to a given item."""
    markers = [
        ('posix',
         pytest.mark.skipif,
         not utils.is_posix,
         "Requires a POSIX os"),
        ('windows',
         pytest.mark.skipif,
         not utils.is_windows,
         "Requires Windows"),
        ('linux',
         pytest.mark.skipif,
         not utils.is_linux,
         "Requires Linux"),
        ('mac',
         pytest.mark.skipif,
         not utils.is_mac,
         "Requires macOS"),
        ('not_mac',
         pytest.mark.skipif,
         utils.is_mac,
         "Skipped on macOS"),
        ('not_frozen',
         pytest.mark.skipif,
         getattr(sys, 'frozen', False),
         "Can't be run when frozen"),
        ('not_flatpak',
         pytest.mark.skipif,
         version.is_flatpak(),
         "Can't be run with Flatpak"),
        ('frozen',
         pytest.mark.skipif,
         not getattr(sys, 'frozen', False),
         "Can only run when frozen"),
        ('ci',
         pytest.mark.skipif,
         not testutils.ON_CI,
         "Only runs on CI."),
        ('no_ci',
         pytest.mark.skipif,
         testutils.ON_CI,
         "Skipped on CI."),
        ('unicode_locale',
         pytest.mark.skipif,
         sys.getfilesystemencoding() == 'ascii',
         "Skipped because of ASCII locale"),
        ('qt5_only',
         pytest.mark.skipif,
         not machinery.IS_QT5,
         f"Only runs on Qt 5, not {machinery.INFO.wrapper}"),
        ('qt6_only',
         pytest.mark.skipif,
         not machinery.IS_QT6,
         f"Only runs on Qt 6, not {machinery.INFO.wrapper}"),
        ('qt5_xfail', pytest.mark.xfail, machinery.IS_QT5, "Fails on Qt 5"),
        ('qt6_xfail', pytest.mark.skipif, machinery.IS_QT6, "Fails on Qt 6"),
        ('qtwebkit_openssl3_skip',
         pytest.mark.skipif,
         not config.webengine and ssl.OPENSSL_VERSION_INFO[0] == 3,
         "Failing due to cheroot: https://github.com/cherrypy/cheroot/issues/346"),
    ]

    for searched_marker, new_marker_kind, condition, default_reason in markers:
        marker = item.get_closest_marker(searched_marker)
        if not marker or not condition:
            continue

        if 'reason' in marker.kwargs:
            reason = '{}: {}'.format(default_reason, marker.kwargs['reason'])
            del marker.kwargs['reason']
        else:
            reason = default_reason + '.'
        new_marker = new_marker_kind(condition, *marker.args,
                                     reason=reason, **marker.kwargs)
        item.add_marker(new_marker)


def pytest_collection_modifyitems(config, items):
    """Handle custom markers.

    pytest hook called after collection has been performed.

    Adds a marker named "gui" which can be used to filter gui tests from the
    command line.

    For example:

        pytest -m "not gui"  # run all tests except gui tests
        pytest -m "gui"  # run only gui tests

    It also handles the platform specific markers by translating them to skipif
    markers.

    Args:
        items: list of _pytest.main.Node items, where each item represents
               a python test that will be executed.

    Reference:
        https://pytest.org/latest/plugins.html
    """
    remaining_items = []
    deselected_items = []

    for item in items:
        deselected = False

        if 'qapp' in getattr(item, 'fixturenames', ()):
            item.add_marker('gui')

        if hasattr(item, 'module'):
            test_basedir = pathlib.Path(__file__).parent
            module_path = pathlib.Path(item.module.__file__)
            module_root_dir = module_path.relative_to(test_basedir).parts[0]

            assert module_root_dir in ['end2end', 'unit', 'helpers',
                                       'test_conftest.py']
            if module_root_dir == 'end2end':
                item.add_marker(pytest.mark.end2end)

        _apply_platform_markers(config, item)
        if list(item.iter_markers('xfail_norun')):
            item.add_marker(pytest.mark.xfail(run=False))

        if deselected:
            deselected_items.append(item)
        else:
            remaining_items.append(item)

    config.hook.pytest_deselected(items=deselected_items)
    items[:] = remaining_items


def pytest_ignore_collect(collection_path: pathlib.Path) -> bool:
    """Ignore BDD tests if we're unable to run them."""
    skip_bdd = hasattr(sys, 'frozen')
    rel_path = collection_path.relative_to(pathlib.Path(__file__).parent)
    return rel_path == pathlib.Path('end2end') / 'features' and skip_bdd


@pytest.fixture(scope='session')
def qapp_args():
    """Make QtWebEngine unit tests run on older Qt versions + newer kernels."""
    if testutils.disable_seccomp_bpf_sandbox():
        return [sys.argv[0], testutils.DISABLE_SECCOMP_BPF_FLAG]

    # Disabling PaintHoldingCrossOrigin makes tests needing UI interaction with
    # QtWebEngine more reliable.
    # Only needed with QtWebEngine and Qt 6.5, but Qt just ignores arguments it
    # doesn't know about anyways.
    return [sys.argv[0], "--webEngineArgs", "--disable-features=PaintHoldingCrossOrigin"]


@pytest.fixture(scope='session')
def qapp(qapp):
    """Change the name of the QApplication instance."""
    qapp.setApplicationName('qute_test')
    return qapp


def pytest_addoption(parser):
    parser.addoption('--qute-delay', action='store', default=0, type=int,
                     help="Delay (in ms) between qutebrowser commands.")
    parser.addoption('--qute-delay-start', action='store', default=0, type=int,
                     help="Delay (in ms) after qutebrowser process started.")
    parser.addoption('--qute-profile-subprocs', action='store_true',
                     default=False, help="Run cProfile for subprocesses.")
    parser.addoption('--qute-backend', action='store',
                     choices=['webkit', 'webengine'], help='Set backend for BDD tests')


def pytest_configure(config):
    backend = _select_backend(config)
    config.webengine = backend == 'webengine'

    earlyinit.configure_pyqt()


def _select_backend(config):
    """Select the backend for running tests.

    The backend is auto-selected in the following manner:
    1. Use QtWebKit if available
    2. Otherwise use QtWebEngine as a fallback

    Auto-selection is overridden by either passing a backend via
    `--qute-backend=<backend>` or setting the environment variable
    `QUTE_TESTS_BACKEND=<backend>`.

    Args:
        config: pytest config

    Raises:
        ImportError if the selected backend is not available.

    Returns:
        The selected backend as a string (e.g. 'webkit').
    """
    backend_arg = config.getoption('--qute-backend')
    backend_env = os.environ.get('QUTE_TESTS_BACKEND')

    backend = backend_arg or backend_env or _auto_select_backend()

    # Fail early if selected backend is not available
    # pylint: disable=unused-import
    if backend == 'webkit':
        import qutebrowser.qt.webkitwidgets
    elif backend == 'webengine':
        import qutebrowser.qt.webenginewidgets
    else:
        raise utils.Unreachable(backend)

    return backend


def _auto_select_backend():
    # pylint: disable=unused-import
    try:
        # Try to use QtWebKit as the default backend
        import qutebrowser.qt.webkitwidgets
        return 'webkit'
    except ImportError:
        # Try to use QtWebEngine as a fallback and fail early
        # if that's also not available
        import qutebrowser.qt.webenginewidgets
        return 'webengine'


def pytest_report_header(config):
    if config.webengine:
        backend_version = version.qtwebengine_versions(avoid_init=True)
    else:
        backend_version = version.qWebKitVersion()

    return f'backend: {backend_version}'


@pytest.fixture(scope='session', autouse=True)
def check_display(request):
    if utils.is_linux and not os.environ.get('DISPLAY', ''):
        raise RuntimeError("No display and no Xvfb available!")


@pytest.fixture(autouse=True)
def set_backend(monkeypatch, request):
    """Make sure the backend global is set."""
    if not request.config.webengine and version.qWebKitVersion:
        backend = usertypes.Backend.QtWebKit
    else:
        backend = usertypes.Backend.QtWebEngine
    monkeypatch.setattr(objects, 'backend', backend)


@pytest.fixture(autouse=True)
def apply_fake_os(monkeypatch, request):
    fake_os = request.node.get_closest_marker('fake_os')
    if not fake_os:
        return

    name = fake_os.args[0]
    mac = False
    windows = False
    linux = False
    posix = False

    if name == 'unknown':
        pass
    elif name == 'mac':
        mac = True
        posix = True
    elif name == 'windows':
        windows = True
    elif name == 'linux':
        linux = True
        posix = True
    elif name == 'posix':
        posix = True
    else:
        raise ValueError("Invalid fake_os {}".format(name))

    monkeypatch.setattr(utils, 'is_mac', mac)
    monkeypatch.setattr(utils, 'is_linux', linux)
    monkeypatch.setattr(utils, 'is_windows', windows)
    monkeypatch.setattr(utils, 'is_posix', posix)


@pytest.fixture(scope='session', autouse=True)
def check_yaml_c_exts():
    """Make sure PyYAML C extensions are available on CI.

    Not available yet with a nightly Python, see:
    https://github.com/yaml/pyyaml/issues/630
    """
    if testutils.ON_CI and sys.version_info[:2] != (3, 11):
        from yaml import CLoader  # pylint: disable=unused-import


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """Make test information available in fixtures.

    See https://pytest.org/latest/example/simple.html#making-test-result-information-available-in-fixtures
    """
    outcome = yield
    rep = outcome.get_result()
    setattr(item, "rep_" + rep.when, rep)


@pytest.hookimpl(hookwrapper=True)
def pytest_terminal_summary(terminalreporter):
    """Group benchmark results on CI."""
    if testutils.ON_CI:
        terminalreporter.write_line(
            testutils.gha_group_begin('Benchmark results'))
        yield
        terminalreporter.write_line(testutils.gha_group_end())
    else:
        yield