Keysyms: Update using latest xorgproto
[platform/upstream/libxkbcommon.git] / test / xkeyboard-config-test.py.in
index 001f1b6..77cff1f 100755 (executable)
@@ -1,14 +1,14 @@
 #!/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'
 
@@ -17,29 +17,105 @@ EXTRA_PATH = '@MESON_BUILD_ROOT@'
 os.environ['PATH'] = ':'.join([EXTRA_PATH, os.getenv('PATH')])
 
 
-def noop_progress_bar(x, total):
-    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', '-', '-']
 
-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)
+        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
+
+
+class XkbcommonInvocation(Invocation):
+    UNRECOGNIZED_KEYSYM_ERROR = "XKB-107"
+
+    def run(self):
+        r, m, l, v, o = self.rmlvo
         args = [
             'xkbcli-compile-keymap',  # this is run in the builddir
             '--verbose',
@@ -52,28 +128,33 @@ def xkbcommontool(rmlvo):
         if o is not None:
             args += ['--options', o]
 
-        success = True
-        out = io.StringIO()
-        if verbose:
-            print(':: {}'.format(' '.join(args)), file=out)
-
+        self.command = " ".join(args)
         try:
             output = subprocess.check_output(args, stderr=subprocess.STDOUT,
                                              universal_newlines=True)
-            if verbose:
-                print(output, file=out)
-
-            if "unrecognized keysym" in output:
+            if self.UNRECOGNIZED_KEYSYM_ERROR in output:
                 for line in output.split('\n'):
-                    if "unrecognized keysym" in line:
-                        print('ERROR: {}'.format(line))
-                success = False
+                    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, 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
 
@@ -85,51 +166,9 @@ def xkbcomp(rmlvo):
         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 v is not None:
-            args.append('-variant')
-            args.append('{}'.format(v))
-        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,
-                                       universal_newlines=True)
-            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, file=out)
-                print(stderr, file=out)
-
-        # This catches setxkbmap errors.
-        except subprocess.CalledProcessError as err:
-            print('ERROR: Failed to compile: {}'.format(' '.join(args)), file=out)
-            print(err.output, file=out)
-            success = False
-
-        return success, out.getvalue()
+        tool = XkbCompInvocation(r, m, l, v, o)
+        tool.run()
+        return tool
     except KeyboardInterrupt:
         pass
 
@@ -159,26 +198,68 @@ def parse(path):
     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 success, output in progress_bar(results, total=len(combos)):
-            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,
     }
 
     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,
@@ -190,12 +271,33 @@ def main(args):
     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)
 
 
@@ -203,4 +305,4 @@ if __name__ == '__main__':
     try:
         main(sys.argv)
     except KeyboardInterrupt:
-        print('Exiting after Ctrl+C')
+        print('Exiting after Ctrl+C')