test: rework the output for the xkeyboard-config layout tester
[platform/upstream/libxkbcommon.git] / test / xkeyboard-config-test.py.in
1 #!/usr/bin/env python3
2 import argparse
3 import multiprocessing
4 import sys
5 import subprocess
6 import os
7 import xml.etree.ElementTree as ET
8
9
10 verbose = False
11
12 DEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml'
13
14 # Meson needs to fill this in so we can call the tool in the buildir.
15 EXTRA_PATH = '@MESON_BUILD_ROOT@'
16 os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
17
18
19 def escape(s):
20     return s.replace('"', '\\"')
21
22
23 # The function generating the progress bar (if any).
24 def create_progress_bar(verbose):
25     def noop_progress_bar(x, total):
26         return x
27
28     progress_bar = noop_progress_bar
29     if not verbose and os.isatty(sys.stdout.fileno()):
30         try:
31             from tqdm import tqdm
32             progress_bar = tqdm
33         except ImportError:
34             pass
35
36     return progress_bar
37
38
39 class Invocation:
40     def __init__(self, r, m, l, v, o):
41         self.command = ""
42         self.rules = r
43         self.model = m
44         self.layout = l
45         self.variant = v
46         self.option = o
47         self.exitstatus = 77  # default to skipped
48         self.error = None
49         self.keymap = None  # The fully compiled keymap
50
51     @property
52     def rmlvo(self):
53         return self.rules, self.model, self.layout, self.variant, self.option
54
55     def __str__(self):
56         s = []
57         rmlvo = [x or "" for x in self.rmlvo]
58         rmlvo = ', '.join([f'"{x}"' for x in rmlvo])
59         s.append(f'- rmlvo: [{rmlvo}]')
60         s.append(f'  cmd: "{escape(self.command)}"')
61         s.append(f'  status: {self.exitstatus}')
62         if self.error:
63             s.append(f'  error: "{escape(self.error.strip())}"')
64         return '\n'.join(s)
65
66     def run(self):
67         raise NotImplementedError
68
69
70 class XkbCompInvocation(Invocation):
71     def run(self):
72         r, m, l, v, o = self.rmlvo
73         args = ['setxkbmap', '-print']
74         if r is not None:
75             args.append('-rules')
76             args.append('{}'.format(r))
77         if m is not None:
78             args.append('-model')
79             args.append('{}'.format(m))
80         if l is not None:
81             args.append('-layout')
82             args.append('{}'.format(l))
83         if v is not None:
84             args.append('-variant')
85             args.append('{}'.format(v))
86         if o is not None:
87             args.append('-option')
88             args.append('{}'.format(o))
89
90         xkbcomp_args = ['xkbcomp', '-xkb', '-', '-']
91
92         self.command = " ".join(args + ["|"] + xkbcomp_args)
93
94         setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE,
95                                      stderr=subprocess.PIPE, universal_newlines=True)
96         stdout, stderr = setxkbmap.communicate()
97         if "Cannot open display" in stderr:
98             self.error = stderr
99             self.exitstatus = 90
100         else:
101             xkbcomp = subprocess.Popen(xkbcomp_args, stdin=subprocess.PIPE,
102                                        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
103                                        universal_newlines=True)
104             stdout, stderr = xkbcomp.communicate(stdout)
105             if xkbcomp.returncode != 0:
106                 self.error = "failed to compile keymap"
107                 self.exitstatus = xkbcomp.returncode
108             else:
109                 self.keymap = stdout
110                 self.exitstatus = 0
111
112
113 class XkbcommonInvocation(Invocation):
114     def run(self):
115         r, m, l, v, o = self.rmlvo
116         args = [
117             'xkbcli-compile-keymap',  # this is run in the builddir
118             '--verbose',
119             '--rules', r,
120             '--model', m,
121             '--layout', l,
122         ]
123         if v is not None:
124             args += ['--variant', v]
125         if o is not None:
126             args += ['--options', o]
127
128         self.command = " ".join(args)
129         try:
130             output = subprocess.check_output(args, stderr=subprocess.STDOUT,
131                                              universal_newlines=True)
132             if "unrecognized keysym" in output:
133                 for line in output.split('\n'):
134                     if "unrecognized keysym" in line:
135                         self.error = line
136                 self.exitstatus = 99  # tool doesn't generate this one
137             else:
138                 self.exitstatus = 0
139                 self.keymap = output
140         except subprocess.CalledProcessError as err:
141             self.error = "failed to compile keymap"
142             self.exitstatus = err.returncode
143
144
145 def xkbcommontool(rmlvo):
146     try:
147         r = rmlvo.get('r', 'evdev')
148         m = rmlvo.get('m', 'pc105')
149         l = rmlvo.get('l', 'us')
150         v = rmlvo.get('v', None)
151         o = rmlvo.get('o', None)
152         tool = XkbcommonInvocation(r, m, l, v, o)
153         tool.run()
154         return tool
155     except KeyboardInterrupt:
156         pass
157
158
159 def xkbcomp(rmlvo):
160     try:
161         r = rmlvo.get('r', 'evdev')
162         m = rmlvo.get('m', 'pc105')
163         l = rmlvo.get('l', 'us')
164         v = rmlvo.get('v', None)
165         o = rmlvo.get('o', None)
166         tool = XkbCompInvocation(r, m, l, v, o)
167         tool.run()
168         return tool
169     except KeyboardInterrupt:
170         pass
171
172
173 def parse(path):
174     root = ET.fromstring(open(path).read())
175     layouts = root.findall('layoutList/layout')
176
177     options = [
178         e.text
179         for e in root.findall('optionList/group/option/configItem/name')
180     ]
181
182     combos = []
183     for l in layouts:
184         layout = l.find('configItem/name').text
185         combos.append({'l': layout})
186
187         variants = l.findall('variantList/variant')
188         for v in variants:
189             variant = v.find('configItem/name').text
190
191             combos.append({'l': layout, 'v': variant})
192             for option in options:
193                 combos.append({'l': layout, 'v': variant, 'o': option})
194
195     return combos
196
197
198 def run(combos, tool, njobs):
199     failed = False
200     with multiprocessing.Pool(njobs) as p:
201         results = p.imap_unordered(tool, combos)
202         for invocation in progress_bar(results, total=len(combos)):
203             if invocation.exitstatus != 0:
204                 failed = True
205                 target = sys.stderr
206             else:
207                 target = sys.stdout if verbose else None
208
209             if target:
210                 print(invocation, file=target)
211
212     return failed
213
214
215 def main(args):
216     global progress_bar
217     global verbose
218
219     tools = {
220         'libxkbcommon': xkbcommontool,
221         'xkbcomp': xkbcomp,
222     }
223
224     parser = argparse.ArgumentParser(
225         description='Tool to test all layout/variant/option combinations.'
226     )
227     parser.add_argument('path', metavar='/path/to/evdev.xml',
228                         nargs='?', type=str,
229                         default=DEFAULT_RULES_XML,
230                         help='Path to xkeyboard-config\'s evdev.xml')
231     parser.add_argument('--tool', choices=tools.keys(),
232                         type=str, default='libxkbcommon',
233                         help='parsing tool to use')
234     parser.add_argument('--jobs', '-j', type=int,
235                         default=os.cpu_count() * 4,
236                         help='number of processes to use')
237     parser.add_argument('--verbose', '-v', default=False, action="store_true")
238     args = parser.parse_args()
239
240     verbose = args.verbose
241     progress_bar = create_progress_bar(verbose)
242
243     tool = tools[args.tool]
244
245     combos = parse(args.path)
246     failed = run(combos, tool, args.jobs)
247     sys.exit(failed)
248
249
250 if __name__ == '__main__':
251     try:
252         main(sys.argv)
253     except KeyboardInterrupt:
254         print('# Exiting after Ctrl+C')