summaryrefslogtreecommitdiff
path: root/tests/test_rules_xml.py
blob: 85ad7174fbb24c194fb223a488b4fd202c0508b8 (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
#!/usr/bin/env python3
#
# Call with pytest. Requires XKB_CONFIG_ROOT to be set

import os
import pytest
from pathlib import Path
import xml.etree.ElementTree as ET


def _xkb_config_root():
    path = os.getenv('XKB_CONFIG_ROOT')
    assert path is not None, 'Environment variable XKB_CONFIG_ROOT must be set'
    print(f'Using XKB_CONFIG_ROOT={path}')

    xkbpath = Path(path)
    assert (xkbpath / 'rules').exists(), f'{path} is not an XKB installation'
    return xkbpath


@pytest.fixture
def xkb_config_root():
    return _xkb_config_root()


def iterate_layouts_variants(rules_xml):
    '''
    Return an iterator of type (layout, variant) for each element in the XML
    file.
    '''
    tree = ET.parse(rules_xml)
    root = tree.getroot()
    for layout in root.iter('layout'):
        yield layout, None

        for variant in layout.iter('variant'):
            yield layout, variant


def pytest_generate_tests(metafunc):
    # for any test_foo function with an argument named rules_xml,
    # make it the list of XKB_CONFIG_ROOT/rules/*.xml files.
    if 'rules_xml' in metafunc.fixturenames:
        rules_xml = list(_xkb_config_root().glob('rules/*.xml'))
        assert rules_xml
        metafunc.parametrize('rules_xml', rules_xml)
    # for any test_foo function with an argument named layout,
    # make it a Layout wrapper class for all layout(variant) combinations
    elif 'layout' in metafunc.fixturenames:
        rules_xml = list(_xkb_config_root().glob('rules/*.xml'))
        assert rules_xml
        layouts = []
        for f in rules_xml:
            for l, v in iterate_layouts_variants(f):
                layouts.append(Layout(f, l, v))
        metafunc.parametrize('layout', layouts)



class Layout:
    '''
    Wrapper class for layout/variants - both ConfigItems are available but
    the properties automatically pick the variant (if it exists) or the
    layout otherwise.
    '''
    def __init__(self, rulesfile, layout, variant=None):
        self.rulesfile = rulesfile
        self.layout = ConfigItem.from_elem(layout)
        self.variant = ConfigItem.from_elem(variant) if variant else None
        if variant:
            self.name = f"{self.layout.name}({self.variant.name})"
        else:
            self.name = f"{self.layout.name}"

    def _fetch(self, name):
        parent = self.variant or self.layout
        elements = parent.findall(name)
        if elements is None:
            return None
        elif len(elements) > 1:
            return elements
        else:
            return elements[0]

    @property
    def iso3166(self):
        return (self.variant or self.layout).iso3166

    @property
    def iso639(self):
        return (self.variant or self.layout).iso639


def prettyxml(element):
    return ET.tostring(element).decode('utf-8')


class ConfigItem:
    def __init__(self, name, shortDescription=None, description=None):
        self.name = name
        self.shortDescription = shortDescription
        self.description = description
        self.iso639 = []
        self.iso3166 = []

    @classmethod
    def _fetch_subelement(cls, parent, name):
        sub_element = parent.findall(name)
        if sub_element is not None and len(sub_element) == 1:
            return sub_element[0]
        else:
            return None

    @classmethod
    def _fetch_subelement_text(cls, parent, name):
        sub_element = parent.findall(name)
        return [e.text for e in sub_element]

    @classmethod
    def _fetch_text(cls, parent, name):
        sub_element = cls._fetch_subelement(parent, name)
        if sub_element is None:
            return None
        return sub_element.text

    @classmethod
    def from_elem(cls, elem):
        try:
            ci_element = cls._fetch_subelement(elem, 'configItem')
            name = cls._fetch_text(ci_element, 'name')
            assert name is not None
            # shortDescription and description are optional
            sdesc = cls._fetch_text(ci_element, 'shortDescription')
            desc = cls._fetch_text(ci_element, 'description')
            ci = ConfigItem(name, sdesc, desc)

            langlist = cls._fetch_subelement(ci_element, 'languageList')
            if langlist:
                ci.iso639 = cls._fetch_subelement_text(langlist, 'iso639Id')

            langlist = cls._fetch_subelement(ci_element, 'languageList')
            if langlist:
                ci.iso639 = cls._fetch_subelement_text(langlist, 'iso639Id')

            countrylist = cls._fetch_subelement(ci_element, 'countryList')
            if countrylist:
                ci.iso3166 = cls._fetch_subelement_text(countrylist, 'iso3166')

            return ci
        except AssertionError as e:
            endl = "\n"  # f{} cannot contain backslashes
            e.args = (f'\nFor element {prettyxml(elem)}\n{endl.join(e.args)}',)
            raise


def test_duplicate_layouts(rules_xml):
    tree = ET.parse(rules_xml)
    root = tree.getroot()
    layouts = {}
    for layout in root.iter('layout'):
        ci = ConfigItem.from_elem(layout)
        assert ci.name not in layouts, f'Duplicate layout {ci.name}'
        layouts[ci.name] = True

        variants = {}
        for variant in layout.iter('variant'):
            vci = ConfigItem.from_elem(variant)
            assert vci.name not in variants, \
                f'{rules_xml}: duplicate variant {ci.name}({vci.name}):\n{prettyxml(variant)}'
            variants[vci.name] = True


def test_duplicate_models(rules_xml):
    tree = ET.parse(rules_xml)
    root = tree.getroot()
    models = {}
    for model in root.iter('model'):
        ci = ConfigItem.from_elem(model)
        assert ci.name not in models, f'Duplicate model {ci.name}'
        models[ci.name] = True


def test_iso3166(layout):
    pycountry = pytest.importorskip('pycountry')
    country_codes = [c.alpha_2 for c in pycountry.countries]
    for code in layout.iso3166:
        assert code in country_codes, \
            f'{layout.rulesfile}: unknown country code "{code}" in {layout.name}'


def test_iso639(layout):
    pycountry = pytest.importorskip('pycountry')

    # A list of languages not in pycountry, so we need to special-case them
    special_langs = [
        'ber',  # Berber languages (collective), https://iso639-3.sil.org/code/ber
        'btb',  # Beti (Cameroon), https://iso639-3.sil.org/code/btb
        'fox',  # Formosan languages (collective), https://iso639-3.sil.org/code/fox
        'phi',  # Philippine languages (collective), https://iso639-3.sil.org/code/phi
        'ovd',  # Elfdalian, https://iso639-3.sil.org/code/ovd
    ]
    language_codes = [c.alpha_3 for c in pycountry.languages] + special_langs
    for code in layout.iso639:
        assert code in language_codes, \
            f'{layout.rulesfile}: unknown language code "{code}" in {layout.name}'