1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2
3# Copyright 2015-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"""Tests for qutebrowser.utils.version."""
21
22import io
23import sys
24import os
25import pathlib
26import subprocess
27import contextlib
28import logging
29import textwrap
30import datetime
31import dataclasses
32
33import pytest
34import hypothesis
35import hypothesis.strategies
36from PyQt5.QtCore import PYQT_VERSION_STR
37
38import qutebrowser
39from qutebrowser.config import config, websettings
40from qutebrowser.utils import version, usertypes, utils, standarddir
41from qutebrowser.misc import pastebin, objects, elf
42from qutebrowser.browser import pdfjs
43
44try:
45    from qutebrowser.browser.webengine import webenginesettings
46except ImportError:
47    webenginesettings = None
48
49
50@pytest.mark.parametrize('os_release, expected', [
51    # No file
52    (None, None),
53    # Invalid file
54    ("\n# foo\n foo=bar=baz",
55     version.DistributionInfo(id=None, parsed=version.Distribution.unknown,
56                              pretty='Unknown')),
57    # Archlinux
58    ("""
59        NAME="Arch Linux"
60        PRETTY_NAME="Arch Linux"
61        ID=arch
62        ID_LIKE=archlinux
63        ANSI_COLOR="0;36"
64        HOME_URL="https://www.archlinux.org/"
65        SUPPORT_URL="https://bbs.archlinux.org/"
66        BUG_REPORT_URL="https://bugs.archlinux.org/"
67     """,
68     version.DistributionInfo(
69         id='arch', parsed=version.Distribution.arch, pretty='Arch Linux')),
70    # Ubuntu 14.04
71    ("""
72        NAME="Ubuntu"
73        VERSION="14.04.5 LTS, Trusty Tahr"
74        ID=ubuntu
75        ID_LIKE=debian
76        PRETTY_NAME="Ubuntu 14.04.5 LTS"
77        VERSION_ID="14.04"
78     """,
79     version.DistributionInfo(
80         id='ubuntu', parsed=version.Distribution.ubuntu, pretty='Ubuntu 14.04.5 LTS')),
81    # Ubuntu 17.04
82    ("""
83        NAME="Ubuntu"
84        VERSION="17.04 (Zesty Zapus)"
85        ID=ubuntu
86        ID_LIKE=debian
87        PRETTY_NAME="Ubuntu 17.04"
88        VERSION_ID="17.04"
89     """,
90     version.DistributionInfo(
91         id='ubuntu', parsed=version.Distribution.ubuntu, pretty='Ubuntu 17.04')),
92    # Debian Jessie
93    ("""
94        PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
95        NAME="Debian GNU/Linux"
96        VERSION_ID="8"
97        VERSION="8 (jessie)"
98        ID=debian
99     """,
100     version.DistributionInfo(
101         id='debian', parsed=version.Distribution.debian,
102         pretty='Debian GNU/Linux 8 (jessie)')),
103    # Void Linux
104    ("""
105        NAME="void"
106        ID="void"
107        DISTRIB_ID="void"
108        PRETTY_NAME="void"
109     """,
110     version.DistributionInfo(
111         id='void', parsed=version.Distribution.void, pretty='void')),
112    # Gentoo
113    ("""
114        NAME=Gentoo
115        ID=gentoo
116        PRETTY_NAME="Gentoo/Linux"
117     """,
118     version.DistributionInfo(
119         id='gentoo', parsed=version.Distribution.gentoo, pretty='Gentoo/Linux')),
120    # Fedora
121    ("""
122        NAME=Fedora
123        VERSION="25 (Twenty Five)"
124        ID=fedora
125        VERSION_ID=25
126        PRETTY_NAME="Fedora 25 (Twenty Five)"
127     """,
128     version.DistributionInfo(
129         id='fedora', parsed=version.Distribution.fedora,
130         pretty='Fedora 25 (Twenty Five)')),
131    # OpenSUSE
132    ("""
133        NAME="openSUSE Leap"
134        VERSION="42.2"
135        ID=opensuse
136        ID_LIKE="suse"
137        VERSION_ID="42.2"
138        PRETTY_NAME="openSUSE Leap 42.2"
139     """,
140     version.DistributionInfo(
141         id='opensuse', parsed=version.Distribution.opensuse,
142         pretty='openSUSE Leap 42.2')),
143    # Linux Mint
144    ("""
145        NAME="Linux Mint"
146        VERSION="18.1 (Serena)"
147        ID=linuxmint
148        ID_LIKE=ubuntu
149        PRETTY_NAME="Linux Mint 18.1"
150        VERSION_ID="18.1"
151     """,
152     version.DistributionInfo(
153         id='linuxmint', parsed=version.Distribution.linuxmint,
154         pretty='Linux Mint 18.1')),
155    # Manjaro
156    ("""
157        NAME="Manjaro Linux"
158        ID=manjaro
159        PRETTY_NAME="Manjaro Linux"
160     """,
161     version.DistributionInfo(
162         id='manjaro', parsed=version.Distribution.manjaro, pretty='Manjaro Linux')),
163    # Funtoo
164    ("""
165        ID="funtoo"
166        NAME="Funtoo GNU/Linux"
167        PRETTY_NAME="Linux"
168     """,
169     version.DistributionInfo(
170         id='funtoo', parsed=version.Distribution.gentoo, pretty='Funtoo GNU/Linux')),
171    # KDE neon
172    ("""
173        NAME="KDE neon"
174        VERSION="5.20"
175        ID=neon
176        ID_LIKE="ubuntu debian"
177        PRETTY_NAME="KDE neon User Edition 5.20"
178        VARIANT="User Edition"
179        VERSION_ID="20.04"
180    """,
181    version.DistributionInfo(
182        id='neon', parsed=version.Distribution.neon,
183        pretty='KDE neon User Edition 5.20')),
184    # Archlinux ARM
185    ("""
186        NAME="Arch Linux ARM"
187        PRETTY_NAME="Arch Linux ARM"
188        ID=archarm
189        ID_LIKE=arch
190    """,
191    version.DistributionInfo(
192        id='archarm', parsed=version.Distribution.arch, pretty='Arch Linux ARM')),
193    # Alpine
194    ("""
195        NAME="Alpine Linux"
196        ID=alpine
197        VERSION_ID=3.12_alpha20200122
198        PRETTY_NAME="Alpine Linux edge"
199    """,
200    version.DistributionInfo(
201        id='alpine', parsed=version.Distribution.alpine, pretty='Alpine Linux edge')),
202    # EndeavourOS
203    ("""
204        NAME="EndeavourOS"
205        PRETTY_NAME="EndeavourOS"
206        ID=endeavouros
207        ID_LIKE=arch
208        BUILD_ID=rolling
209        DOCUMENTATION_URL="https://endeavouros.com/wiki/"
210        LOGO=endeavouros
211    """,
212    version.DistributionInfo(
213        id='endeavouros', parsed=version.Distribution.arch, pretty='EndeavourOS')),
214    # Manjaro ARM
215    ("""
216        NAME="Manjaro-ARM"
217        ID=manjaro-arm
218        ID_LIKE=manjaro arch
219        PRETTY_NAME="Manjaro ARM"
220    """,
221    version.DistributionInfo(
222        id='manjaro-arm', parsed=version.Distribution.manjaro, pretty='Manjaro ARM')),
223    # Artix Linux
224    ("""
225        NAME="Artix Linux"
226        PRETTY_NAME="Artix Linux"
227        ID=artix
228    """,
229    version.DistributionInfo(
230        id='artix', parsed=version.Distribution.arch, pretty='Artix Linux')),
231    # NixOS
232    ("""
233        NAME=NixOS
234        ID=nixos
235        VERSION="21.03pre268206.536fe36e23a (Okapi)"
236        VERSION_CODENAME=okapi
237        VERSION_ID="21.03pre268206.536fe36e23a"
238        PRETTY_NAME="NixOS 21.03 (Okapi)"
239    """,
240    version.DistributionInfo(
241        id='nixos', parsed=version.Distribution.nixos, pretty='NixOS 21.03 (Okapi)')),
242    # NixOS (fake fourth version component)
243    ("""
244        NAME=NixOS
245        ID=nixos
246        VERSION="21.05.20210402.1dead (Okapi)"
247    """,
248    version.DistributionInfo(
249        id='nixos', parsed=version.Distribution.nixos, pretty='NixOS')),
250    # SolusOS
251    ("""
252        NAME="Solus"
253        VERSION="4.2"
254        ID="solus"
255        VERSION_CODENAME=fortitude
256        VERSION_ID="4.2"
257        PRETTY_NAME="Solus 4.2 Fortitude"
258    """,
259    version.DistributionInfo(
260        id='solus', parsed=version.Distribution.solus, pretty='Solus 4.2 Fortitude')),
261    # KDE Platform
262    ("""
263        NAME=KDE
264        VERSION="5.12 (Flatpak runtime)"
265        VERSION_ID="5.12"
266        ID=org.kde.Platform
267    """,
268    version.DistributionInfo(
269        id='org.kde.Platform', parsed=version.Distribution.kde_flatpak, pretty='KDE')),
270    # No PRETTY_NAME
271    ("""
272        NAME="Tux"
273        ID=tux
274    """,
275    version.DistributionInfo(
276        id='tux', parsed=version.Distribution.unknown, pretty='Tux')),
277    # Invalid multi-line value
278    ("""
279        ID=tux
280        PRETTY_NAME="Multiline
281        Text"
282    """,
283    version.DistributionInfo(
284        id='tux', parsed=version.Distribution.unknown, pretty='Multiline')),
285])
286def test_distribution(tmp_path, monkeypatch, os_release, expected):
287    os_release_file = tmp_path / 'os-release'
288    if os_release is not None:
289        os_release_file.write_text(textwrap.dedent(os_release), encoding="utf-8")
290    monkeypatch.setenv('QUTE_FAKE_OS_RELEASE', str(os_release_file))
291
292    assert version.distribution() == expected
293
294
295@pytest.mark.parametrize('has_env', [True, False])
296@pytest.mark.parametrize('has_file', [True, False])
297def test_is_flatpak(monkeypatch, tmp_path, has_env, has_file):
298    if has_env:
299        monkeypatch.setenv('FLATPAK_ID', 'org.qutebrowser.qutebrowser')
300    else:
301        monkeypatch.delenv('FLATPAK_ID', raising=False)
302
303    fake_info_path = tmp_path / '.flatpak_info'
304    if has_file:
305        lines = [
306            "[Application]",
307            "name=org.qutebrowser.qutebrowser",
308            "runtime=runtime/org.kde.Platform/x86_64/5.15",
309        ]
310        fake_info_path.write_text('\n'.join(lines))
311    else:
312        assert not fake_info_path.exists()
313    monkeypatch.setattr(version, '_FLATPAK_INFO_PATH', str(fake_info_path))
314
315    assert version.is_flatpak() == (has_env or has_file)
316
317
318class GitStrSubprocessFake:
319    """Object returned by the git_str_subprocess_fake fixture.
320
321    This provides a function which is used to patch _git_str_subprocess.
322
323    Attributes:
324        retval: The value to return when called. Needs to be set before func is
325                called.
326    """
327
328    UNSET = object()
329
330    def __init__(self):
331        self.retval = self.UNSET
332
333    def func(self, gitpath):
334        """Function called instead of _git_str_subprocess.
335
336        Checks whether the path passed is what we expected, and returns
337        self.retval.
338        """
339        if self.retval is self.UNSET:
340            raise ValueError("func got called without retval being set!")
341        retval = self.retval
342        self.retval = self.UNSET
343        gitpath = pathlib.Path(gitpath).resolve()
344        expected = pathlib.Path(qutebrowser.__file__).parent.parent
345        assert gitpath == expected
346        return retval
347
348
349class TestGitStr:
350
351    """Tests for _git_str()."""
352
353    @pytest.fixture
354    def commit_file_mock(self, mocker):
355        """Fixture providing a mock for resources.read_file for git-commit-id.
356
357        On fixture teardown, it makes sure it got called with git-commit-id as
358        argument.
359        """
360        mocker.patch('qutebrowser.utils.version.subprocess',
361                     side_effect=AssertionError)
362        m = mocker.patch('qutebrowser.utils.version.resources.read_file')
363        yield m
364        m.assert_called_with('git-commit-id')
365
366    @pytest.fixture
367    def git_str_subprocess_fake(self, mocker, monkeypatch):
368        """Fixture patching _git_str_subprocess with a GitStrSubprocessFake."""
369        mocker.patch('qutebrowser.utils.version.subprocess',
370                     side_effect=AssertionError)
371        fake = GitStrSubprocessFake()
372        monkeypatch.setattr(version, '_git_str_subprocess', fake.func)
373        return fake
374
375    def test_frozen_ok(self, commit_file_mock, monkeypatch):
376        """Test with sys.frozen=True and a successful git-commit-id read."""
377        monkeypatch.setattr(version.sys, 'frozen', True, raising=False)
378        commit_file_mock.return_value = 'deadbeef'
379        assert version._git_str() == 'deadbeef'
380
381    def test_frozen_oserror(self, caplog, commit_file_mock, monkeypatch):
382        """Test with sys.frozen=True and OSError when reading git-commit-id."""
383        monkeypatch.setattr(version.sys, 'frozen', True, raising=False)
384        commit_file_mock.side_effect = OSError
385        with caplog.at_level(logging.ERROR, 'misc'):
386            assert version._git_str() is None
387
388    @pytest.mark.not_frozen
389    def test_normal_successful(self, git_str_subprocess_fake):
390        """Test with git returning a successful result."""
391        git_str_subprocess_fake.retval = 'c0ffeebabe'
392        assert version._git_str() == 'c0ffeebabe'
393
394    @pytest.mark.frozen
395    def test_normal_successful_frozen(self, git_str_subprocess_fake):
396        """Test with git returning a successful result."""
397        # The value is defined in scripts/freeze_tests.py.
398        assert version._git_str() == 'fake-frozen-git-commit'
399
400    def test_normal_error(self, commit_file_mock, git_str_subprocess_fake):
401        """Test without repo (but git-commit-id)."""
402        git_str_subprocess_fake.retval = None
403        commit_file_mock.return_value = '1b4d1dea'
404        assert version._git_str() == '1b4d1dea'
405
406    def test_normal_path_oserror(self, mocker, git_str_subprocess_fake,
407                                 caplog):
408        """Test with things raising OSError."""
409        m = mocker.patch('qutebrowser.utils.version.os')
410        m.path.join.side_effect = OSError
411        mocker.patch('qutebrowser.utils.version.resources.read_file',
412                     side_effect=OSError)
413        with caplog.at_level(logging.ERROR, 'misc'):
414            assert version._git_str() is None
415
416    @pytest.mark.not_frozen
417    def test_normal_path_nofile(self, monkeypatch, caplog,
418                                git_str_subprocess_fake, commit_file_mock):
419        """Test with undefined __file__ but available git-commit-id."""
420        monkeypatch.delattr(version, '__file__')
421        commit_file_mock.return_value = '0deadcode'
422        with caplog.at_level(logging.ERROR, 'misc'):
423            assert version._git_str() == '0deadcode'
424        assert caplog.messages == ["Error while getting git path"]
425
426
427def _has_git():
428    """Check if git is installed."""
429    try:
430        subprocess.run(['git', '--version'], stdout=subprocess.DEVNULL,
431                       stderr=subprocess.DEVNULL, check=True)
432    except (OSError, subprocess.CalledProcessError):
433        return False
434    else:
435        return True
436
437
438# Decorator for tests needing git, so they get skipped when it's unavailable.
439needs_git = pytest.mark.skipif(not _has_git(), reason='Needs git installed.')
440
441
442class TestGitStrSubprocess:
443
444    """Tests for _git_str_subprocess."""
445
446    @pytest.fixture
447    def git_repo(self, tmp_path):
448        """A fixture to create a temporary git repo.
449
450        Some things are tested against a real repo so we notice if something in
451        git would change, or we call git incorrectly.
452        """
453        def _git(*args):
454            """Helper closure to call git."""
455            env = os.environ.copy()
456            env.update({
457                'GIT_AUTHOR_NAME': 'qutebrowser testsuite',
458                'GIT_AUTHOR_EMAIL': 'mail@qutebrowser.org',
459                'GIT_AUTHOR_DATE': 'Thu  1 Jan 01:00:00 CET 1970',
460                'GIT_COMMITTER_NAME': 'qutebrowser testsuite',
461                'GIT_COMMITTER_EMAIL': 'mail@qutebrowser.org',
462                'GIT_COMMITTER_DATE': 'Thu  1 Jan 01:00:00 CET 1970',
463            })
464            if utils.is_windows:
465                # If we don't call this with shell=True it might fail under
466                # some environments on Windows...
467                # https://bugs.python.org/issue24493
468                subprocess.run(
469                    'git -C "{}" {}'.format(tmp_path, ' '.join(args)),
470                    env=env, check=True, shell=True)
471            else:
472                subprocess.run(
473                    ['git', '-C', str(tmp_path)] + list(args),
474                    check=True, env=env)
475
476        (tmp_path / 'file').write_text("Hello World!", encoding='utf-8')
477        _git('init')
478        _git('add', 'file')
479        _git('commit', '-am', 'foo', '--no-verify', '--no-edit',
480             '--no-post-rewrite', '--quiet', '--no-gpg-sign')
481        _git('tag', 'foobar')
482        return tmp_path
483
484    @needs_git
485    def test_real_git(self, git_repo):
486        """Test with a real git repository."""
487        branch_name = subprocess.run(
488            ['git', 'config', 'init.defaultBranch'],
489            check=False,
490            stdout=subprocess.PIPE,
491            encoding='utf-8',
492        ).stdout.strip()
493        if not branch_name:
494            branch_name = 'master'
495
496        ret = version._git_str_subprocess(str(git_repo))
497        assert ret == f'6e4b65a on {branch_name} (1970-01-01 01:00:00 +0100)'
498
499    def test_missing_dir(self, tmp_path):
500        """Test with a directory which doesn't exist."""
501        ret = version._git_str_subprocess(str(tmp_path / 'does-not-exist'))
502        assert ret is None
503
504    @pytest.mark.parametrize('exc', [
505        OSError,
506        subprocess.CalledProcessError(1, 'foobar')
507    ])
508    def test_exception(self, exc, mocker, tmp_path):
509        """Test with subprocess.run raising an exception.
510
511        Args:
512            exc: The exception to raise.
513        """
514        m = mocker.patch('qutebrowser.utils.version.os')
515        m.path.isdir.return_value = True
516        mocker.patch('qutebrowser.utils.version.subprocess.run',
517                     side_effect=exc)
518        ret = version._git_str_subprocess(str(tmp_path))
519        assert ret is None
520
521
522class ReleaseInfoFake:
523
524    """An object providing fakes for glob.glob/open for test_release_info.
525
526    Attributes:
527        _files: The files which should be returned, or None if an exception
528        should be raised. A {filename: [lines]} dict.
529    """
530
531    def __init__(self, files):
532        self._files = files
533
534    def glob_fake(self, pattern):
535        """Fake for glob.glob.
536
537        Verifies the arguments and returns the files listed in self._files, or
538        a single fake file if an exception is expected.
539        """
540        assert pattern == '/etc/*-release'
541        if self._files is None:
542            return ['fake-file']
543        else:
544            return sorted(self._files)
545
546    @contextlib.contextmanager
547    def open_fake(self, filename, mode, encoding):
548        """Fake for open().
549
550        Verifies the arguments and returns a StringIO with the content listed
551        in self._files.
552        """
553        assert mode == 'r'
554        assert encoding == 'utf-8'
555        if self._files is None:
556            raise OSError
557        yield io.StringIO(''.join(self._files[filename]))
558
559
560@pytest.mark.parametrize('files, expected', [
561    # no files -> no output
562    ({}, []),
563    # empty files are stripped
564    ({'file': ['']}, []),
565    ({'file': []}, []),
566    # newlines at EOL are stripped
567    (
568        {'file1': ['foo\n', 'bar\n'], 'file2': ['baz\n']},
569        [('file1', 'foo\nbar'), ('file2', 'baz')]
570    ),
571    # blacklisted lines
572    (
573        {'file': ['HOME_URL=example.com\n', 'NAME=FOO']},
574        [('file', 'NAME=FOO')]
575    ),
576    # only blacklisted lines
577    ({'file': ['HOME_URL=example.com']}, []),
578    # broken file
579    (None, []),
580])
581def test_release_info(files, expected, caplog, monkeypatch):
582    """Test _release_info().
583
584    Args:
585        files: The file dict passed to ReleaseInfoFake.
586        expected: The expected _release_info output.
587    """
588    fake = ReleaseInfoFake(files)
589    monkeypatch.setattr(version.glob, 'glob', fake.glob_fake)
590    monkeypatch.setattr(version, 'open', fake.open_fake, raising=False)
591    with caplog.at_level(logging.ERROR, 'misc'):
592        assert version._release_info() == expected
593    if files is None:
594        assert caplog.messages == ["Error while reading fake-file."]
595
596
597@pytest.mark.parametrize('equal', [True, False])
598def test_path_info(monkeypatch, equal):
599    """Test _path_info().
600
601    Args:
602        equal: Whether system data / data and system config / config are equal.
603    """
604    patches = {
605        'config': lambda auto=False: (
606            'AUTO CONFIG PATH' if auto and not equal
607            else 'CONFIG PATH'),
608        'data': lambda system=False: (
609            'SYSTEM DATA PATH' if system and not equal
610            else 'DATA PATH'),
611        'cache': lambda: 'CACHE PATH',
612        'runtime': lambda: 'RUNTIME PATH',
613    }
614
615    for name, val in patches.items():
616        monkeypatch.setattr(version.standarddir, name, val)
617
618    pathinfo = version._path_info()
619
620    assert pathinfo['config'] == 'CONFIG PATH'
621    assert pathinfo['data'] == 'DATA PATH'
622    assert pathinfo['cache'] == 'CACHE PATH'
623    assert pathinfo['runtime'] == 'RUNTIME PATH'
624
625    if equal:
626        assert 'auto config' not in pathinfo
627        assert 'system data' not in pathinfo
628    else:
629        assert pathinfo['auto config'] == 'AUTO CONFIG PATH'
630        assert pathinfo['system data'] == 'SYSTEM DATA PATH'
631
632
633@pytest.fixture
634def import_fake(stubs, monkeypatch):
635    """Fixture to patch imports using ImportFake."""
636    fake = stubs.ImportFake({mod: True for mod in version.MODULE_INFO}, monkeypatch)
637    fake.patch()
638    return fake
639
640
641class TestModuleVersions:
642
643    """Tests for _module_versions() and ModuleInfo."""
644
645    def test_all_present(self, import_fake):
646        """Test with all modules present in version 1.2.3."""
647        expected = []
648        for name in import_fake.modules:
649            version.MODULE_INFO[name]._reset_cache()
650            if '__version__' not in version.MODULE_INFO[name]._version_attributes:
651                expected.append('{}: yes'.format(name))
652            else:
653                expected.append('{}: 1.2.3'.format(name))
654        assert version._module_versions() == expected
655
656    @pytest.mark.parametrize('module, idx, expected', [
657        ('colorama', 1, 'colorama: no'),
658        ('adblock', 5, 'adblock: no'),
659    ])
660    def test_missing_module(self, module, idx, expected, import_fake):
661        """Test with a module missing.
662
663        Args:
664            module: The name of the missing module.
665            idx: The index where the given text is expected.
666            expected: The expected text.
667        """
668        import_fake.modules[module] = False
669        # Needed after mocking the module
670        mod_info = version.MODULE_INFO[module]
671        mod_info._reset_cache()
672
673        assert version._module_versions()[idx] == expected
674
675        for method_name, expected_result in [
676            ("is_installed", False),
677            ("is_usable", False),
678            ("get_version", None),
679            ("is_outdated", None)
680        ]:
681            method = getattr(mod_info, method_name)
682            # With hot cache
683            mod_info._initialize_info()
684            assert method() == expected_result
685            # With cold cache
686            mod_info._reset_cache()
687            assert method() == expected_result
688
689    def test_outdated_adblock(self, import_fake):
690        """Test that warning is shown when adblock module is outdated."""
691        mod_info = version.MODULE_INFO["adblock"]
692        fake_version = "0.1.0"
693
694        # Needed after mocking version attribute
695        mod_info._reset_cache()
696
697        assert mod_info.min_version is not None
698        assert fake_version < mod_info.min_version
699        import_fake.version = fake_version
700
701        assert mod_info.is_installed()
702        assert mod_info.is_outdated()
703        assert not mod_info.is_usable()
704
705        expected = f"adblock: {fake_version} (< {mod_info.min_version}, outdated)"
706        assert version._module_versions()[5] == expected
707
708    @pytest.mark.parametrize('attribute, expected_modules', [
709        ('VERSION', ['colorama']),
710        ('SIP_VERSION_STR', ['sip']),
711        (None, []),
712    ])
713    def test_version_attribute(self, attribute, expected_modules, import_fake):
714        """Test with a different version attribute.
715
716        VERSION is tested for old colorama versions, and None to make sure
717        things still work if some package suddenly doesn't have __version__.
718
719        Args:
720            attribute: The name of the version attribute.
721            expected: The expected return value.
722        """
723        import_fake.version_attribute = attribute
724
725        for mod_info in version.MODULE_INFO.values():
726            # Invalidate the "version cache" since we just mocked some of the
727            # attributes.
728            mod_info._reset_cache()
729
730        expected = []
731        for name in import_fake.modules:
732            mod_info = version.MODULE_INFO[name]
733            if name in expected_modules:
734                assert mod_info.get_version() == "1.2.3"
735                expected.append('{}: 1.2.3'.format(name))
736            else:
737                assert mod_info.get_version() is None
738                expected.append('{}: yes'.format(name))
739
740        assert version._module_versions() == expected
741
742    @pytest.mark.parametrize('name, has_version', [
743        ('sip', False),
744        ('colorama', True),
745        ('jinja2', True),
746        ('pygments', True),
747        ('yaml', True),
748        ('adblock', True),
749        ('dataclasses', False),
750        ('importlib_resources', False),
751    ])
752    def test_existing_attributes(self, name, has_version):
753        """Check if all dependencies have an expected __version__ attribute.
754
755        The aim of this test is to fail if modules suddenly don't have a
756        __version__ attribute anymore in a newer version.
757
758        Args:
759            name: The name of the module to check.
760            has_version: Whether a __version__ attribute is expected.
761        """
762        module = pytest.importorskip(name)
763        assert hasattr(module, '__version__') == has_version
764
765    def test_existing_sip_attribute(self):
766        """Test if sip has a SIP_VERSION_STR attribute.
767
768        The aim of this test is to fail if that gets missing in some future
769        version of sip.
770        """
771        from qutebrowser.qt import sip
772        assert isinstance(sip.SIP_VERSION_STR, str)
773
774
775class TestOsInfo:
776
777    """Tests for _os_info."""
778
779    @pytest.mark.fake_os('linux')
780    def test_linux_fake(self, monkeypatch):
781        """Test with a fake Linux.
782
783        No args because osver is set to '' if the OS is linux.
784        """
785        monkeypatch.setattr(version, '_release_info',
786                            lambda: [('releaseinfo', 'Hello World')])
787        ret = version._os_info()
788        expected = ['OS Version: ', '',
789                    '--- releaseinfo ---', 'Hello World']
790        assert ret == expected
791
792    @pytest.mark.fake_os('windows')
793    def test_windows_fake(self, monkeypatch):
794        """Test with a fake Windows."""
795        monkeypatch.setattr(version.platform, 'win32_ver',
796                            lambda: ('eggs', 'bacon', 'ham', 'spam'))
797        ret = version._os_info()
798        expected = ['OS Version: eggs, bacon, ham, spam']
799        assert ret == expected
800
801    @pytest.mark.fake_os('mac')
802    @pytest.mark.parametrize('mac_ver, mac_ver_str', [
803        (('x', ('', '', ''), 'y'), 'x, y'),
804        (('', ('', '', ''), ''), ''),
805        (('x', ('1', '2', '3'), 'y'), 'x, 1.2.3, y'),
806    ])
807    def test_mac_fake(self, monkeypatch, mac_ver, mac_ver_str):
808        """Test with a fake macOS.
809
810        Args:
811            mac_ver: The tuple to set platform.mac_ver() to.
812            mac_ver_str: The expected Mac version string in version._os_info().
813        """
814        monkeypatch.setattr(version.platform, 'mac_ver', lambda: mac_ver)
815        ret = version._os_info()
816        expected = ['OS Version: {}'.format(mac_ver_str)]
817        assert ret == expected
818
819    @pytest.mark.fake_os('posix')
820    def test_posix_fake(self, monkeypatch):
821        """Test with a fake posix platform."""
822        uname_tuple = ('PosixOS', 'localhost', '1.0', '1.0', 'i386', 'i386')
823        monkeypatch.setattr(version.platform, 'uname', lambda: uname_tuple)
824        ret = version._os_info()
825        expected = ['OS Version: PosixOS localhost 1.0 1.0 i386 i386']
826        assert ret == expected
827
828    @pytest.mark.fake_os('unknown')
829    def test_unknown_fake(self):
830        """Test with a fake unknown platform."""
831        ret = version._os_info()
832        expected = ['OS Version: ?']
833        assert ret == expected
834
835    @pytest.mark.linux
836    def test_linux_real(self):
837        """Make sure there are no exceptions with a real Linux."""
838        version._os_info()
839
840    @pytest.mark.windows
841    def test_windows_real(self):
842        """Make sure there are no exceptions with a real Windows."""
843        version._os_info()
844
845    @pytest.mark.mac
846    def test_mac_real(self):
847        """Make sure there are no exceptions with a real macOS."""
848        version._os_info()
849
850    @pytest.mark.posix
851    def test_posix_real(self):
852        """Make sure there are no exceptions with a real posix."""
853        version._os_info()
854
855
856class TestPDFJSVersion:
857
858    """Tests for _pdfjs_version."""
859
860    def test_not_found(self, mocker):
861        mocker.patch('qutebrowser.utils.version.pdfjs.get_pdfjs_res_and_path',
862                     side_effect=pdfjs.PDFJSNotFound('/build/pdf.js'))
863        assert version._pdfjs_version() == 'no'
864
865    def test_unknown(self, monkeypatch):
866        monkeypatch.setattr(
867            'qutebrowser.utils.version.pdfjs.get_pdfjs_res_and_path',
868            lambda path: (b'foobar', None))
869        assert version._pdfjs_version() == 'unknown (bundled)'
870
871    @pytest.mark.parametrize('varname', [
872        'PDFJS.version',  # v1.10.100 and older
873        'var pdfjsVersion',  # v2.0.943
874        'const pdfjsVersion',  # v2.5.207
875    ])
876    def test_known(self, monkeypatch, varname):
877        pdfjs_code = textwrap.dedent("""
878            // Initializing PDFJS global object (if still undefined)
879            if (typeof PDFJS === 'undefined') {
880              (typeof window !== 'undefined' ? window : this).PDFJS = {};
881            }
882
883            VARNAME = '1.2.109';
884            PDFJS.build = '875588d';
885
886            (function pdfjsWrapper() {
887              // Use strict in our context only - users might not want it
888              'use strict';
889        """.replace('VARNAME', varname)).strip().encode('utf-8')
890        monkeypatch.setattr(
891            'qutebrowser.utils.version.pdfjs.get_pdfjs_res_and_path',
892            lambda path: (pdfjs_code, '/foo/bar/pdf.js'))
893        assert version._pdfjs_version() == '1.2.109 (/foo/bar/pdf.js)'
894
895    def test_real_file(self, data_tmpdir):
896        """Test against the real file if pdfjs was found."""
897        try:
898            pdfjs.get_pdfjs_res_and_path('build/pdf.js')
899        except pdfjs.PDFJSNotFound:
900            pytest.skip("No pdfjs found")
901        ver = version._pdfjs_version()
902        assert ver.split()[0] not in ['no', 'unknown'], ver
903
904
905class TestWebEngineVersions:
906
907    @pytest.mark.parametrize('version, expected', [
908        (
909            version.WebEngineVersions(
910                webengine=utils.VersionNumber(5, 15, 2),
911                chromium=None,
912                source='UA'),
913            "QtWebEngine 5.15.2",
914        ),
915        (
916            version.WebEngineVersions(
917                webengine=utils.VersionNumber(5, 15, 2),
918                chromium='87.0.4280.144',
919                source='UA'),
920            "QtWebEngine 5.15.2, based on Chromium 87.0.4280.144",
921        ),
922        (
923            version.WebEngineVersions(
924                webengine=utils.VersionNumber(5, 15, 2),
925                chromium='87.0.4280.144',
926                source='faked'),
927            "QtWebEngine 5.15.2, based on Chromium 87.0.4280.144 (from faked)",
928        ),
929    ])
930    def test_str(self, version, expected):
931        assert str(version) == expected
932
933    @pytest.mark.parametrize('version, expected', [
934        (
935            version.WebEngineVersions(
936                webengine=utils.VersionNumber(5, 15, 2),
937                chromium=None,
938                source='test'),
939            None,
940        ),
941        (
942            version.WebEngineVersions(
943                webengine=utils.VersionNumber(5, 15, 2),
944                chromium='87.0.4280.144',
945                source='test'),
946            87,
947        ),
948    ])
949    def test_chromium_major(self, version, expected):
950        assert version.chromium_major == expected
951
952    def test_from_ua(self):
953        ua = websettings.UserAgent(
954            os_info='X11; Linux x86_64',
955            webkit_version='537.36',
956            upstream_browser_key='Chrome',
957            upstream_browser_version='83.0.4103.122',
958            qt_key='QtWebEngine',
959            qt_version='5.15.2',
960        )
961        expected = version.WebEngineVersions(
962            webengine=utils.VersionNumber(5, 15, 2),
963            chromium='83.0.4103.122',
964            source='UA',
965        )
966        assert version.WebEngineVersions.from_ua(ua) == expected
967
968    def test_from_elf(self):
969        elf_version = elf.Versions(webengine='5.15.2', chromium='83.0.4103.122')
970        expected = version.WebEngineVersions(
971            webengine=utils.VersionNumber(5, 15, 2),
972            chromium='83.0.4103.122',
973            source='ELF',
974        )
975        assert version.WebEngineVersions.from_elf(elf_version) == expected
976
977    @pytest.mark.parametrize('pyqt_version, chromium_version', [
978        ('5.12.10', '69.0.3497.128'),
979        ('5.14.2', '77.0.3865.129'),
980        ('5.15.1', '80.0.3987.163'),
981        ('5.15.2', '83.0.4103.122'),
982        ('5.15.3', '87.0.4280.144'),
983        ('5.15.4', '87.0.4280.144'),
984        ('5.15.5', '87.0.4280.144'),
985    ])
986    def test_from_pyqt(self, freezer, pyqt_version, chromium_version):
987        if freezer and pyqt_version in ['5.15.3', '5.15.4', '5.15.5']:
988            chromium_version = '83.0.4103.122'
989            expected_pyqt_version = '5.15.2'
990        else:
991            expected_pyqt_version = pyqt_version
992
993        expected = version.WebEngineVersions(
994            webengine=utils.VersionNumber.parse(expected_pyqt_version),
995            chromium=chromium_version,
996            source='PyQt',
997        )
998        assert version.WebEngineVersions.from_pyqt(pyqt_version) == expected
999
1000    def test_real_chromium_version(self, qapp):
1001        """Compare the inferred Chromium version with the real one."""
1002        pyqt_webengine_version = version._get_pyqt_webengine_qt_version()
1003        if pyqt_webengine_version is None:
1004            if '.dev' in PYQT_VERSION_STR:
1005                pytest.skip("dev version of PyQt5")
1006
1007            try:
1008                from PyQt5.QtWebEngine import (
1009                    PYQT_WEBENGINE_VERSION_STR, PYQT_WEBENGINE_VERSION)
1010            except ImportError as e:
1011                # QtWebKit or QtWebEngine < 5.13
1012                pytest.skip(str(e))
1013
1014            if PYQT_WEBENGINE_VERSION >= 0x050F02:
1015                # Starting with Qt 5.15.2, we can only do bad guessing anyways...
1016                pytest.skip("Could be QtWebEngine 5.15.2 or 5.15.3")
1017
1018            pyqt_webengine_version = PYQT_WEBENGINE_VERSION_STR
1019
1020        versions = version.WebEngineVersions.from_pyqt(pyqt_webengine_version)
1021        inferred = versions.chromium
1022
1023        webenginesettings.init_user_agent()
1024        real = webenginesettings.parsed_user_agent.upstream_browser_version
1025
1026        assert inferred == real
1027
1028
1029class FakeQSslSocket:
1030
1031    """Fake for the QSslSocket Qt class.
1032
1033    Attributes:
1034        _version: What QSslSocket::sslLibraryVersionString() should return.
1035        _support: Whether SSL is supported.
1036    """
1037
1038    def __init__(self, version=None, support=True):
1039        self._version = version
1040        self._support = support
1041
1042    def supportsSsl(self):
1043        """Fake for QSslSocket::supportsSsl()."""
1044        return self._support
1045
1046    def sslLibraryVersionString(self):
1047        """Fake for QSslSocket::sslLibraryVersionString()."""
1048        if self._version is None:
1049            raise utils.Unreachable("Got called with version None!")
1050        return self._version
1051
1052
1053_QTWE_USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) "
1054                    "AppleWebKit/537.36 (KHTML, like Gecko) "
1055                    "QtWebEngine/5.14.0 Chrome/{} Safari/537.36")
1056
1057
1058class TestChromiumVersion:
1059
1060    @pytest.fixture(autouse=True)
1061    def clear_parsed_ua(self, monkeypatch):
1062        pytest.importorskip('PyQt5.QtWebEngineWidgets')
1063        if webenginesettings is not None:
1064            # Not available with QtWebKit
1065            monkeypatch.setattr(webenginesettings, 'parsed_user_agent', None)
1066
1067    def test_fake_ua(self, monkeypatch, caplog):
1068        ver = '77.0.3865.98'
1069        webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format(ver))
1070
1071        assert version.qtwebengine_versions().chromium == ver
1072
1073    def test_prefers_saved_user_agent(self, monkeypatch):
1074        webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87'))
1075
1076        class FakeProfile:
1077            def defaultProfile(self):
1078                raise AssertionError("Should not be called")
1079
1080        monkeypatch.setattr(webenginesettings, 'QWebEngineProfile', FakeProfile())
1081
1082        version.qtwebengine_versions()
1083
1084    def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub):
1085        assert version.qtwebengine_versions().chromium is not None
1086
1087    def test_avoided(self, monkeypatch):
1088        versions = version.qtwebengine_versions(avoid_init=True)
1089        assert versions.source in ['ELF', 'importlib', 'PyQt', 'Qt']
1090
1091    @pytest.fixture
1092    def patch_elf_fail(self, monkeypatch):
1093        """Simulate parsing the version from ELF to fail."""
1094        monkeypatch.setattr(elf, 'parse_webenginecore', lambda: None)
1095
1096    @pytest.fixture
1097    def patch_old_pyqt(self, monkeypatch):
1098        """Simulate an old PyQt without PYQT_WEBENGINE_VERSION_STR."""
1099        monkeypatch.setattr(version, 'PYQT_WEBENGINE_VERSION_STR', None)
1100
1101    @pytest.fixture
1102    def patch_no_importlib(self, monkeypatch, stubs):
1103        """Simulate missing importlib modules."""
1104        import_fake = stubs.ImportFake({
1105            'importlib_metadata': False,
1106            'importlib.metadata': False,
1107        }, monkeypatch)
1108        import_fake.patch()
1109
1110    @pytest.fixture
1111    def importlib_patcher(self, monkeypatch):
1112        """Patch the importlib module."""
1113        def _patch(*, qt, qt5):
1114            try:
1115                import importlib.metadata as importlib_metadata
1116            except ImportError:
1117                importlib_metadata = pytest.importorskip("importlib_metadata")
1118
1119            def _fake_version(name):
1120                if name == 'PyQtWebEngine-Qt':
1121                    outcome = qt
1122                elif name == 'PyQtWebEngine-Qt5':
1123                    outcome = qt5
1124                else:
1125                    raise utils.Unreachable(outcome)
1126
1127                if outcome is None:
1128                    raise importlib_metadata.PackageNotFoundError(name)
1129                return outcome
1130
1131            monkeypatch.setattr(importlib_metadata, 'version', _fake_version)
1132
1133        return _patch
1134
1135    @pytest.fixture
1136    def patch_importlib_no_package(self, importlib_patcher):
1137        """Simulate importlib not finding PyQtWebEngine-Qt[5]."""
1138        importlib_patcher(qt=None, qt5=None)
1139
1140    @pytest.mark.parametrize('patches, sources', [
1141        (['elf_fail'], ['importlib', 'PyQt', 'Qt']),
1142        (['elf_fail', 'old_pyqt'], ['importlib', 'Qt']),
1143        (['elf_fail', 'no_importlib'], ['PyQt', 'Qt']),
1144        (['elf_fail', 'no_importlib', 'old_pyqt'], ['Qt']),
1145        (['elf_fail', 'importlib_no_package'], ['PyQt', 'Qt']),
1146        (['elf_fail', 'importlib_no_package', 'old_pyqt'], ['Qt']),
1147    ], ids=','.join)
1148    def test_simulated(self, request, patches, sources):
1149        """Test various simulated error conditions.
1150
1151        This dynamically gets a list of fixtures (above) to do the patching. It then
1152        checks whether the version it got is from one of the expected sources. Depending
1153        on the environment this test is run in, some sources might fail "naturally",
1154        i.e. without any patching related to them.
1155        """
1156        for patch in patches:
1157            request.getfixturevalue(f'patch_{patch}')
1158
1159        versions = version.qtwebengine_versions(avoid_init=True)
1160        assert versions.source in sources
1161
1162    @pytest.mark.parametrize('qt, qt5, expected', [
1163        (None, '5.15.4', utils.VersionNumber(5, 15, 4)),
1164        ('5.15.3', None, utils.VersionNumber(5, 15, 3)),
1165        ('5.15.3', '5.15.4', utils.VersionNumber(5, 15, 4)),  # -Qt5 takes precedence
1166    ])
1167    def test_importlib(self, qt, qt5, expected, patch_elf_fail, importlib_patcher):
1168        """Test the importlib version logic with different Qt packages.
1169
1170        With PyQtWebEngine 5.15.4, PyQtWebEngine-Qt was renamed to PyQtWebEngine-Qt5.
1171        """
1172        importlib_patcher(qt=qt, qt5=qt5)
1173        versions = version.qtwebengine_versions(avoid_init=True)
1174        assert versions.source == 'importlib'
1175        assert versions.webengine == expected
1176
1177    @pytest.mark.parametrize('override', [
1178        utils.VersionNumber(5, 12, 10),
1179        utils.VersionNumber(5, 15, 3),
1180    ])
1181    def test_override(self, monkeypatch, override):
1182        monkeypatch.setenv('QUTE_QTWEBENGINE_VERSION_OVERRIDE', str(override))
1183        versions = version.qtwebengine_versions(avoid_init=True)
1184        assert versions.source == 'override'
1185        assert versions.webengine == override
1186
1187
1188@dataclasses.dataclass
1189class VersionParams:
1190
1191    name: str
1192    git_commit: bool = True
1193    frozen: bool = False
1194    qapp: bool = True
1195    with_webkit: bool = True
1196    known_distribution: bool = True
1197    ssl_support: bool = True
1198    autoconfig_loaded: bool = True
1199    config_py_loaded: bool = True
1200
1201
1202@pytest.mark.parametrize('params', [
1203    VersionParams('normal'),
1204    VersionParams('no-git-commit', git_commit=False),
1205    VersionParams('frozen', frozen=True),
1206    VersionParams('no-qapp', qapp=False),
1207    VersionParams('no-webkit', with_webkit=False),
1208    VersionParams('unknown-dist', known_distribution=False),
1209    VersionParams('no-ssl', ssl_support=False),
1210    VersionParams('no-autoconfig-loaded', autoconfig_loaded=False),
1211    VersionParams('no-config-py-loaded', config_py_loaded=False),
1212], ids=lambda param: param.name)
1213def test_version_info(params, stubs, monkeypatch, config_stub):
1214    """Test version.version_info()."""
1215    config.instance.config_py_loaded = params.config_py_loaded
1216    import_path = pathlib.Path('/IMPORTPATH').resolve()
1217
1218    patches = {
1219        'qutebrowser.__file__': str(import_path / '__init__.py'),
1220        'qutebrowser.__version__': 'VERSION',
1221        '_git_str': lambda: ('GIT COMMIT' if params.git_commit else None),
1222        'platform.python_implementation': lambda: 'PYTHON IMPLEMENTATION',
1223        'platform.python_version': lambda: 'PYTHON VERSION',
1224        'sys.executable': 'EXECUTABLE PATH',
1225        'PYQT_VERSION_STR': 'PYQT VERSION',
1226        'earlyinit.qt_version': lambda: 'QT VERSION',
1227        '_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'],
1228        '_pdfjs_version': lambda: 'PDFJS VERSION',
1229        'QSslSocket': FakeQSslSocket('SSL VERSION', params.ssl_support),
1230        'platform.platform': lambda: 'PLATFORM',
1231        'platform.architecture': lambda: ('ARCHITECTURE', ''),
1232        '_os_info': lambda: ['OS INFO 1', 'OS INFO 2'],
1233        '_path_info': lambda: {'PATH DESC': 'PATH NAME'},
1234        'objects.qapp': (stubs.FakeQApplication(style='STYLE', platform_name='PLATFORM')
1235                         if params.qapp else None),
1236        'QLibraryInfo.location': (lambda _loc: 'QT PATH'),
1237        'sql.version': lambda: 'SQLITE VERSION',
1238        '_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45),
1239        'config.instance.yaml_loaded': params.autoconfig_loaded,
1240    }
1241
1242    version.opengl_info.cache_clear()
1243    monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION')
1244
1245    substitutions = {
1246        'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '',
1247        'style': '\nStyle: STYLE' if params.qapp else '',
1248        'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp
1249                            else ''),
1250        'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '',
1251        'qt': 'QT VERSION',
1252        'frozen': str(params.frozen),
1253        'import_path': import_path,
1254        'python_path': 'EXECUTABLE PATH',
1255        'uptime': "1:23:45",
1256        'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no",
1257    }
1258
1259    patches['qtwebengine_versions'] = (
1260        lambda avoid_init: version.WebEngineVersions(
1261            webengine=utils.VersionNumber(1, 2, 3),
1262            chromium=None,
1263            source='faked',
1264        )
1265    )
1266
1267    if params.config_py_loaded:
1268        substitutions["config_py_loaded"] = "{} has been loaded".format(
1269            standarddir.config_py())
1270    else:
1271        substitutions["config_py_loaded"] = "no config.py was loaded"
1272
1273    if params.with_webkit:
1274        patches['qWebKitVersion'] = lambda: 'WEBKIT VERSION'
1275        patches['objects.backend'] = usertypes.Backend.QtWebKit
1276        substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)'
1277    else:
1278        monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False)
1279        patches['objects.backend'] = usertypes.Backend.QtWebEngine
1280        substitutions['backend'] = 'QtWebEngine 1.2.3 (from faked)'
1281
1282    if params.known_distribution:
1283        patches['distribution'] = lambda: version.DistributionInfo(
1284            parsed=version.Distribution.arch, pretty='LINUX DISTRIBUTION', id='arch')
1285        substitutions['linuxdist'] = ('\nLinux distribution: '
1286                                      'LINUX DISTRIBUTION (arch)')
1287        substitutions['osinfo'] = ''
1288    else:
1289        patches['distribution'] = lambda: None
1290        substitutions['linuxdist'] = ''
1291        substitutions['osinfo'] = 'OS INFO 1\nOS INFO 2\n'
1292
1293    substitutions['ssl'] = 'SSL VERSION' if params.ssl_support else 'no'
1294
1295    for name, val in patches.items():
1296        monkeypatch.setattr(f'qutebrowser.utils.version.{name}', val)
1297
1298    if params.frozen:
1299        monkeypatch.setattr(sys, 'frozen', True, raising=False)
1300    else:
1301        monkeypatch.delattr(sys, 'frozen', raising=False)
1302
1303    template = version._LOGO.lstrip('\n') + textwrap.dedent("""
1304        qutebrowser vVERSION{git_commit}
1305        Backend: {backend}
1306        Qt: {qt}
1307
1308        PYTHON IMPLEMENTATION: PYTHON VERSION
1309        PyQt: PYQT VERSION
1310
1311        MODULE VERSION 1
1312        MODULE VERSION 2
1313        pdf.js: PDFJS VERSION
1314        sqlite: SQLITE VERSION
1315        QtNetwork SSL: {ssl}
1316        {style}{platform_plugin}{opengl}
1317        Platform: PLATFORM, ARCHITECTURE{linuxdist}
1318        Frozen: {frozen}
1319        Imported from {import_path}
1320        Using Python from {python_path}
1321        Qt library executable path: QT PATH, data path: QT PATH
1322        {osinfo}
1323        Paths:
1324        PATH DESC: PATH NAME
1325
1326        Autoconfig loaded: {autoconfig_loaded}
1327        Config.py: {config_py_loaded}
1328        Uptime: {uptime}
1329    """.lstrip('\n'))
1330
1331    expected = template.rstrip('\n').format(**substitutions)
1332    assert version.version_info() == expected
1333
1334
1335class TestOpenGLInfo:
1336
1337    @pytest.fixture(autouse=True)
1338    def cache_clear(self):
1339        """Clear the lru_cache between tests."""
1340        version.opengl_info.cache_clear()
1341
1342    def test_func(self, qapp):
1343        """Simply call version.opengl_info() and see if it doesn't crash."""
1344        pytest.importorskip("PyQt5.QtOpenGL")
1345        version.opengl_info()
1346
1347    def test_func_fake(self, qapp, monkeypatch):
1348        monkeypatch.setenv('QUTE_FAKE_OPENGL', 'Outtel Inc., 3.0 Messiah 20.0')
1349        info = version.opengl_info()
1350        assert info.vendor == 'Outtel Inc.'
1351        assert info.version_str == '3.0 Messiah 20.0'
1352        assert info.version == (3, 0)
1353        assert info.vendor_specific == 'Messiah 20.0'
1354
1355    @pytest.mark.parametrize('version_str, reason', [
1356        ('blah', 'missing space'),
1357        ('2,x blah', 'parsing int'),
1358    ])
1359    def test_parse_invalid(self, caplog, version_str, reason):
1360        with caplog.at_level(logging.WARNING):
1361            info = version.OpenGLInfo.parse(vendor="vendor",
1362                                            version=version_str)
1363
1364        assert info.version is None
1365        assert info.vendor_specific is None
1366        assert info.vendor == 'vendor'
1367        assert info.version_str == version_str
1368
1369        msg = "Failed to parse OpenGL version ({}): {}".format(
1370            reason, version_str)
1371        assert caplog.messages == [msg]
1372
1373    @hypothesis.given(vendor=hypothesis.strategies.text(),
1374                      version_str=hypothesis.strategies.text())
1375    def test_parse_hypothesis(self, caplog, vendor, version_str):
1376        with caplog.at_level(logging.WARNING):
1377            info = version.OpenGLInfo.parse(vendor=vendor, version=version_str)
1378
1379        assert info.vendor == vendor
1380        assert info.version_str == version_str
1381        assert vendor in str(info)
1382        assert version_str in str(info)
1383
1384    @pytest.mark.parametrize('version_str, expected', [
1385        ("2.1 INTEL-10.36.26", (2, 1)),
1386        ("4.6 (Compatibility Profile) Mesa 20.0.7", (4, 6)),
1387        ("3.0 Mesa 20.0.7", (3, 0)),
1388        ("3.0 Mesa 20.0.6", (3, 0)),
1389        # Not from the wild, but can happen according to standards
1390        ("3.0.2 Mesa 20.0.6", (3, 0, 2)),
1391    ])
1392    def test_version(self, version_str, expected):
1393        info = version.OpenGLInfo.parse(vendor='vendor', version=version_str)
1394        assert info.version == expected
1395
1396    def test_str_gles(self):
1397        info = version.OpenGLInfo(gles=True)
1398        assert str(info) == 'OpenGL ES'
1399
1400
1401@pytest.fixture
1402def pbclient(stubs):
1403    http_stub = stubs.HTTPPostStub()
1404    client = pastebin.PastebinClient(http_stub)
1405    yield client
1406    version.pastebin_url = None
1407
1408
1409def test_pastebin_version(pbclient, message_mock, monkeypatch, qtbot):
1410    """Test version.pastebin_version() sets the url."""
1411    monkeypatch.setattr(version, 'version_info', lambda: 'dummy')
1412    monkeypatch.setattr(utils, 'log_clipboard', True)
1413
1414    version.pastebin_version(pbclient)
1415    pbclient.success.emit("https://www.example.com/\n")
1416
1417    msg = message_mock.getmsg(usertypes.MessageLevel.info)
1418    expected_text = "Version url https://www.example.com/ yanked to clipboard."
1419    assert msg.text == expected_text
1420    assert version.pastebin_url == "https://www.example.com/"
1421
1422
1423def test_pastebin_version_twice(pbclient, monkeypatch):
1424    """Test whether calling pastebin_version twice sends no data."""
1425    monkeypatch.setattr(version, 'version_info', lambda: 'dummy')
1426
1427    version.pastebin_version(pbclient)
1428    pbclient.success.emit("https://www.example.com/\n")
1429
1430    pbclient.url = None
1431    pbclient.data = None
1432    version.pastebin_url = "https://www.example.org/"
1433
1434    version.pastebin_version(pbclient)
1435    assert pbclient.url is None
1436    assert pbclient.data is None
1437    assert version.pastebin_url == "https://www.example.org/"
1438
1439
1440def test_pastebin_version_error(pbclient, caplog, message_mock, monkeypatch):
1441    """Test version.pastebin_version() with errors."""
1442    monkeypatch.setattr(version, 'version_info', lambda: 'dummy')
1443
1444    version.pastebin_url = None
1445    with caplog.at_level(logging.ERROR):
1446        version.pastebin_version(pbclient)
1447        pbclient._client.error.emit("test")
1448
1449    assert version.pastebin_url is None
1450
1451    msg = message_mock.getmsg(usertypes.MessageLevel.error)
1452    assert msg.text == "Failed to pastebin version info: test"
1453
1454
1455def test_uptime(monkeypatch, qapp):
1456    """Test _uptime runs and check if microseconds are dropped."""
1457    monkeypatch.setattr(objects, 'qapp', qapp)
1458
1459    launch_time = datetime.datetime(1, 1, 1, 1, 1, 1, 1)
1460    monkeypatch.setattr(qapp, "launch_time", launch_time, raising=False)
1461
1462    class FakeDateTime(datetime.datetime):
1463        now = lambda x=datetime.datetime(1, 1, 1, 1, 1, 1, 2): x
1464    monkeypatch.setattr(datetime, 'datetime', FakeDateTime)
1465
1466    uptime_delta = version._uptime()
1467    assert uptime_delta == datetime.timedelta(0)
1468