1import json
2import pytest
3import conftest
4import os
5import re
6
7
8@pytest.fixture()
9def json_doc():
10    json_filepath = os.path.join(conftest.BINDIR, 'doc/hlwm-doc.json')
11    with open(json_filepath, 'r') as fh:
12        doc = json.loads(fh.read())
13    return doc
14
15
16def create_client(hlwm):
17    winid, _ = hlwm.create_client()
18    return f'clients.{winid}'
19
20
21def create_clients_with_all_links(hlwm):
22    # enforce that 'clients.focus' exists
23    winid, _ = hlwm.create_client()
24    # enforce that 'clients.dragged' exists
25    hlwm.call('floating on')
26    hlwm.call('drag "" move')
27    return 'clients'
28
29
30def create_frame_split(hlwm):
31    hlwm.call('split explode')
32    return 'tags.0.tiling.root'
33
34
35def create_tag_with_all_links(hlwm):
36    """create a tag with focused_client set"""
37    hlwm.create_client()
38    return 'tags.0'
39
40
41# map every c++ class name to a function ("constructor") accepting an hlwm
42# fixture and returning the path to an example object of the C++ class
43classname2examplepath = [
44    ('ByName', lambda _: 'monitors.by-name'),
45    ('Client', create_client),
46    ('ClientManager', create_clients_with_all_links),
47    ('DecTriple', lambda _: 'theme.tiling'),
48    ('DecorationScheme', lambda _: 'theme.tiling.urgent'),
49    ('FrameLeaf', lambda _: 'tags.0.tiling.root'),
50    ('FrameSplit', create_frame_split),
51    ('HSTag', create_tag_with_all_links),
52    ('Monitor', lambda _: 'monitors.0'),
53    ('MonitorManager', lambda _: 'monitors'),
54    ('Root', lambda _: ''),
55    ('Settings', lambda _: 'settings'),
56    ('TagManager', lambda _: 'tags'),
57    ('Theme', lambda _: 'theme'),
58]
59
60
61@pytest.mark.parametrize('clsname,object_path', classname2examplepath)
62def test_documented_attributes_writable(hlwm, clsname, object_path, json_doc):
63    """test whether the writable field is correct. This checks the
64    existence of the attributes implicitly
65    """
66    object_path = object_path(hlwm)
67    for _, attr in json_doc['objects'][clsname]['attributes'].items():
68        print("checking attribute {}::{}".format(clsname, attr['cpp_name']))
69        full_attr_path = '{}.{}'.format(object_path, attr['name']).lstrip('.')
70        value = hlwm.get_attr(full_attr_path)
71        if value == 'default':
72            continue
73        if attr['writable']:
74            hlwm.call(['set_attr', full_attr_path, value])
75        else:
76            hlwm.call_xfail(['set_attr', full_attr_path, value]) \
77                .expect_stderr('attribute is read-only')
78
79
80def types_and_shorthands():
81    """a mapping from type names in the json doc to their
82    one letter short hands in the output of 'attr'
83    """
84    return {
85        'int': 'i',
86        'uint': 'u',
87        'bool': 'b',
88        'decimal': 'd',
89        'color': 'c',
90        'string': 's',
91        'regex': 'r',
92        'SplitAlign': 'n',
93        'LayoutAlgorithm': 'n',
94        'font': 'f',
95        'Rectangle': 'R',
96    }
97
98
99@pytest.mark.parametrize('clsname,object_path', classname2examplepath)
100def test_documented_attribute_type(hlwm, clsname, object_path, json_doc):
101    object_path = object_path(hlwm)
102    attr_output = hlwm.call(['attr', object_path]).stdout.splitlines()
103    attr_output = [line.split(' ') for line in attr_output if '=' in line]
104    attrname2shorttype = {line[4]: line[1] for line in attr_output}
105    fulltype2shorttype = types_and_shorthands()
106    for _, attr in json_doc['objects'][clsname]['attributes'].items():
107        assert fulltype2shorttype[attr['type']] == attrname2shorttype[attr['name']]
108
109
110@pytest.mark.parametrize('clsname,object_path', classname2examplepath)
111def test_documented_children_exist(hlwm, clsname, object_path, json_doc):
112    object_path = object_path(hlwm)
113    object_path_dot = object_path + '.' if object_path != '' else ''
114    for _, child in json_doc['objects'][clsname]['children'].items():
115        hlwm.call(['object_tree', object_path_dot + child['name']])
116
117
118@pytest.mark.parametrize('clsname,object_path', classname2examplepath)
119def test_attributes_and_children_are_documented(hlwm, clsname, object_path, json_doc):
120    # if a path matches the following re, then it's OK if it
121    # is not mentioned explicitly in the docs
122    undocumented_paths = '|'.join([
123        r'tags\.[0-9]+',
124        r'clients\.0x[0-9a-f]+',
125        r'monitors\.[0-9]+',
126        r'tags\.by-name\.default',
127    ])
128    undocumented_path_re = re.compile(r'^({})[\. ]*$'.format(undocumented_paths))
129
130    object_path = object_path(hlwm)
131    object_path_dot = object_path + '.' if object_path != '' else ''
132
133    entries = hlwm.complete(['get_attr', object_path_dot], position=1, partial=True)
134    for full_entry_path in entries:
135        if undocumented_path_re.match(full_entry_path):
136            continue
137        assert full_entry_path[0:len(object_path_dot)] == object_path_dot
138        entry = full_entry_path[len(object_path_dot):]
139        if entry[-1] == '.':
140            assert entry[0:-1] in json_doc['objects'][clsname]['children']
141        else:
142            assert entry[-1] == ' ', "it's an attribute if it's no child"
143            assert entry[0:-1] in json_doc['objects'][clsname]['attributes']
144
145
146@pytest.mark.parametrize('clsname,object_path', classname2examplepath)
147def test_class_doc(hlwm, clsname, object_path, json_doc):
148    path = object_path(hlwm)
149    attr_output = hlwm.call(['attr', path]).stdout
150
151    object_doc = json_doc['objects'][clsname].get('doc', None)
152    if object_doc is not None:
153        assert attr_output.startswith(object_doc)
154    else:
155        # if no class doc is in the json file, then there
156        # is indeed none:
157        assert re.match(r'1 child:|[0-9]* children[\.:]$', attr_output.splitlines()[0])
158
159
160@pytest.mark.parametrize('clsname,object_path', classname2examplepath)
161def test_help_on_attribute_vs_json(hlwm, clsname, object_path, json_doc):
162    path = object_path(hlwm)
163    attrs_doc = json_doc['objects'][clsname]['attributes']
164    for _, attr in attrs_doc.items():
165        attr_name = attr['name']
166        help_txt = hlwm.call(['help', f'{path}.{attr_name}'.lstrip('.')]).stdout
167
168        assert f"Attribute '{attr_name}'" in help_txt
169        doc = attr.get('doc', '')
170        assert doc in help_txt
171
172
173@pytest.mark.parametrize('clsname,object_path', classname2examplepath)
174def test_help_on_children_vs_json(hlwm, clsname, object_path, json_doc):
175    path = object_path(hlwm)
176    child_doc = json_doc['objects'][clsname]['children']
177    for _, child in child_doc.items():
178        name = child['name']
179        help_txt = hlwm.call(['help', f'{path}.{name}'.lstrip('.')]).stdout
180
181        if 'doc' in child:
182            assert f"Entry '{name}'" in help_txt
183            assert child['doc'] in help_txt
184