Add LICENSE.BSD-3-Clause file
[platform/upstream/libxkbcommon.git] / test / xkeyboard-config-test.py.in
index e965858..c33e707 100755 (executable)
-#!/usr/bin/env python
+#!/usr/bin/env python3
 import argparse
+import multiprocessing
 import sys
 import subprocess
 import os
-import io
 import xml.etree.ElementTree as ET
-from multiprocessing import Pool
+from pathlib import Path
 
 
-verbose = True
+verbose = False
 
-DEFAULT_RULES_XML = '@XKB_CONFIG_ROOT@/rules/evdev.xml'
+DEFAULT_RULES_XML = "@XKB_CONFIG_ROOT@/rules/evdev.xml"
 
 # Meson needs to fill this in so we can call the tool in the buildir.
-EXTRA_PATH = '@MESON_BUILD_ROOT@'
-os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
+EXTRA_PATH = "@MESON_BUILD_ROOT@"
+os.environ["PATH"] = ":".join([EXTRA_PATH, os.getenv("PATH")])
 
 
-def noop_progress_bar(x, desc):
-    return x
+def escape(s):
+    return s.replace('"', '\\"')
 
 
 # The function generating the progress bar (if any).
-progress_bar = noop_progress_bar
-if os.isatty(sys.stdout.fileno()):
-    try:
-        from tqdm import tqdm
-        progress_bar = tqdm
+def create_progress_bar(verbose):
+    def noop_progress_bar(x, total, file=None):
+        return x
 
-        verbose = False
-    except ImportError:
-        pass
+    progress_bar = noop_progress_bar
+    if not verbose and os.isatty(sys.stdout.fileno()):
+        try:
+            from tqdm import tqdm
+
+            progress_bar = tqdm
+        except ImportError:
+            pass
+
+    return progress_bar
+
+
+class Invocation:
+    def __init__(self, r, m, l, v, o):
+        self.command = ""
+        self.rules = r
+        self.model = m
+        self.layout = l
+        self.variant = v
+        self.option = o
+        self.exitstatus = 77  # default to skipped
+        self.error = None
+        self.keymap = None  # The fully compiled keymap
+
+    @property
+    def rmlvo(self):
+        return self.rules, self.model, self.layout, self.variant, self.option
+
+    def __str__(self):
+        s = []
+        rmlvo = [x or "" for x in self.rmlvo]
+        rmlvo = ", ".join([f'"{x}"' for x in rmlvo])
+        s.append(f"- rmlvo: [{rmlvo}]")
+        s.append(f'  cmd: "{escape(self.command)}"')
+        s.append(f"  status: {self.exitstatus}")
+        if self.error:
+            s.append(f'  error: "{escape(self.error.strip())}"')
+        return "\n".join(s)
+
+    def run(self):
+        raise NotImplementedError
+
+
+class XkbCompInvocation(Invocation):
+    def run(self):
+        r, m, l, v, o = self.rmlvo
+        args = ["setxkbmap", "-print"]
+        if r is not None:
+            args.append("-rules")
+            args.append("{}".format(r))
+        if m is not None:
+            args.append("-model")
+            args.append("{}".format(m))
+        if l is not None:
+            args.append("-layout")
+            args.append("{}".format(l))
+        if v is not None:
+            args.append("-variant")
+            args.append("{}".format(v))
+        if o is not None:
+            args.append("-option")
+            args.append("{}".format(o))
+
+        xkbcomp_args = ["xkbcomp", "-xkb", "-", "-"]
+
+        self.command = " ".join(args + ["|"] + xkbcomp_args)
+
+        setxkbmap = subprocess.Popen(
+            args,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            universal_newlines=True,
+        )
+        stdout, stderr = setxkbmap.communicate()
+        if "Cannot open display" in stderr:
+            self.error = stderr
+            self.exitstatus = 90
+        else:
+            xkbcomp = subprocess.Popen(
+                xkbcomp_args,
+                stdin=subprocess.PIPE,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                universal_newlines=True,
+            )
+            stdout, stderr = xkbcomp.communicate(stdout)
+            if xkbcomp.returncode != 0:
+                self.error = "failed to compile keymap"
+                self.exitstatus = xkbcomp.returncode
+            else:
+                self.keymap = stdout
+                self.exitstatus = 0
 
 
-def xkbcommontool(rmlvo):
-    try:
-        r = rmlvo.get('r', 'evdev')
-        m = rmlvo.get('m', 'pc105')
-        l = rmlvo.get('l', 'us')
-        v = rmlvo.get('v', None)
-        o = rmlvo.get('o', None)
+class XkbcommonInvocation(Invocation):
+    UNRECOGNIZED_KEYSYM_ERROR = "XKB-107"
+
+    def run(self):
+        r, m, l, v, o = self.rmlvo
         args = [
-            'rmlvo-to-keymap',
-            '--rules', r,
-            '--model', m,
-            '--layout', l,
+            "xkbcli-compile-keymap",  # this is run in the builddir
+            "--verbose",
+            "--rules",
+            r,
+            "--model",
+            m,
+            "--layout",
+            l,
         ]
         if v is not None:
-            args += ['--variant', v]
+            args += ["--variant", v]
         if o is not None:
-            args += ['--options', o]
-
-        success = True
-        out = io.StringIO()
-        if verbose:
-            print(':: {}'.format(' '.join(args)), file=out)
+            args += ["--options", o]
 
+        self.command = " ".join(args)
         try:
-            output = subprocess.check_output(args, stderr=subprocess.STDOUT)
-            if verbose:
-                print(output.decode('utf-8'), file=out)
+            output = subprocess.check_output(
+                args, stderr=subprocess.STDOUT, universal_newlines=True
+            )
+            if self.UNRECOGNIZED_KEYSYM_ERROR in output:
+                for line in output.split("\n"):
+                    if self.UNRECOGNIZED_KEYSYM_ERROR in line:
+                        self.error = line
+                self.exitstatus = 99  # tool doesn't generate this one
+            else:
+                self.exitstatus = 0
+                self.keymap = output
         except subprocess.CalledProcessError as err:
-            print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out)
-            print(err.output.decode('utf-8'), file=out)
-            success = False
+            self.error = "failed to compile keymap"
+            self.exitstatus = err.returncode
+
 
-        return success, out.getvalue()
+def xkbcommontool(rmlvo):
+    try:
+        r = rmlvo.get("r", "evdev")
+        m = rmlvo.get("m", "pc105")
+        l = rmlvo.get("l", "us")
+        v = rmlvo.get("v", None)
+        o = rmlvo.get("o", None)
+        tool = XkbcommonInvocation(r, m, l, v, o)
+        tool.run()
+        return tool
     except KeyboardInterrupt:
         pass
 
 
 def xkbcomp(rmlvo):
     try:
-        r = rmlvo.get('r', 'evdev')
-        m = rmlvo.get('m', 'pc105')
-        l = rmlvo.get('l', 'us')
-        v = rmlvo.get('v', None)
-        o = rmlvo.get('o', None)
-        args = ['setxkbmap', '-print']
-        if r is not None:
-            args.append('-rules')
-            args.append('{}'.format(r))
-        if m is not None:
-            args.append('-model')
-            args.append('{}'.format(m))
-        if l is not None:
-            args.append('-layout')
-            args.append('{}'.format(l))
-        if o is not None:
-            args.append('-option')
-            args.append('{}'.format(o))
-
-        success = True
-        out = io.StringIO()
-        if verbose:
-            print(':: {}'.format(' '.join(args)), file=out)
-
-        try:
-            xkbcomp_args = ['xkbcomp', '-xkb', '-', '-']
-
-            setxkbmap = subprocess.Popen(args, stdout=subprocess.PIPE)
-            xkbcomp = subprocess.Popen(xkbcomp_args, stdin=setxkbmap.stdout,
-                                       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            setxkbmap.stdout.close()
-            stdout, stderr = xkbcomp.communicate()
-            if xkbcomp.returncode != 0:
-                print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out)
-                success = False
-            if xkbcomp.returncode != 0 or verbose:
-                print(stdout.decode('utf-8'), file=out)
-                print(stderr.decode('utf-8'), file=out)
-
-        # This catches setxkbmap errors.
-        except subprocess.CalledProcessError as err:
-            print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out)
-            print(err.output.decode('utf-8'), file=out)
-            success = False
-
-        return success, out.getvalue()
+        r = rmlvo.get("r", "evdev")
+        m = rmlvo.get("m", "pc105")
+        l = rmlvo.get("l", "us")
+        v = rmlvo.get("v", None)
+        o = rmlvo.get("o", None)
+        tool = XkbCompInvocation(r, m, l, v, o)
+        tool.run()
+        return tool
     except KeyboardInterrupt:
         pass
 
 
 def parse(path):
     root = ET.fromstring(open(path).read())
-    layouts = root.findall('layoutList/layout')
+    layouts = root.findall("layoutList/layout")
 
-    options = [
-        e.text
-        for e in root.findall('optionList/group/option/configItem/name')
-    ]
+    options = [e.text for e in root.findall("optionList/group/option/configItem/name")]
 
     combos = []
     for l in layouts:
-        layout = l.find('configItem/name').text
-        combos.append({'l': layout})
+        layout = l.find("configItem/name").text
+        combos.append({"l": layout})
 
-        variants = l.findall('variantList/variant')
+        variants = l.findall("variantList/variant")
         for v in variants:
-            variant = v.find('configItem/name').text
+            variant = v.find("configItem/name").text
 
-            combos.append({'l': layout, 'v': variant})
+            combos.append({"l": layout, "v": variant})
             for option in options:
-                combos.append({'l': layout, 'v': variant, 'o': option})
+                combos.append({"l": layout, "v": variant, "o": option})
 
     return combos
 
 
-def run(combos, tool, njobs):
+def run(combos, tool, njobs, keymap_output_dir):
+    if keymap_output_dir:
+        keymap_output_dir = Path(keymap_output_dir)
+        try:
+            keymap_output_dir.mkdir()
+        except FileExistsError as e:
+            print(e, file=sys.stderr)
+            return False
+
+    keymap_file = None
+    keymap_file_fd = None
+
     failed = False
-    with Pool(njobs) as p:
+    with multiprocessing.Pool(njobs) as p:
         results = p.imap_unordered(tool, combos)
-        for r in progress_bar(results, 'testing'):
-            success, output = r
-            if not success:
+        for invocation in progress_bar(results, total=len(combos), file=sys.stdout):
+            if invocation.exitstatus != 0:
                 failed = True
-            if output:
-                print(output, file=sys.stdout if success else sys.stderr)
+                target = sys.stderr
+            else:
+                target = sys.stdout if verbose else None
+
+            if target:
+                print(invocation, file=target)
+
+            if keymap_output_dir:
+                # we're running through the layouts in a somewhat sorted manner,
+                # so let's keep the fd open until we switch layouts
+                layout = invocation.layout
+                if invocation.variant:
+                    layout += f"({invocation.variant})"
+                fname = keymap_output_dir / layout
+                if fname != keymap_file:
+                    keymap_file = fname
+                    if keymap_file_fd:
+                        keymap_file_fd.close()
+                    keymap_file_fd = open(keymap_file, "a")
+
+                rmlvo = ", ".join([x or "" for x in invocation.rmlvo])
+                print(f"// {rmlvo}", file=keymap_file_fd)
+                print(invocation.keymap, file=keymap_file_fd)
+                keymap_file_fd.flush()
 
     return failed
 
 
 def main(args):
+    global progress_bar
+    global verbose
+
     tools = {
-        'libxkbcommon': xkbcommontool,
-        'xkbcomp': xkbcomp,
+        "libxkbcommon": xkbcommontool,
+        "xkbcomp": xkbcomp,
     }
 
     parser = argparse.ArgumentParser(
-        description='Tool to test all layout/variant/option combinations.'
+        description="""
+                    This tool compiles a keymap for each layout, variant and
+                    options combination in the given rules XML file. The output
+                    of this tool is YAML, use your favorite YAML parser to
+                    extract error messages. Errors are printed to stderr.
+                    """
     )
-    parser.add_argument('path', metavar='/path/to/evdev.xml',
-                        nargs='?', type=str,
-                        default=DEFAULT_RULES_XML,
-                        help='Path to xkeyboard-config\'s evdev.xml')
-    parser.add_argument('--tool', choices=tools.keys(),
-                        type=str, default='libxkbcommon',
-                        help='parsing tool to use')
-    parser.add_argument('--jobs', '-j', type=int,
-                        default=os.cpu_count() * 4,
-                        help='number of processes to use')
+    parser.add_argument(
+        "path",
+        metavar="/path/to/evdev.xml",
+        nargs="?",
+        type=str,
+        default=DEFAULT_RULES_XML,
+        help="Path to xkeyboard-config's evdev.xml",
+    )
+    parser.add_argument(
+        "--tool",
+        choices=tools.keys(),
+        type=str,
+        default="libxkbcommon",
+        help="parsing tool to use",
+    )
+    parser.add_argument(
+        "--jobs",
+        "-j",
+        type=int,
+        default=os.cpu_count() * 4,
+        help="number of processes to use",
+    )
+    parser.add_argument("--verbose", "-v", default=False, action="store_true")
+    parser.add_argument(
+        "--keymap-output-dir",
+        default=None,
+        type=str,
+        help="Directory to print compiled keymaps to",
+    )
+    parser.add_argument(
+        "--layout", default=None, type=str, help="Only test the given layout"
+    )
+    parser.add_argument(
+        "--variant", default=None, type=str, help="Only test the given variant"
+    )
+    parser.add_argument(
+        "--option", default=None, type=str, help="Only test the given option"
+    )
+
     args = parser.parse_args()
 
+    verbose = args.verbose
+    keymapdir = args.keymap_output_dir
+    progress_bar = create_progress_bar(verbose)
+
     tool = tools[args.tool]
 
-    combos = parse(args.path)
-    failed = run(combos, tool, args.jobs)
+    if any([args.layout, args.variant, args.option]):
+        combos = [
+            {
+                "l": args.layout,
+                "v": args.variant,
+                "o": args.option,
+            }
+        ]
+    else:
+        combos = parse(args.path)
+    failed = run(combos, tool, args.jobs, keymapdir)
     sys.exit(failed)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     try:
         main(sys.argv)
     except KeyboardInterrupt:
-        print('Exiting after Ctrl+C')
+        print("# Exiting after Ctrl+C")