From d5a1fff637c5261443562e2078707489f93d7f2b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 Sep 2017 14:08:06 +0200 Subject: [PATCH] Move init stuff from config.py to configinit.py Closes #2997 --- qutebrowser/app.py | 9 +- qutebrowser/config/config.py | 124 +------------- qutebrowser/config/configinit.py | 144 +++++++++++++++++ tests/unit/config/test_config.py | 216 +------------------------ tests/unit/config/test_configinit.py | 234 +++++++++++++++++++++++++++ 5 files changed, 391 insertions(+), 336 deletions(-) create mode 100644 qutebrowser/config/configinit.py create mode 100644 tests/unit/config/test_configinit.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 5bca765eb..1f938c57e 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -43,7 +43,8 @@ import qutebrowser import qutebrowser.resources from qutebrowser.completion.models import miscmodels from qutebrowser.commands import cmdutils, runners, cmdexc -from qutebrowser.config import config, websettings, configexc, configfiles +from qutebrowser.config import (config, websettings, configexc, configfiles, + configinit) from qutebrowser.browser import (urlmarks, adblock, history, browsertab, downloads) from qutebrowser.browser.network import proxy @@ -77,7 +78,7 @@ def run(args): standarddir.init(args) log.init.debug("Initializing config...") - config.early_init(args) + configinit.early_init(args) global qApp qApp = Application(args) @@ -393,7 +394,7 @@ def _init_modules(args, crash_handler): log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(qApp) objreg.register('save-manager', save_manager) - config.late_init(save_manager) + configinit.late_init(save_manager) log.init.debug("Initializing network...") networkmanager.init() @@ -762,7 +763,7 @@ class Application(QApplication): """ self._last_focus_object = None - qt_args = config.qt_args(args) + qt_args = configinit.qt_args(args) log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args)) super().__init__(qt_args) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index e99498947..0365eb049 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -19,18 +19,15 @@ """Configuration storage and config-related utilities.""" -import sys import copy import contextlib import functools from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl -from PyQt5.QtWidgets import QMessageBox from qutebrowser.config import configdata, configexc, configtypes, configfiles -from qutebrowser.utils import (utils, objreg, message, log, usertypes, jinja, - qtutils) -from qutebrowser.misc import objects, msgbox, earlyinit +from qutebrowser.utils import utils, objreg, message, log, jinja, qtutils +from qutebrowser.misc import objects from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.completion.models import configmodel @@ -40,9 +37,7 @@ instance = None key_instance = None # Keeping track of all change filters to validate them later. -_change_filters = [] -# Errors which happened during init, so we can show a message box. -_init_errors = [] +change_filters = [] class change_filter: # pylint: disable=invalid-name @@ -68,7 +63,7 @@ class change_filter: # pylint: disable=invalid-name """ self._option = option self._function = function - _change_filters.append(self) + change_filters.append(self) def validate(self): """Make sure the configured option or prefix exists. @@ -634,114 +629,3 @@ class StyleSheetObserver(QObject): self._obj.setStyleSheet(qss) if update: instance.changed.connect(self._update_stylesheet) - - -def early_init(args): - """Initialize the part of the config which works without a QApplication.""" - configdata.init() - - yaml_config = configfiles.YamlConfig() - - global val, instance, key_instance - instance = Config(yaml_config=yaml_config) - val = ConfigContainer(instance) - key_instance = KeyConfig(instance) - - for cf in _change_filters: - cf.validate() - - configtypes.Font.monospace_fonts = val.fonts.monospace - - config_commands = ConfigCommands(instance, key_instance) - objreg.register('config-commands', config_commands) - - config_api = None - - try: - config_api = configfiles.read_config_py() - # Raised here so we get the config_api back. - if config_api.errors: - raise configexc.ConfigFileErrors('config.py', config_api.errors) - except configexc.ConfigFileErrors as e: - log.config.exception("Error while loading config.py") - _init_errors.append(e) - - try: - if getattr(config_api, 'load_autoconfig', True): - try: - instance.read_yaml() - except configexc.ConfigFileErrors as e: - raise # caught in outer block - except configexc.Error as e: - desc = configexc.ConfigErrorDesc("Error", e) - raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) - except configexc.ConfigFileErrors as e: - log.config.exception("Error while loading config.py") - _init_errors.append(e) - - configfiles.init() - - objects.backend = get_backend(args) - earlyinit.init_with_backend(objects.backend) - - -def get_backend(args): - """Find out what backend to use based on available libraries.""" - try: - import PyQt5.QtWebKit # pylint: disable=unused-variable - except ImportError: - webkit_available = False - else: - webkit_available = qtutils.is_new_qtwebkit() - - str_to_backend = { - 'webkit': usertypes.Backend.QtWebKit, - 'webengine': usertypes.Backend.QtWebEngine, - } - - if args.backend is not None: - return str_to_backend[args.backend] - elif val.backend != 'auto': - return str_to_backend[val.backend] - elif webkit_available: - return usertypes.Backend.QtWebKit - else: - return usertypes.Backend.QtWebEngine - - -def late_init(save_manager): - """Initialize the rest of the config after the QApplication is created.""" - global _init_errors - for err in _init_errors: - errbox = msgbox.msgbox(parent=None, - title="Error while reading config", - text=err.to_html(), - icon=QMessageBox.Warning, - plain_text=False) - errbox.exec_() - _init_errors = [] - - instance.init_save_manager(save_manager) - configfiles.state.init_save_manager(save_manager) - - -def qt_args(namespace): - """Get the Qt QApplication arguments based on an argparse namespace. - - Args: - namespace: The argparse namespace. - - Return: - The argv list to be passed to Qt. - """ - argv = [sys.argv[0]] - - if namespace.qt_flag is not None: - argv += ['--' + flag[0] for flag in namespace.qt_flag] - - if namespace.qt_arg is not None: - for name, value in namespace.qt_arg: - argv += ['--' + name, value] - - argv += ['--' + arg for arg in val.qt_args] - return argv diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py new file mode 100644 index 000000000..27a37b5e9 --- /dev/null +++ b/qutebrowser/config/configinit.py @@ -0,0 +1,144 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Initialization of the configuration.""" + +import sys + +from PyQt5.QtWidgets import QMessageBox + +from qutebrowser.config import (config, configdata, configfiles, configtypes, + configexc) +from qutebrowser.utils import objreg, qtutils, usertypes, log +from qutebrowser.misc import earlyinit, msgbox, objects + + +# Errors which happened during init, so we can show a message box. +_init_errors = [] + + +def early_init(args): + """Initialize the part of the config which works without a QApplication.""" + configdata.init() + + yaml_config = configfiles.YamlConfig() + + config.instance = config.Config(yaml_config=yaml_config) + config.val = config.ConfigContainer(config.instance) + config.key_instance = config.KeyConfig(config.instance) + + for cf in config.change_filters: + cf.validate() + + configtypes.Font.monospace_fonts = config.val.fonts.monospace + + config_commands = config.ConfigCommands(config.instance, + config.key_instance) + objreg.register('config-commands', config_commands) + + config_api = None + + try: + config_api = configfiles.read_config_py() + # Raised here so we get the config_api back. + if config_api.errors: + raise configexc.ConfigFileErrors('config.py', config_api.errors) + except configexc.ConfigFileErrors as e: + log.config.exception("Error while loading config.py") + _init_errors.append(e) + + try: + if getattr(config_api, 'load_autoconfig', True): + try: + config.instance.read_yaml() + except configexc.ConfigFileErrors as e: + raise # caught in outer block + except configexc.Error as e: + desc = configexc.ConfigErrorDesc("Error", e) + raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) + except configexc.ConfigFileErrors as e: + log.config.exception("Error while loading config.py") + _init_errors.append(e) + + configfiles.init() + + objects.backend = get_backend(args) + earlyinit.init_with_backend(objects.backend) + + +def get_backend(args): + """Find out what backend to use based on available libraries.""" + try: + import PyQt5.QtWebKit # pylint: disable=unused-variable + except ImportError: + webkit_available = False + else: + webkit_available = qtutils.is_new_qtwebkit() + + str_to_backend = { + 'webkit': usertypes.Backend.QtWebKit, + 'webengine': usertypes.Backend.QtWebEngine, + } + + if args.backend is not None: + return str_to_backend[args.backend] + elif config.val.backend != 'auto': + return str_to_backend[config.val.backend] + elif webkit_available: + return usertypes.Backend.QtWebKit + else: + return usertypes.Backend.QtWebEngine + + +def late_init(save_manager): + """Initialize the rest of the config after the QApplication is created.""" + global _init_errors + for err in _init_errors: + errbox = msgbox.msgbox(parent=None, + title="Error while reading config", + text=err.to_html(), + icon=QMessageBox.Warning, + plain_text=False) + errbox.exec_() + _init_errors = [] + + config.instance.init_save_manager(save_manager) + configfiles.state.init_save_manager(save_manager) + + +def qt_args(namespace): + """Get the Qt QApplication arguments based on an argparse namespace. + + Args: + namespace: The argparse namespace. + + Return: + The argv list to be passed to Qt. + """ + argv = [sys.argv[0]] + + if namespace.qt_flag is not None: + argv += ['--' + flag[0] for flag in namespace.qt_flag] + + if namespace.qt_arg is not None: + for name, value in namespace.qt_arg: + argv += ['--' + name, value] + + argv += ['--' + arg for arg in config.val.qt_args] + return argv diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 4e8bd684b..24c6814b5 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -18,19 +18,15 @@ """Tests for qutebrowser.config.config.""" -import sys import copy import types -import logging -import unittest.mock import pytest from PyQt5.QtCore import QObject, QUrl from PyQt5.QtGui import QColor -from qutebrowser import qutebrowser from qutebrowser.commands import cmdexc -from qutebrowser.config import config, configdata, configexc, configfiles +from qutebrowser.config import config, configdata, configexc from qutebrowser.utils import objreg, usertypes from qutebrowser.misc import objects @@ -52,8 +48,8 @@ class TestChangeFilter: @pytest.fixture(autouse=True) def cleanup_globals(self, monkeypatch): - """Make sure config._change_filters is cleaned up.""" - monkeypatch.setattr(config, '_change_filters', []) + """Make sure config.change_filters is cleaned up.""" + monkeypatch.setattr(config, 'change_filters', []) @pytest.mark.parametrize('option', ['foobar', 'tab', 'tabss', 'tabs.']) def test_unknown_option(self, option): @@ -65,7 +61,7 @@ class TestChangeFilter: def test_validate(self, option): cf = config.change_filter(option) cf.validate() - assert cf in config._change_filters + assert cf in config.change_filters @pytest.mark.parametrize('method', [True, False]) @pytest.mark.parametrize('option, changed, matches', [ @@ -864,207 +860,3 @@ def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot, expected = 'yellow' assert obj.rendered_stylesheet == expected - - -@pytest.fixture -def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir, - data_tmpdir): - monkeypatch.setattr(configdata, 'DATA', None) - monkeypatch.setattr(configfiles, 'state', None) - monkeypatch.setattr(config, 'instance', None) - monkeypatch.setattr(config, 'key_instance', None) - monkeypatch.setattr(config, '_change_filters', []) - monkeypatch.setattr(config, '_init_errors', []) - # Make sure we get no SSL warning - monkeypatch.setattr(config.earlyinit, 'check_backend_ssl_support', - lambda _backend: None) - yield - try: - objreg.delete('config-commands') - except KeyError: - pass - - -@pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa -@pytest.mark.parametrize('config_py', [True, 'error', False]) -@pytest.mark.parametrize('invalid_yaml', - ['42', 'unknown', 'wrong-type', False]) -# pylint: disable=too-many-branches -def test_early_init(init_patch, config_tmpdir, caplog, fake_args, - load_autoconfig, config_py, invalid_yaml): - # Prepare files - autoconfig_file = config_tmpdir / 'autoconfig.yml' - config_py_file = config_tmpdir / 'config.py' - - if invalid_yaml == '42': - text = '42' - elif invalid_yaml == 'unknown': - text = 'global:\n colors.foobar: magenta\n' - elif invalid_yaml == 'wrong-type': - text = 'global:\n tabs.position: true\n' - else: - assert not invalid_yaml - text = 'global:\n colors.hints.fg: magenta\n' - autoconfig_file.write_text(text, 'utf-8', ensure=True) - - if config_py: - config_py_lines = ['c.colors.hints.bg = "red"'] - if not load_autoconfig: - config_py_lines.append('config.load_autoconfig = False') - if config_py == 'error': - config_py_lines.append('c.foo = 42') - config_py_file.write_text('\n'.join(config_py_lines), - 'utf-8', ensure=True) - - with caplog.at_level(logging.ERROR): - config.early_init(fake_args) - - # Check error messages - expected_errors = [] - if config_py == 'error': - expected_errors.append( - "Errors occurred while reading config.py:\n" - " While setting 'foo': No option 'foo'") - if load_autoconfig or not config_py: - error = "Errors occurred while reading autoconfig.yml:\n" - if invalid_yaml == '42': - error += " While loading data: Toplevel object is not a dict" - expected_errors.append(error) - elif invalid_yaml == 'wrong-type': - error += (" Error: Invalid value 'True' - expected a value of " - "type str but got bool.") - expected_errors.append(error) - - actual_errors = [str(err) for err in config._init_errors] - assert actual_errors == expected_errors - - # Make sure things have been init'ed - objreg.get('config-commands') - assert isinstance(config.instance, config.Config) - assert isinstance(config.key_instance, config.KeyConfig) - - # Check config values - if config_py and load_autoconfig and not invalid_yaml: - assert config.instance._values == { - 'colors.hints.bg': 'red', - 'colors.hints.fg': 'magenta', - } - elif config_py: - assert config.instance._values == {'colors.hints.bg': 'red'} - elif invalid_yaml: - assert config.instance._values == {} - else: - assert config.instance._values == {'colors.hints.fg': 'magenta'} - - -def test_early_init_invalid_change_filter(init_patch, fake_args): - config.change_filter('foobar') - with pytest.raises(configexc.NoOptionError): - config.early_init(fake_args) - - -@pytest.mark.parametrize('errors', [True, False]) -def test_late_init(init_patch, monkeypatch, fake_save_manager, fake_args, - mocker, errors): - config.early_init(fake_args) - if errors: - err = configexc.ConfigErrorDesc("Error text", Exception("Exception")) - errs = configexc.ConfigFileErrors("config.py", [err]) - monkeypatch.setattr(config, '_init_errors', [errs]) - msgbox_mock = mocker.patch('qutebrowser.config.config.msgbox.msgbox', - autospec=True) - - config.late_init(fake_save_manager) - - fake_save_manager.add_saveable.assert_any_call( - 'state-config', unittest.mock.ANY) - fake_save_manager.add_saveable.assert_any_call( - 'yaml-config', unittest.mock.ANY) - if errors: - assert len(msgbox_mock.call_args_list) == 1 - _call_posargs, call_kwargs = msgbox_mock.call_args_list[0] - text = call_kwargs['text'].strip() - assert text.startswith('Errors occurred while reading config.py:') - assert 'Error text: Exception' in text - else: - assert not msgbox_mock.called - - -class TestQtArgs: - - @pytest.fixture - def parser(self, mocker): - """Fixture to provide an argparser. - - Monkey-patches .exit() of the argparser so it doesn't exit on errors. - """ - parser = qutebrowser.get_argparser() - mocker.patch.object(parser, 'exit', side_effect=Exception) - return parser - - @pytest.mark.parametrize('args, expected', [ - # No Qt arguments - (['--debug'], [sys.argv[0]]), - # Qt flag - (['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '--reverse']), - # Qt argument with value - (['--qt-arg', 'stylesheet', 'foo'], - [sys.argv[0], '--stylesheet', 'foo']), - # --qt-arg given twice - (['--qt-arg', 'stylesheet', 'foo', '--qt-arg', 'geometry', 'bar'], - [sys.argv[0], '--stylesheet', 'foo', '--geometry', 'bar']), - # --qt-flag given twice - (['--qt-flag', 'foo', '--qt-flag', 'bar'], - [sys.argv[0], '--foo', '--bar']), - ]) - def test_qt_args(self, config_stub, args, expected, parser): - """Test commandline with no Qt arguments given.""" - parsed = parser.parse_args(args) - assert config.qt_args(parsed) == expected - - def test_qt_both(self, config_stub, parser): - """Test commandline with a Qt argument and flag.""" - args = parser.parse_args(['--qt-arg', 'stylesheet', 'foobar', - '--qt-flag', 'reverse']) - qt_args = config.qt_args(args) - assert qt_args[0] == sys.argv[0] - assert '--reverse' in qt_args - assert '--stylesheet' in qt_args - assert 'foobar' in qt_args - - def test_with_settings(self, config_stub, parser): - parsed = parser.parse_args(['--qt-flag', 'foo']) - config_stub.val.qt_args = ['bar'] - assert config.qt_args(parsed) == [sys.argv[0], '--foo', '--bar'] - - -@pytest.mark.parametrize('arg, confval, can_import, is_new_webkit, used', [ - # overridden by commandline arg - ('webkit', 'auto', False, False, usertypes.Backend.QtWebKit), - # overridden by config - (None, 'webkit', False, False, usertypes.Backend.QtWebKit), - # WebKit available but too old - (None, 'auto', True, False, usertypes.Backend.QtWebEngine), - # WebKit available and new - (None, 'auto', True, True, usertypes.Backend.QtWebKit), - # WebKit unavailable - (None, 'auto', False, False, usertypes.Backend.QtWebEngine), -]) -def test_get_backend(monkeypatch, fake_args, config_stub, - arg, confval, can_import, is_new_webkit, used): - real_import = __import__ - - def fake_import(name, *args, **kwargs): - if name != 'PyQt5.QtWebKit': - return real_import(name, *args, **kwargs) - if can_import: - return None - raise ImportError - - fake_args.backend = arg - config_stub.val.backend = confval - monkeypatch.setattr(config.qtutils, 'is_new_qtwebkit', - lambda: is_new_webkit) - monkeypatch.setattr('builtins.__import__', fake_import) - - assert config.get_backend(fake_args) == used diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py new file mode 100644 index 000000000..c5108d95f --- /dev/null +++ b/tests/unit/config/test_configinit.py @@ -0,0 +1,234 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: +# Copyright 2017 Florian Bruhin (The Compiler) + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tests for qutebrowser.config.configinit.""" + +import sys +import logging +import unittest.mock + +import pytest + +from qutebrowser import qutebrowser +from qutebrowser.config import (config, configdata, configexc, configfiles, + configinit) +from qutebrowser.utils import objreg, usertypes + + +@pytest.fixture +def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir, + data_tmpdir): + monkeypatch.setattr(configdata, 'DATA', None) + monkeypatch.setattr(configfiles, 'state', None) + monkeypatch.setattr(config, 'instance', None) + monkeypatch.setattr(config, 'key_instance', None) + monkeypatch.setattr(config, 'change_filters', []) + monkeypatch.setattr(configinit, '_init_errors', []) + # Make sure we get no SSL warning + monkeypatch.setattr(configinit.earlyinit, 'check_backend_ssl_support', + lambda _backend: None) + yield + try: + objreg.delete('config-commands') + except KeyError: + pass + + +@pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa +@pytest.mark.parametrize('config_py', [True, 'error', False]) +@pytest.mark.parametrize('invalid_yaml', + ['42', 'unknown', 'wrong-type', False]) +# pylint: disable=too-many-branches +def test_early_init(init_patch, config_tmpdir, caplog, fake_args, + load_autoconfig, config_py, invalid_yaml): + # Prepare files + autoconfig_file = config_tmpdir / 'autoconfig.yml' + config_py_file = config_tmpdir / 'config.py' + + if invalid_yaml == '42': + text = '42' + elif invalid_yaml == 'unknown': + text = 'global:\n colors.foobar: magenta\n' + elif invalid_yaml == 'wrong-type': + text = 'global:\n tabs.position: true\n' + else: + assert not invalid_yaml + text = 'global:\n colors.hints.fg: magenta\n' + autoconfig_file.write_text(text, 'utf-8', ensure=True) + + if config_py: + config_py_lines = ['c.colors.hints.bg = "red"'] + if not load_autoconfig: + config_py_lines.append('config.load_autoconfig = False') + if config_py == 'error': + config_py_lines.append('c.foo = 42') + config_py_file.write_text('\n'.join(config_py_lines), + 'utf-8', ensure=True) + + with caplog.at_level(logging.ERROR): + configinit.early_init(fake_args) + + # Check error messages + expected_errors = [] + if config_py == 'error': + expected_errors.append( + "Errors occurred while reading config.py:\n" + " While setting 'foo': No option 'foo'") + if load_autoconfig or not config_py: + error = "Errors occurred while reading autoconfig.yml:\n" + if invalid_yaml == '42': + error += " While loading data: Toplevel object is not a dict" + expected_errors.append(error) + elif invalid_yaml == 'wrong-type': + error += (" Error: Invalid value 'True' - expected a value of " + "type str but got bool.") + expected_errors.append(error) + + actual_errors = [str(err) for err in configinit._init_errors] + assert actual_errors == expected_errors + + # Make sure things have been init'ed + objreg.get('config-commands') + assert isinstance(config.instance, config.Config) + assert isinstance(config.key_instance, config.KeyConfig) + + # Check config values + if config_py and load_autoconfig and not invalid_yaml: + assert config.instance._values == { + 'colors.hints.bg': 'red', + 'colors.hints.fg': 'magenta', + } + elif config_py: + assert config.instance._values == {'colors.hints.bg': 'red'} + elif invalid_yaml: + assert config.instance._values == {} + else: + assert config.instance._values == {'colors.hints.fg': 'magenta'} + + +def test_early_init_invalid_change_filter(init_patch, fake_args): + config.change_filter('foobar') + with pytest.raises(configexc.NoOptionError): + configinit.early_init(fake_args) + + +@pytest.mark.parametrize('errors', [True, False]) +def test_late_init(init_patch, monkeypatch, fake_save_manager, fake_args, + mocker, errors): + configinit.early_init(fake_args) + if errors: + err = configexc.ConfigErrorDesc("Error text", Exception("Exception")) + errs = configexc.ConfigFileErrors("config.py", [err]) + monkeypatch.setattr(configinit, '_init_errors', [errs]) + msgbox_mock = mocker.patch('qutebrowser.config.configinit.msgbox.msgbox', + autospec=True) + + configinit.late_init(fake_save_manager) + + fake_save_manager.add_saveable.assert_any_call( + 'state-config', unittest.mock.ANY) + fake_save_manager.add_saveable.assert_any_call( + 'yaml-config', unittest.mock.ANY) + if errors: + assert len(msgbox_mock.call_args_list) == 1 + _call_posargs, call_kwargs = msgbox_mock.call_args_list[0] + text = call_kwargs['text'].strip() + assert text.startswith('Errors occurred while reading config.py:') + assert 'Error text: Exception' in text + else: + assert not msgbox_mock.called + + +class TestQtArgs: + + @pytest.fixture + def parser(self, mocker): + """Fixture to provide an argparser. + + Monkey-patches .exit() of the argparser so it doesn't exit on errors. + """ + parser = qutebrowser.get_argparser() + mocker.patch.object(parser, 'exit', side_effect=Exception) + return parser + + @pytest.mark.parametrize('args, expected', [ + # No Qt arguments + (['--debug'], [sys.argv[0]]), + # Qt flag + (['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '--reverse']), + # Qt argument with value + (['--qt-arg', 'stylesheet', 'foo'], + [sys.argv[0], '--stylesheet', 'foo']), + # --qt-arg given twice + (['--qt-arg', 'stylesheet', 'foo', '--qt-arg', 'geometry', 'bar'], + [sys.argv[0], '--stylesheet', 'foo', '--geometry', 'bar']), + # --qt-flag given twice + (['--qt-flag', 'foo', '--qt-flag', 'bar'], + [sys.argv[0], '--foo', '--bar']), + ]) + def test_qt_args(self, config_stub, args, expected, parser): + """Test commandline with no Qt arguments given.""" + parsed = parser.parse_args(args) + assert configinit.qt_args(parsed) == expected + + def test_qt_both(self, config_stub, parser): + """Test commandline with a Qt argument and flag.""" + args = parser.parse_args(['--qt-arg', 'stylesheet', 'foobar', + '--qt-flag', 'reverse']) + qt_args = configinit.qt_args(args) + assert qt_args[0] == sys.argv[0] + assert '--reverse' in qt_args + assert '--stylesheet' in qt_args + assert 'foobar' in qt_args + + def test_with_settings(self, config_stub, parser): + parsed = parser.parse_args(['--qt-flag', 'foo']) + config_stub.val.qt_args = ['bar'] + assert configinit.qt_args(parsed) == [sys.argv[0], '--foo', '--bar'] + + +@pytest.mark.parametrize('arg, confval, can_import, is_new_webkit, used', [ + # overridden by commandline arg + ('webkit', 'auto', False, False, usertypes.Backend.QtWebKit), + # overridden by config + (None, 'webkit', False, False, usertypes.Backend.QtWebKit), + # WebKit available but too old + (None, 'auto', True, False, usertypes.Backend.QtWebEngine), + # WebKit available and new + (None, 'auto', True, True, usertypes.Backend.QtWebKit), + # WebKit unavailable + (None, 'auto', False, False, usertypes.Backend.QtWebEngine), +]) +def test_get_backend(monkeypatch, fake_args, config_stub, + arg, confval, can_import, is_new_webkit, used): + real_import = __import__ + + def fake_import(name, *args, **kwargs): + if name != 'PyQt5.QtWebKit': + return real_import(name, *args, **kwargs) + if can_import: + return None + raise ImportError + + fake_args.backend = arg + config_stub.val.backend = confval + monkeypatch.setattr(config.qtutils, 'is_new_qtwebkit', + lambda: is_new_webkit) + monkeypatch.setattr('builtins.__import__', fake_import) + + assert configinit.get_backend(fake_args) == used