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