1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2
3# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
4#
5# This file is part of qutebrowser.
6#
7# qutebrowser is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# qutebrowser is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.
19
20# pylint: disable=unused-import,wildcard-import,unused-wildcard-import
21
22"""The qutebrowser test suite conftest file."""
23
24import os
25import pathlib
26import sys
27import warnings
28
29import pytest
30import hypothesis
31from PyQt5.QtCore import PYQT_VERSION
32
33pytest.register_assert_rewrite('helpers')
34
35from helpers import logfail
36from helpers.logfail import fail_on_logging
37from helpers.messagemock import message_mock
38from helpers.fixtures import *  # noqa: F403
39from helpers import testutils
40from qutebrowser.utils import qtutils, standarddir, usertypes, utils, version
41from qutebrowser.misc import objects, earlyinit
42from qutebrowser.qt import sip
43
44import qutebrowser.app  # To register commands
45
46
47_qute_scheme_handler = None
48
49
50# Set hypothesis settings
51hypothesis.settings.register_profile(
52    'default', hypothesis.settings(
53        deadline=600,
54        suppress_health_check=[hypothesis.HealthCheck.function_scoped_fixture],
55    )
56)
57hypothesis.settings.register_profile(
58    'ci', hypothesis.settings(
59        deadline=None,
60        suppress_health_check=[
61            hypothesis.HealthCheck.function_scoped_fixture,
62            hypothesis.HealthCheck.too_slow,
63        ]
64    )
65)
66hypothesis.settings.load_profile('ci' if testutils.ON_CI else 'default')
67
68
69def _apply_platform_markers(config, item):
70    """Apply a skip marker to a given item."""
71    markers = [
72        ('posix',
73         pytest.mark.skipif,
74         not utils.is_posix,
75         "Requires a POSIX os"),
76        ('windows',
77         pytest.mark.skipif,
78         not utils.is_windows,
79         "Requires Windows"),
80        ('linux',
81         pytest.mark.skipif,
82         not utils.is_linux,
83         "Requires Linux"),
84        ('mac',
85         pytest.mark.skipif,
86         not utils.is_mac,
87         "Requires macOS"),
88        ('not_mac',
89         pytest.mark.skipif,
90         utils.is_mac,
91         "Skipped on macOS"),
92        ('not_frozen',
93         pytest.mark.skipif,
94         getattr(sys, 'frozen', False),
95         "Can't be run when frozen"),
96        ('not_flatpak',
97         pytest.mark.skipif,
98         version.is_flatpak(),
99         "Can't be run with Flatpak"),
100        ('frozen',
101         pytest.mark.skipif,
102         not getattr(sys, 'frozen', False),
103         "Can only run when frozen"),
104        ('ci',
105         pytest.mark.skipif,
106         not testutils.ON_CI,
107         "Only runs on CI."),
108        ('no_ci',
109         pytest.mark.skipif,
110         testutils.ON_CI,
111         "Skipped on CI."),
112        ('unicode_locale',
113         pytest.mark.skipif,
114         sys.getfilesystemencoding() == 'ascii',
115         "Skipped because of ASCII locale"),
116    ]
117
118    for searched_marker, new_marker_kind, condition, default_reason in markers:
119        marker = item.get_closest_marker(searched_marker)
120        if not marker or not condition:
121            continue
122
123        if 'reason' in marker.kwargs:
124            reason = '{}: {}'.format(default_reason, marker.kwargs['reason'])
125            del marker.kwargs['reason']
126        else:
127            reason = default_reason + '.'
128        new_marker = new_marker_kind(condition, *marker.args,
129                                     reason=reason, **marker.kwargs)
130        item.add_marker(new_marker)
131
132
133def pytest_collection_modifyitems(config, items):
134    """Handle custom markers.
135
136    pytest hook called after collection has been performed.
137
138    Adds a marker named "gui" which can be used to filter gui tests from the
139    command line.
140
141    For example:
142
143        pytest -m "not gui"  # run all tests except gui tests
144        pytest -m "gui"  # run only gui tests
145
146    It also handles the platform specific markers by translating them to skipif
147    markers.
148
149    Args:
150        items: list of _pytest.main.Node items, where each item represents
151               a python test that will be executed.
152
153    Reference:
154        https://pytest.org/latest/plugins.html
155    """
156    remaining_items = []
157    deselected_items = []
158
159    for item in items:
160        deselected = False
161
162        if 'qapp' in getattr(item, 'fixturenames', ()):
163            item.add_marker('gui')
164
165        if hasattr(item, 'module'):
166            test_basedir = pathlib.Path(__file__).parent
167            module_path = pathlib.Path(item.module.__file__)
168            module_root_dir = module_path.relative_to(test_basedir).parts[0]
169
170            assert module_root_dir in ['end2end', 'unit', 'helpers',
171                                       'test_conftest.py']
172            if module_root_dir == 'end2end':
173                item.add_marker(pytest.mark.end2end)
174
175        _apply_platform_markers(config, item)
176        if list(item.iter_markers('xfail_norun')):
177            item.add_marker(pytest.mark.xfail(run=False))
178
179        if deselected:
180            deselected_items.append(item)
181        else:
182            remaining_items.append(item)
183
184    config.hook.pytest_deselected(items=deselected_items)
185    items[:] = remaining_items
186
187
188def pytest_ignore_collect(path):
189    """Ignore BDD tests if we're unable to run them."""
190    fspath = pathlib.Path(path)
191    skip_bdd = hasattr(sys, 'frozen')
192    rel_path = fspath.relative_to(pathlib.Path(__file__).parent)
193    return rel_path == pathlib.Path('end2end') / 'features' and skip_bdd
194
195
196@pytest.fixture(scope='session')
197def qapp_args():
198    """Make QtWebEngine unit tests run on older Qt versions + newer kernels."""
199    seccomp_args = testutils.seccomp_args(qt_flag=False)
200    if seccomp_args:
201        return [sys.argv[0]] + seccomp_args
202    return []
203
204
205@pytest.fixture(scope='session')
206def qapp(qapp):
207    """Change the name of the QApplication instance."""
208    qapp.setApplicationName('qute_test')
209    return qapp
210
211
212def pytest_addoption(parser):
213    parser.addoption('--qute-delay', action='store', default=0, type=int,
214                     help="Delay between qutebrowser commands.")
215    parser.addoption('--qute-profile-subprocs', action='store_true',
216                     default=False, help="Run cProfile for subprocesses.")
217    parser.addoption('--qute-bdd-webengine', action='store_true',
218                     help='Use QtWebEngine for BDD tests')
219
220
221def pytest_configure(config):
222    webengine_arg = config.getoption('--qute-bdd-webengine')
223    webengine_env = os.environ.get('QUTE_BDD_WEBENGINE', 'false')
224    config.webengine = webengine_arg or webengine_env == 'true'
225    # Fail early if QtWebEngine is not available
226    if config.webengine:
227        import PyQt5.QtWebEngineWidgets
228    earlyinit.configure_pyqt()
229
230
231@pytest.fixture(scope='session', autouse=True)
232def check_display(request):
233    if utils.is_linux and not os.environ.get('DISPLAY', ''):
234        raise Exception("No display and no Xvfb available!")
235
236
237@pytest.fixture(autouse=True)
238def set_backend(monkeypatch, request):
239    """Make sure the backend global is set."""
240    if not request.config.webengine and version.qWebKitVersion:
241        backend = usertypes.Backend.QtWebKit
242    else:
243        backend = usertypes.Backend.QtWebEngine
244    monkeypatch.setattr(objects, 'backend', backend)
245
246
247@pytest.fixture(autouse=True)
248def apply_fake_os(monkeypatch, request):
249    fake_os = request.node.get_closest_marker('fake_os')
250    if not fake_os:
251        return
252
253    name = fake_os.args[0]
254    mac = False
255    windows = False
256    linux = False
257    posix = False
258
259    if name == 'unknown':
260        pass
261    elif name == 'mac':
262        mac = True
263        posix = True
264    elif name == 'windows':
265        windows = True
266    elif name == 'linux':
267        linux = True
268        posix = True
269    elif name == 'posix':
270        posix = True
271    else:
272        raise ValueError("Invalid fake_os {}".format(name))
273
274    monkeypatch.setattr(utils, 'is_mac', mac)
275    monkeypatch.setattr(utils, 'is_linux', linux)
276    monkeypatch.setattr(utils, 'is_windows', windows)
277    monkeypatch.setattr(utils, 'is_posix', posix)
278
279
280@pytest.fixture(scope='session', autouse=True)
281def check_yaml_c_exts():
282    """Make sure PyYAML C extensions are available on CI.
283
284    Not available yet with a nightly Python, see:
285    https://github.com/yaml/pyyaml/issues/416
286    """
287    if testutils.ON_CI and sys.version_info[:2] != (3, 10):
288        from yaml import CLoader
289
290
291@pytest.hookimpl(tryfirst=True, hookwrapper=True)
292def pytest_runtest_makereport(item, call):
293    """Make test information available in fixtures.
294
295    See https://pytest.org/latest/example/simple.html#making-test-result-information-available-in-fixtures
296    """
297    outcome = yield
298    rep = outcome.get_result()
299    setattr(item, "rep_" + rep.when, rep)
300
301
302@pytest.hookimpl(hookwrapper=True)
303def pytest_terminal_summary(terminalreporter):
304    """Group benchmark results on CI."""
305    if testutils.ON_CI:
306        terminalreporter.write_line(
307            testutils.gha_group_begin('Benchmark results'))
308        yield
309        terminalreporter.write_line(testutils.gha_group_end())
310    else:
311        yield
312