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