qutebrowser/tests/unit/config/test_configfiles.py
Florian Bruhin 7cad8f41f2 Remove unknown YAML data from config
I considered introducing another list of deleted options (or a "deleted: True"
in configdata.yml), similar to what we had with the old config.

However, let's take the easier route and just delete everything we don't know
from configdata.yml. If someone edits it by hand, it's their fault :P

See #2772, #2847
2017-09-21 16:29:40 +02:00

411 lines
14 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 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.configfiles."""
import os
import pytest
from qutebrowser.config import config, configfiles, configexc
from qutebrowser.utils import utils
from PyQt5.QtCore import QSettings
@pytest.mark.parametrize('old_data, insert, new_data', [
(None, False, '[general]\n\n[geometry]\n\n'),
('[general]\nfooled = true', False, '[general]\n\n[geometry]\n\n'),
('[general]\nfoobar = 42', False,
'[general]\nfoobar = 42\n\n[geometry]\n\n'),
(None, True, '[general]\nnewval = 23\n\n[geometry]\n\n'),
])
def test_state_config(fake_save_manager, data_tmpdir,
old_data, insert, new_data):
statefile = data_tmpdir / 'state'
if old_data is not None:
statefile.write_text(old_data, 'utf-8')
state = configfiles.StateConfig()
if insert:
state['general']['newval'] = '23'
# WORKAROUND for https://github.com/PyCQA/pylint/issues/574
if 'foobar' in (old_data or ''): # pylint: disable=superfluous-parens
assert state['general']['foobar'] == '42'
state._save()
assert statefile.read_text('utf-8') == new_data
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
# Unknown key
'global:\n hello: world',
])
@pytest.mark.parametrize('insert', [True, False])
def test_yaml_config(fake_save_manager, config_tmpdir, old_config, insert):
autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
yaml = configfiles.YamlConfig()
yaml.load()
if insert:
yaml['tabs.show'] = 'never'
yaml._save()
if not insert and old_config is None:
lines = []
else:
text = autoconfig.read_text('utf-8')
lines = text.splitlines()
if insert:
assert lines[0].startswith('# DO NOT edit this file by hand,')
assert 'config_version: {}'.format(yaml.VERSION) in lines
assert 'global:' in lines
print(lines)
# WORKAROUND for https://github.com/PyCQA/pylint/issues/574
if 'magenta' in (old_config or ''): # pylint: disable=superfluous-parens
assert ' colors.hints.fg: magenta' in lines
if insert:
assert ' tabs.show: never' in lines
assert ' hello:' not in lines
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
@pytest.mark.parametrize('key, value', [
('colors.hints.fg', 'green'),
('colors.hints.bg', None),
('confirm_quit', True),
('confirm_quit', False),
])
def test_yaml_config_changed(fake_save_manager, config_tmpdir, old_config,
key, value):
autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
yaml = configfiles.YamlConfig()
yaml.load()
yaml[key] = value
assert key in yaml
assert yaml[key] == value
yaml._save()
yaml = configfiles.YamlConfig()
yaml.load()
assert key in yaml
assert yaml[key] == value
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
def test_yaml_config_unchanged(fake_save_manager, config_tmpdir, old_config):
autoconfig = config_tmpdir / 'autoconfig.yml'
mtime = None
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
mtime = autoconfig.stat().mtime
yaml = configfiles.YamlConfig()
yaml.load()
yaml._save()
if old_config is None:
assert not autoconfig.exists()
else:
assert autoconfig.stat().mtime == mtime
@pytest.mark.parametrize('line, text, exception', [
('%', 'While parsing', 'while scanning a directive'),
('global: 42', 'While loading data', "'global' object is not a dict"),
('foo: 42', 'While loading data',
"Toplevel object does not contain 'global' key"),
('42', 'While loading data', "Toplevel object is not a dict"),
])
def test_yaml_config_invalid(fake_save_manager, config_tmpdir,
line, text, exception):
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text(line, 'utf-8', ensure=True)
yaml = configfiles.YamlConfig()
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load()
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert error.text == text
assert str(error.exception).splitlines()[0] == exception
assert error.traceback is None
def test_yaml_oserror(fake_save_manager, config_tmpdir):
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.ensure()
autoconfig.chmod(0)
if os.access(str(autoconfig), os.R_OK):
# Docker container or similar
pytest.skip("File was still readable")
yaml = configfiles.YamlConfig()
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load()
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert error.text == "While reading"
assert isinstance(error.exception, OSError)
assert error.traceback is None
class TestConfigPy:
"""Tests for ConfigAPI and read_config_py()."""
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
class ConfPy:
"""Helper class to get a confpy fixture."""
def __init__(self, tmpdir):
self._confpy = tmpdir / 'config.py'
self.filename = str(self._confpy)
def write(self, *lines):
text = '\n'.join(lines)
self._confpy.write_text(text, 'utf-8', ensure=True)
def read(self):
"""Read the config.py via configfiles and check for errors."""
api = configfiles.read_config_py(self.filename)
assert not api.errors
@pytest.fixture
def confpy(self, tmpdir):
return self.ConfPy(tmpdir)
@pytest.mark.parametrize('line', [
'c.colors.hints.bg = "red"',
'config.set("colors.hints.bg", "red")',
])
def test_set(self, confpy, line):
confpy.write(line)
confpy.read()
assert config.instance._values['colors.hints.bg'] == 'red'
@pytest.mark.parametrize('set_first', [True, False])
@pytest.mark.parametrize('get_line', [
'c.colors.hints.fg',
'config.get("colors.hints.fg")',
])
def test_get(self, confpy, set_first, get_line):
"""Test whether getting options works correctly.
We test this by doing the following:
- Set colors.hints.fg to some value (inside the config.py with
set_first, outside of it otherwise).
- In the config.py, read .fg and set .bg to the same value.
- Verify that .bg has been set correctly.
"""
# pylint: disable=bad-config-option
config.val.colors.hints.fg = 'green'
if set_first:
confpy.write('c.colors.hints.fg = "red"',
'c.colors.hints.bg = {}'.format(get_line))
expected = 'red'
else:
confpy.write('c.colors.hints.bg = {}'.format(get_line))
expected = 'green'
confpy.read()
assert config.instance._values['colors.hints.bg'] == expected
@pytest.mark.parametrize('line, mode', [
('config.bind(",a", "message-info foo")', 'normal'),
('config.bind(",a", "message-info foo", "prompt")', 'prompt'),
])
def test_bind(self, confpy, line, mode):
confpy.write(line)
confpy.read()
expected = {mode: {',a': 'message-info foo'}}
assert config.instance._values['bindings.commands'] == expected
@pytest.mark.parametrize('line, key, mode', [
('config.unbind("o")', 'o', 'normal'),
('config.unbind("y", mode="prompt")', 'y', 'prompt'),
])
def test_unbind(self, confpy, line, key, mode):
confpy.write(line)
confpy.read()
expected = {mode: {key: None}}
assert config.instance._values['bindings.commands'] == expected
def test_mutating(self, confpy):
confpy.write('c.aliases["foo"] = "message-info foo"',
'c.aliases["bar"] = "message-info bar"')
confpy.read()
assert config.instance._values['aliases']['foo'] == 'message-info foo'
assert config.instance._values['aliases']['bar'] == 'message-info bar'
def test_reading_default_location(self, config_tmpdir):
(config_tmpdir / 'config.py').write_text(
'c.colors.hints.bg = "red"', 'utf-8')
configfiles.read_config_py()
assert config.instance._values['colors.hints.bg'] == 'red'
def test_reading_missing_default_location(self, config_tmpdir):
assert not (config_tmpdir / 'config.py').exists()
configfiles.read_config_py() # Should not crash
def test_oserror(self, tmpdir):
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(str(tmpdir / 'foo'))
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert isinstance(error.exception, OSError)
assert error.text == "Error while reading foo"
assert error.traceback is None
def test_nul_bytes(self, confpy):
confpy.write('\0')
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(confpy.filename)
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert isinstance(error.exception, (TypeError, ValueError))
assert error.text == "Error while compiling"
exception_text = 'source code string cannot contain null bytes'
assert str(error.exception) == exception_text
assert error.traceback is None
def test_syntax_error(self, confpy):
confpy.write('+')
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(confpy.filename)
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert isinstance(error.exception, SyntaxError)
assert error.text == "Syntax Error"
exception_text = 'invalid syntax (config.py, line 1)'
assert str(error.exception) == exception_text
tblines = error.traceback.strip().splitlines()
assert tblines[0] == "Traceback (most recent call last):"
assert tblines[-1] == "SyntaxError: invalid syntax"
assert " +" in tblines
assert " ^" in tblines
def test_unhandled_exception(self, confpy):
confpy.write("config.load_autoconfig = False", "1/0")
api = configfiles.read_config_py(confpy.filename)
assert not api.load_autoconfig
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
tblines = error.traceback.strip().splitlines()
assert tblines[0] == "Traceback (most recent call last):"
assert tblines[-1] == "ZeroDivisionError: division by zero"
assert " 1/0" in tblines
def test_config_val(self, confpy):
"""Using config.val should not work in config.py files."""
confpy.write("config.val.colors.hints.bg = 'red'")
api = configfiles.read_config_py(confpy.filename)
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "Unhandled exception"
assert isinstance(error.exception, AttributeError)
message = "'ConfigAPI' object has no attribute 'val'"
assert str(error.exception) == message
@pytest.mark.parametrize('line', ["c.foo = 42", "config.set('foo', 42)"])
def test_config_error(self, confpy, line):
confpy.write(line, "config.load_autoconfig = False")
api = configfiles.read_config_py(confpy.filename)
assert not api.load_autoconfig
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError)
assert str(error.exception) == "No option 'foo'"
assert error.traceback is None
def test_multiple_errors(self, confpy):
confpy.write("c.foo = 42", "config.set('foo', 42)", "1/0")
api = configfiles.read_config_py(confpy.filename)
assert len(api.errors) == 3
for error in api.errors[:2]:
assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError)
assert str(error.exception) == "No option 'foo'"
assert error.traceback is None
error = api.errors[2]
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
assert error.traceback is not None
@pytest.fixture
def init_patch(qapp, fake_save_manager, config_tmpdir, data_tmpdir,
config_stub, monkeypatch):
monkeypatch.setattr(configfiles, 'state', None)
yield
def test_init(init_patch, config_tmpdir):
configfiles.init()
# Make sure qsettings land in a subdir
if utils.is_linux:
settings = QSettings()
settings.setValue("hello", "world")
settings.sync()
assert (config_tmpdir / 'qsettings').exists()
# Lots of other stuff is tested in test_config.py in test_init