qutebrowser/tests/unit/config/test_configinit.py
2019-02-22 21:45:08 -08:00

568 lines
22 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017-2019 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# 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 <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.config.configinit."""
import os
import sys
import logging
import unittest.mock
import pytest
from qutebrowser import qutebrowser
from qutebrowser.config import (config, configexc, configfiles, configinit,
configdata, configtypes)
from qutebrowser.utils import objreg, usertypes
from helpers import utils
@pytest.fixture
def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
data_tmpdir):
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', None)
monkeypatch.setattr(configtypes.Font, 'monospace_fonts', None)
yield
try:
objreg.delete('config-commands')
except KeyError:
pass
@pytest.fixture
def args(fake_args):
"""Arguments needed for the config to init."""
fake_args.temp_settings = []
return fake_args
@pytest.fixture(autouse=True)
def configdata_init(monkeypatch):
"""Make sure configdata is init'ed and no test re-init's it."""
if not configdata.DATA:
configdata.init()
monkeypatch.setattr(configdata, 'init', lambda: None)
class TestEarlyInit:
@pytest.mark.parametrize('config_py', [True, 'error', False])
def test_config_py(self, init_patch, config_tmpdir, caplog, args,
config_py):
"""Test loading with only a config.py."""
config_py_file = config_tmpdir / 'config.py'
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
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(args)
# Check error messages
expected_errors = []
if config_py == 'error':
expected_errors.append("While setting 'foo': No option 'foo'")
if configinit._init_errors is None:
actual_errors = []
else:
actual_errors = [str(err)
for err in configinit._init_errors.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:
expected = 'colors.hints.bg = red'
else:
expected = '<Default configuration>'
assert config.instance.dump_userconfig() == expected
@pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa
@pytest.mark.parametrize('config_py', [True, 'error', False])
@pytest.mark.parametrize('invalid_yaml', ['42', 'list', 'unknown',
'wrong-type', False])
def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, args,
load_autoconfig, config_py, invalid_yaml):
"""Test interaction between config.py and autoconfig.yml."""
# Prepare files
autoconfig_file = config_tmpdir / 'autoconfig.yml'
config_py_file = config_tmpdir / 'config.py'
yaml_lines = {
'42': '42',
'list': '[1, 2]',
'unknown': [
'settings:',
' colors.foobar:',
' global: magenta',
'config_version: 2',
],
'wrong-type': [
'settings:',
' tabs.position:',
' global: true',
'config_version: 2',
],
False: [
'settings:',
' colors.hints.fg:',
' global: magenta',
'config_version: 2',
],
}
text = '\n'.join(yaml_lines[invalid_yaml])
autoconfig_file.write_text(text, 'utf-8', ensure=True)
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
if load_autoconfig:
config_py_lines.append('config.load_autoconfig()')
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(args)
# Check error messages
expected_errors = []
if load_autoconfig or not config_py:
suffix = ' (autoconfig.yml)' if config_py else ''
if invalid_yaml in ['42', 'list']:
error = ("While loading data{}: Toplevel object is not a dict"
.format(suffix))
expected_errors.append(error)
elif invalid_yaml == 'wrong-type':
error = ("Error{}: Invalid value 'True' - expected a value of "
"type str but got bool.".format(suffix))
expected_errors.append(error)
elif invalid_yaml == 'unknown':
error = ("While loading options{}: Unknown option "
"colors.foobar".format(suffix))
expected_errors.append(error)
if config_py == 'error':
expected_errors.append("While setting 'foo': No option 'foo'")
if configinit._init_errors is None:
actual_errors = []
else:
actual_errors = [str(err)
for err in configinit._init_errors.errors]
assert actual_errors == expected_errors
# Check config values
dump = config.instance.dump_userconfig()
if config_py and load_autoconfig and not invalid_yaml:
expected = [
'colors.hints.bg = red',
'colors.hints.fg = magenta',
]
elif config_py:
expected = ['colors.hints.bg = red']
elif invalid_yaml:
expected = ['<Default configuration>']
else:
expected = ['colors.hints.fg = magenta']
assert dump == '\n'.join(expected)
def test_invalid_change_filter(self, init_patch, args):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
configinit.early_init(args)
def test_temp_settings_valid(self, init_patch, args):
args.temp_settings = [('colors.completion.fg', 'magenta')]
configinit.early_init(args)
assert config.instance.get_obj('colors.completion.fg') == 'magenta'
def test_temp_settings_invalid(self, caplog, init_patch, message_mock,
args):
"""Invalid temp settings should show an error."""
args.temp_settings = [('foo', 'bar')]
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
msg = message_mock.getmsg()
assert msg.level == usertypes.MessageLevel.error
assert msg.text == "set: NoOptionError - No option 'foo'"
@pytest.mark.parametrize('settings, size, family', [
# Only fonts.monospace customized
([('fonts.monospace', '"Comic Sans MS"')], 10, 'Comic Sans MS'),
# fonts.monospace and font settings customized
# https://github.com/qutebrowser/qutebrowser/issues/3096
([('fonts.monospace', '"Comic Sans MS"'),
('fonts.tabs', '12pt monospace'),
('fonts.keyhint', '12pt monospace')], 12, 'Comic Sans MS'),
])
@pytest.mark.parametrize('method', ['temp', 'auto', 'py'])
def test_monospace_fonts_init(self, init_patch, args, config_tmpdir,
method, settings, size, family):
"""Ensure setting fonts.monospace at init works properly.
See https://github.com/qutebrowser/qutebrowser/issues/2973
"""
if method == 'temp':
args.temp_settings = settings
elif method == 'auto':
autoconfig_file = config_tmpdir / 'autoconfig.yml'
lines = (["config_version: 2", "settings:"] +
[" {}:\n global:\n '{}'".format(k, v)
for k, v in settings])
autoconfig_file.write_text('\n'.join(lines), 'utf-8', ensure=True)
elif method == 'py':
config_py_file = config_tmpdir / 'config.py'
lines = ["c.{} = '{}'".format(k, v) for k, v in settings]
config_py_file.write_text('\n'.join(lines), 'utf-8', ensure=True)
configinit.early_init(args)
# Font
expected = '{}pt "{}"'.format(size, family)
assert config.instance.get('fonts.keyhint') == expected
# QtFont
font = config.instance.get('fonts.tabs')
assert font.pointSize() == size
assert font.family() == family
def test_monospace_fonts_later(self, init_patch, args):
"""Ensure setting fonts.monospace after init works properly.
See https://github.com/qutebrowser/qutebrowser/issues/2973
"""
configinit.early_init(args)
changed_options = []
config.instance.changed.connect(changed_options.append)
config.instance.set_obj('fonts.monospace', '"Comic Sans MS"')
assert 'fonts.keyhint' in changed_options # Font
assert config.instance.get('fonts.keyhint') == '10pt "Comic Sans MS"'
assert 'fonts.tabs' in changed_options # QtFont
assert config.instance.get('fonts.tabs').family() == 'Comic Sans MS'
# Font subclass, but doesn't end with "monospace"
assert 'fonts.web.family.standard' not in changed_options
def test_setting_monospace_fonts_family(self, init_patch, args):
"""Make sure setting fonts.monospace after a family works.
See https://github.com/qutebrowser/qutebrowser/issues/3130
"""
configinit.early_init(args)
config.instance.set_str('fonts.web.family.standard', '')
config.instance.set_str('fonts.monospace', 'Terminus')
@pytest.mark.parametrize('config_opt, config_val, envvar, expected', [
('qt.force_software_rendering', 'software-opengl',
'QT_XCB_FORCE_SOFTWARE_OPENGL', '1'),
('qt.force_software_rendering', 'qt-quick',
'QT_QUICK_BACKEND', 'software'),
('qt.force_software_rendering', 'chromium',
'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND', '1'),
('qt.force_platform', 'toaster', 'QT_QPA_PLATFORM', 'toaster'),
('qt.highdpi', True, 'QT_AUTO_SCREEN_SCALE_FACTOR', '1'),
('window.hide_decoration', True,
'QT_WAYLAND_DISABLE_WINDOWDECORATION', '1')
])
def test_env_vars(self, monkeypatch, config_stub,
config_opt, config_val, envvar, expected):
"""Check settings which set an environment variable."""
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
monkeypatch.setenv(envvar, '') # to make sure it gets restored
monkeypatch.delenv(envvar)
config_stub.set_obj(config_opt, config_val)
configinit._init_envvars()
assert os.environ[envvar] == expected
def test_env_vars_webkit(self, monkeypatch, config_stub):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebKit)
configinit._init_envvars()
@pytest.mark.parametrize('errors', [True, False])
def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
mocker, errors):
configinit.early_init(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, 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 '<b>Error text</b>: 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.fixture(autouse=True)
def reduce_args(self, monkeypatch, config_stub):
"""Make sure no --disable-shared-workers/referer argument get added."""
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, compiled=False: True)
config_stub.val.content.headers.referer = 'always'
@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('backend, expected', [
(usertypes.Backend.QtWebEngine, ['--disable-shared-workers']),
(usertypes.Backend.QtWebKit, []),
])
def test_shared_workers(self, config_stub, monkeypatch, parser,
backend, expected):
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, compiled=False: False)
monkeypatch.setattr(configinit.objects, 'backend', backend)
parsed = parser.parse_args([])
assert configinit.qt_args(parsed) == [sys.argv[0]] + expected
def test_disable_gpu(self, config_stub, monkeypatch, parser):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
config_stub.val.qt.force_software_rendering = 'chromium'
parsed = parser.parse_args([])
expected = [sys.argv[0], '--disable-gpu']
assert configinit.qt_args(parsed) == expected
@utils.qt510
@pytest.mark.parametrize('new_version, autoplay, added', [
(True, False, False), # new enough to not need it
(False, True, False), # autoplay enabled
(False, False, True),
])
def test_autoplay(self, config_stub, monkeypatch, parser,
new_version, autoplay, added):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
config_stub.val.content.autoplay = autoplay
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, compiled=False: new_version)
parsed = parser.parse_args([])
args = configinit.qt_args(parsed)
assert ('--autoplay-policy=user-gesture-required' in args) == added
@utils.qt59
@pytest.mark.parametrize('policy, arg', [
('all-interfaces', None),
('default-public-and-private-interfaces',
'--force-webrtc-ip-handling-policy='
'default_public_and_private_interfaces'),
('default-public-interface-only',
'--force-webrtc-ip-handling-policy='
'default_public_interface_only'),
('disable-non-proxied-udp',
'--force-webrtc-ip-handling-policy='
'disable_non_proxied_udp'),
])
def test_webrtc(self, config_stub, monkeypatch, parser,
policy, arg):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
config_stub.val.content.webrtc_ip_handling_policy = policy
parsed = parser.parse_args([])
args = configinit.qt_args(parsed)
if arg is None:
assert not any(a.startswith('--force-webrtc-ip-handling-policy=')
for a in args)
else:
assert arg in args
@pytest.mark.parametrize('canvas_reading, added', [
(True, False), # canvas reading enabled
(False, True),
])
def test_canvas_reading(self, config_stub, monkeypatch, parser,
canvas_reading, added):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
config_stub.val.content.canvas_reading = canvas_reading
parsed = parser.parse_args([])
args = configinit.qt_args(parsed)
assert ('--disable-reading-from-canvas' in args) == added
@pytest.mark.parametrize('process_model, added', [
('process-per-site-instance', False),
('process-per-site', True),
('single-process', True),
])
def test_process_model(self, config_stub, monkeypatch, parser,
process_model, added):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
config_stub.val.qt.process_model = process_model
parsed = parser.parse_args([])
args = configinit.qt_args(parsed)
if added:
assert '--' + process_model in args
else:
assert '--process-per-site' not in args
assert '--single-process' not in args
assert '--process-per-site-instance' not in args
assert '--process-per-tab' not in args
@pytest.mark.parametrize('low_end_device_mode, arg', [
('auto', None),
('always', '--enable-low-end-device-mode'),
('never', '--disable-low-end-device-mode'),
])
def test_low_end_device_mode(self, config_stub, monkeypatch, parser,
low_end_device_mode, arg):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
config_stub.val.qt.low_end_device_mode = low_end_device_mode
parsed = parser.parse_args([])
args = configinit.qt_args(parsed)
if arg is None:
assert '--enable-low-end-device-mode' not in args
assert '--disable-low-end-device-mode' not in args
else:
assert arg in args
@pytest.mark.parametrize('referer, arg', [
('always', None),
('never', '--no-referrers'),
('same-domain', '--reduced-referrer-granularity'),
])
def test_referer(self, config_stub, monkeypatch, parser, referer, arg):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
config_stub.val.content.headers.referer = referer
parsed = parser.parse_args([])
args = configinit.qt_args(parsed)
if arg is None:
assert '--no-referrers' not in args
assert '--reduced-referrer-granularity' not in args
else:
assert arg in args
@pytest.mark.parametrize('arg, confval, used', [
# overridden by commandline arg
('webkit', 'webengine', usertypes.Backend.QtWebKit),
# set in config
(None, 'webkit', usertypes.Backend.QtWebKit),
])
def test_get_backend(monkeypatch, args, config_stub,
arg, confval, used):
real_import = __import__
def fake_import(name, *args, **kwargs):
if name != 'PyQt5.QtWebKit':
return real_import(name, *args, **kwargs)
raise ImportError
args.backend = arg
config_stub.val.backend = confval
monkeypatch.setattr('builtins.__import__', fake_import)
assert configinit.get_backend(args) == used