77cff1f06515466961eb5e4c1c2e4f4da15f0788
[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 from pathlib import Path
9
10
11 verbose = False
12
13 DEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml'
14
15 # Meson needs to fill this in so we can call the tool in the buildir.
16 EXTRA_PATH = '@MESON_BUILD_ROOT@'
17 os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
18
19
20 def escape(s):
21     return s.replace('"', '\\"')
22
23
24 # The function generating the progress bar (if any).
25 def create_progress_bar(verbose):
26     def noop_progress_bar(x, total, file=None):
27         return x
28
29     progress_bar = noop_progress_bar
30     if not verbose and os.isatty(sys.stdout.fileno()):
31         try:
32             from tqdm import tqdm
33             progress_bar = tqdm
34         except ImportError:
35             pass
36
37     return progress_bar
38
39
40 class Invocation:
41     def __init__(self, r, m, l, v, o):
42         self.command = ""
43         self.rules = r
44         self.model = m
45         self.layout = l
46         self.variant = v
47         self.option = o
48         self.exitstatus = 77  # default to skipped
49         self.error = None
50         self.keymap = None  # The fully compiled keymap
51
52     @property
53     def rmlvo(self):
54         return self.rules, self.model, self.layout, self.variant, self.option
55
56     def __str__(self):
57         s = []
58         rmlvo = [x or "" for x in self.rmlvo]
59         rmlvo = ', '.join([f'"{x}"' for x in rmlvo])
60         s.append(f'- rmlvo: [{rmlvo}]')
61         s.append(f'  cmd: "{escape(self.command)}"')
62         s.append(f'  status: {self.exitstatus}')
63         if self.error:
64             s.append(f'  error: "{escape(self.error.strip())}"')
65         return '\n'.join(s)
66
67     def run(self):
68         raise NotImplementedError
69
70
71 class XkbCompInvocation(Invocation):
72     def run(self):
73         r, m, l, v, o = self.rmlvo
74         args = ['setxkbmap', '-print']
75         if r is not None:
76             args.append('-rules')
77             args.append('{}'.format(r))
78         if m is not None:
79             args.append('-model')
80             args.append('{}'.format(m))
81         if l is not None:
82             args.append('-layout')
83             args.append('{}'.format(l))
84         if v is not None:
85             args.append('-variant')
86             args.append('{}'.format(v))
87         if o is not None:
88             args.append('-option')
89             args.append('{}'.format(o))
90
91         xkbcomp_args = ['xkbcomp', '-xkb', '-', '-']
92
93         self.command = " ".join(args + ["|"] + xkbcomp_args)
94
95         setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE,
96                                      stderr=subprocess.PIPE, universal_newlines=True)
97         stdout, stderr = setxkbmap.communicate()
98         if "Cannot open display" in stderr:
99             self.error = stderr
100             self.exitstatus = 90
101         else:
102             xkbcomp = subprocess.Popen(xkbcomp_args, stdin=subprocess.PIPE,
103                                        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
104                                        universal_newlines=True)
105             stdout, stderr = xkbcomp.communicate(stdout)
106             if xkbcomp.returncode != 0:
107                 self.error = "failed to compile keymap"
108                 self.exitstatus = xkbcomp.returncode
109             else:
110                 self.keymap = stdout
111                 self.exitstatus = 0
112
113
114 class XkbcommonInvocation(Invocation):
115     UNRECOGNIZED_KEYSYM_ERROR = "XKB-107"
116
117     def run(self):
118         r, m, l, v, o = self.rmlvo
119         args = [
120             'xkbcli-compile-keymap',  # this is run in the builddir
121             '--verbose',
122             '--rules', r,
123             '--model', m,
124             '--layout', l,
125         ]
126         if v is not None:
127             args += ['--variant', v]
128         if o is not None:
129             args += ['--options', o]
130
131         self.command = " ".join(args)
132         try:
133             output = subprocess.check_output(args, stderr=subprocess.STDOUT,
134                                              universal_newlines=True)
135             if self.UNRECOGNIZED_KEYSYM_ERROR in output:
136                 for line in output.split('\n'):
137                     if self.UNRECOGNIZED_KEYSYM_ERROR in line:
138                         self.error = line
139                 self.exitstatus = 99  # tool doesn't generate this one
140             else:
141                 self.exitstatus = 0
142                 self.keymap = output
143         except subprocess.CalledProcessError as err:
144             self.error = "failed to compile keymap"
145             self.exitstatus = err.returncode
146
147
148 def xkbcommontool(rmlvo):
149     try:
150         r = rmlvo.get('r', 'evdev')
151         m = rmlvo.get('m', 'pc105')
152         l = rmlvo.get('l', 'us')
153         v = rmlvo.get('v', None)
154         o = rmlvo.get('o', None)
155         tool = XkbcommonInvocation(r, m, l, v, o)
156         tool.run()
157         return tool
158     except KeyboardInterrupt:
159         pass
160
161
162 def xkbcomp(rmlvo):
163     try:
164         r = rmlvo.get('r', 'evdev')
165         m = rmlvo.get('m', 'pc105')
166         l = rmlvo.get('l', 'us')
167         v = rmlvo.get('v', None)
168         o = rmlvo.get('o', None)
169         tool = XkbCompInvocation(r, m, l, v, o)
170         tool.run()
171         return tool
172     except KeyboardInterrupt:
173         pass
174
175
176 def parse(path):
177     root = ET.fromstring(open(path).read())
178     layouts = root.findall('layoutList/layout')
179
180     options = [
181         e.text
182         for e in root.findall('optionList/group/option/configItem/name')
183     ]
184
185     combos = []
186     for l in layouts:
187         layout = l.find('configItem/name').text
188         combos.append({'l': layout})
189
190         variants = l.findall('variantList/variant')
191         for v in variants:
192             variant = v.find('configItem/name').text
193
194             combos.append({'l': layout, 'v': variant})
195             for option in options:
196                 combos.append({'l': layout, 'v': variant, 'o': option})
197
198     return combos
199
200
201 def run(combos, tool, njobs, keymap_output_dir):
202     if keymap_output_dir:
203         keymap_output_dir = Path(keymap_output_dir)
204         try:
205             keymap_output_dir.mkdir()
206         except FileExistsError as e:
207             print(e, file=sys.stderr)
208             return False
209
210     keymap_file = None
211     keymap_file_fd = None
212
213     failed = False
214     with multiprocessing.Pool(njobs) as p:
215         results = p.imap_unordered(tool, combos)
216         for invocation in progress_bar(results, total=len(combos), file=sys.stdout):
217             if invocation.exitstatus != 0:
218                 failed = True
219                 target = sys.stderr
220             else:
221                 target = sys.stdout if verbose else None
222
223             if target:
224                 print(invocation, file=target)
225
226             if keymap_output_dir:
227                 # we're running through the layouts in a somewhat sorted manner,
228                 # so let's keep the fd open until we switch layouts
229                 layout = invocation.layout
230                 if invocation.variant:
231                     layout += f"({invocation.variant})"
232                 fname = keymap_output_dir / layout
233                 if fname != keymap_file:
234                     keymap_file = fname
235                     if keymap_file_fd:
236                         keymap_file_fd.close()
237                     keymap_file_fd = open(keymap_file, 'a')
238
239                 rmlvo = ', '.join([x or '' for x in invocation.rmlvo])
240                 print(f"// {rmlvo}", file=keymap_file_fd)
241                 print(invocation.keymap, file=keymap_file_fd)
242                 keymap_file_fd.flush()
243
244     return failed
245
246
247 def main(args):
248     global progress_bar
249     global verbose
250
251     tools = {
252         'libxkbcommon': xkbcommontool,
253         'xkbcomp': xkbcomp,
254     }
255
256     parser = argparse.ArgumentParser(
257         description='''
258                     This tool compiles a keymap for each layout, variant and
259                     options combination in the given rules XML file. The output
260                     of this tool is YAML, use your favorite YAML parser to
261                     extract error messages. Errors are printed to stderr.
262                     '''
263     )
264     parser.add_argument('path', metavar='/path/to/evdev.xml',
265                         nargs='?', type=str,
266                         default=DEFAULT_RULES_XML,
267                         help='Path to xkeyboard-config\'s evdev.xml')
268     parser.add_argument('--tool', choices=tools.keys(),
269                         type=str, default='libxkbcommon',
270                         help='parsing tool to use')
271     parser.add_argument('--jobs', '-j', type=int,
272                         default=os.cpu_count() * 4,
273                         help='number of processes to use')
274     parser.add_argument('--verbose', '-v', default=False, action="store_true")
275     parser.add_argument('--keymap-output-dir', default=None, type=str,
276                         help='Directory to print compiled keymaps to')
277     parser.add_argument('--layout', default=None, type=str,
278                         help='Only test the given layout')
279     parser.add_argument('--variant', default=None, type=str,
280                         help='Only test the given variant')
281     parser.add_argument('--option', default=None, type=str,
282                         help='Only test the given option')
283
284     args = parser.parse_args()
285
286     verbose = args.verbose
287     keymapdir = args.keymap_output_dir
288     progress_bar = create_progress_bar(verbose)
289
290     tool = tools[args.tool]
291
292     if any([args.layout, args.variant, args.option]):
293         combos = [{
294             'l': args.layout,
295             'v': args.variant,
296             'o': args.option,
297         }]
298     else:
299         combos = parse(args.path)
300     failed = run(combos, tool, args.jobs, keymapdir)
301     sys.exit(failed)
302
303
304 if __name__ == '__main__':
305     try:
306         main(sys.argv)
307     except KeyboardInterrupt:
308         print('# Exiting after Ctrl+C')