summaryrefslogtreecommitdiff
path: root/tests/unit/config/test_configdata.py
blob: 4ea5ffe6dd2e32bb84bbab8522ec23695ab59338 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2020 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.configdata."""

import textwrap

import yaml
import pytest

# To run cmdutils.register decorators
from qutebrowser import app  # pylint: disable=unused-import
from qutebrowser.config import configdata, configtypes
from qutebrowser.utils import usertypes


def test_init(config_stub):
    """Test reading the default yaml file."""
    # configdata.init() is called by config_stub
    config_stub.val.aliases = {}
    assert isinstance(configdata.DATA, dict)
    assert 'search.ignore_case' in configdata.DATA


def test_data(config_stub):
    """Test various properties of the default values."""
    for option in configdata.DATA.values():
        # Make sure to_py and to_str work
        option.typ.to_py(option.default)
        option.typ.to_str(option.default)

        # https://github.com/qutebrowser/qutebrowser/issues/3104
        # For lists/dicts, don't use None as default
        if isinstance(option.typ, (configtypes.Dict, configtypes.List)):
            assert option.default is not None, option
        # For ListOrValue, use a list as default
        if isinstance(option.typ, configtypes.ListOrValue):
            assert isinstance(option.default, list), option

        # Make sure floats also have floats for defaults/bounds
        if isinstance(option.typ, configtypes.Float):
            for value in [option.default,
                          option.typ.minval,
                          option.typ.maxval]:
                assert value is None or isinstance(value, float), option

        # No double spaces after dots
        assert '.  ' not in option.description, option


def test_init_benchmark(benchmark):
    benchmark(configdata.init)


def test_is_valid_prefix(monkeypatch):
    monkeypatch.setattr(configdata, 'DATA', ['foo.bar'])
    assert configdata.is_valid_prefix('foo')
    assert not configdata.is_valid_prefix('foo.bar')
    assert not configdata.is_valid_prefix('foa')


class TestReadYaml:

    def test_valid(self):
        yaml_data = textwrap.dedent("""
            test1:
                type: Bool
                default: true
                desc: Hello World

            test2:
                type: String
                default: foo
                backend: QtWebKit
                desc: Hello World 2
        """)
        data, _migrations = configdata._read_yaml(yaml_data)
        assert data.keys() == {'test1', 'test2'}
        assert data['test1'].description == "Hello World"
        assert data['test2'].default == "foo"
        assert data['test2'].backends == [usertypes.Backend.QtWebKit]
        assert isinstance(data['test1'].typ, configtypes.Bool)

    def test_invalid_keys(self):
        """Test reading with unknown keys."""
        data = textwrap.dedent("""
            test:
                type: Bool
                default: true
                desc: Hello World
                hello: world
        """,)
        with pytest.raises(ValueError, match='Invalid keys'):
            configdata._read_yaml(data)

    @pytest.mark.parametrize('first, second, shadowing', [
        ('foo', 'foo.bar', True),
        ('foo.bar', 'foo', True),
        ('foo.bar', 'foo.bar.baz', True),
        ('foo.bar', 'foo.baz', False),
    ])
    def test_shadowing(self, first, second, shadowing):
        """Make sure a setting can't shadow another."""
        data = textwrap.dedent("""
            {first}:
                type: Bool
                default: true
                desc: Hello World

            {second}:
                type: Bool
                default: true
                desc: Hello World
        """.format(first=first, second=second))
        if shadowing:
            with pytest.raises(ValueError, match='Shadowing keys'):
                configdata._read_yaml(data)
        else:
            configdata._read_yaml(data)

    def test_rename(self):
        yaml_data = textwrap.dedent("""
            test:
                renamed: test_new

            test_new:
                type: Bool
                default: true
                desc: Hello World
        """)
        data, migrations = configdata._read_yaml(yaml_data)
        assert data.keys() == {'test_new'}
        assert migrations.renamed == {'test': 'test_new'}

    def test_rename_unknown_target(self):
        yaml_data = textwrap.dedent("""
            test:
                renamed: test2
        """)
        with pytest.raises(ValueError, match='Renaming test to unknown test2'):
            configdata._read_yaml(yaml_data)

    def test_delete(self):
        yaml_data = textwrap.dedent("""
            test:
                deleted: true
        """)
        data, migrations = configdata._read_yaml(yaml_data)
        assert not data.keys()
        assert migrations.deleted == ['test']

    def test_delete_invalid_value(self):
        yaml_data = textwrap.dedent("""
            test:
                deleted: false
        """)
        with pytest.raises(ValueError, match='Invalid deleted value: False'):
            configdata._read_yaml(yaml_data)


class TestParseYamlType:

    def _yaml(self, s):
        """Get the type from parsed YAML data."""
        return yaml.safe_load(textwrap.dedent(s))['type']

    def test_simple(self):
        """Test type which is only a name."""
        data = self._yaml("type: Bool")
        typ = configdata._parse_yaml_type('test', data)
        assert isinstance(typ, configtypes.Bool)
        assert not typ.none_ok

    def test_complex(self):
        """Test type parsing with arguments."""
        data = self._yaml("""
            type:
              name: String
              minlen: 2
        """)
        typ = configdata._parse_yaml_type('test', data)
        assert isinstance(typ, configtypes.String)
        assert not typ.none_ok
        assert typ.minlen == 2

    def test_list(self):
        """Test type parsing with a list and subtypes."""
        data = self._yaml("""
            type:
              name: List
              valtype: String
        """)
        typ = configdata._parse_yaml_type('test', data)
        assert isinstance(typ, configtypes.List)
        assert isinstance(typ.valtype, configtypes.String)
        assert not typ.none_ok
        assert not typ.valtype.none_ok

    def test_dict(self):
        """Test type parsing with a dict and subtypes."""
        data = self._yaml("""
            type:
              name: Dict
              keytype: String
              valtype:
                name: Int
                minval: 10
        """)
        typ = configdata._parse_yaml_type('test', data)
        assert isinstance(typ, configtypes.Dict)
        assert isinstance(typ.keytype, configtypes.String)
        assert isinstance(typ.valtype, configtypes.Int)
        assert not typ.none_ok
        assert typ.valtype.minval == 10

    def test_invalid_node(self):
        """Test type parsing with invalid node type."""
        data = self._yaml("type: 42")
        with pytest.raises(ValueError, match="Invalid node for test while "
                                             "reading type: 42"):
            configdata._parse_yaml_type('test', data)

    def test_unknown_type(self):
        """Test type parsing with type which doesn't exist."""
        data = self._yaml("type: Foobar")
        with pytest.raises(AttributeError,
                           match="Did not find type Foobar for test"):
            configdata._parse_yaml_type('test', data)

    def test_unknown_dict(self):
        """Test type parsing with a dict without keytype."""
        data = self._yaml("type: Dict")
        with pytest.raises(ValueError, match="Invalid node for test while "
                                             "reading 'keytype': 'Dict'"):
            configdata._parse_yaml_type('test', data)

    def test_unknown_args(self):
        """Test type parsing with unknown type arguments."""
        data = self._yaml("""
            type:
              name: Int
              answer: 42
        """)
        with pytest.raises(TypeError, match="Error while creating Int"):
            configdata._parse_yaml_type('test', data)


class TestParseYamlBackend:

    def _yaml(self, s):
        """Get the type from parsed YAML data."""
        return yaml.safe_load(textwrap.dedent(s))['backend']

    @pytest.mark.parametrize('backend, expected', [
        ('QtWebKit', [usertypes.Backend.QtWebKit]),
        ('QtWebEngine', [usertypes.Backend.QtWebEngine]),
        # This is also what _parse_yaml_backends gets when backend: is not
        # given at all
        ('null', [usertypes.Backend.QtWebKit, usertypes.Backend.QtWebEngine]),
    ])
    def test_simple(self, backend, expected):
        """Check a simple "backend: QtWebKit"."""
        data = self._yaml("backend: {}".format(backend))
        backends = configdata._parse_yaml_backends('test', data)
        assert backends == expected

    @pytest.mark.parametrize('webkit, has_new_version, expected', [
        (True, True, [usertypes.Backend.QtWebEngine,
                      usertypes.Backend.QtWebKit]),
        (False, True, [usertypes.Backend.QtWebEngine]),
        (True, False, [usertypes.Backend.QtWebKit]),
    ])
    def test_dict(self, monkeypatch, webkit, has_new_version, expected):
        data = self._yaml("""
            backend:
              QtWebKit: {}
              QtWebEngine: Qt 5.8
        """.format('true' if webkit else 'false'))
        monkeypatch.setattr(configdata.qtutils, 'version_check',
                            lambda v: has_new_version)
        backends = configdata._parse_yaml_backends('test', data)
        assert backends == expected

    @pytest.mark.parametrize('yaml_data', [
        # Wrong type
        "backend: 42",
        # Unknown key
        """
        backend:
          QtWebKit: true
          QtWebEngine: true
          foo: bar
        """,
        # Missing key
        """
        backend:
          QtWebKit: true
        """,
    ])
    def test_invalid_backend(self, yaml_data):
        with pytest.raises(ValueError, match="Invalid node for test while "
                                             "reading backends:"):
            configdata._parse_yaml_backends('test', self._yaml(yaml_data))