libxkbcommon/test/xkeyboard-config-test.py.in

302 lines
9.2 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import multiprocessing
import sys
import subprocess
import os
import xml.etree.ElementTree as ET
from pathlib import Path
verbose = False
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')])
def escape(s):
return s.replace('"', '\\"')
# The function generating the progress bar (if any).
def create_progress_bar(verbose):
def noop_progress_bar(x, total):
return x
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
class XkbcommonInvocation(Invocation):
def run(self):
r, m, l, v, o = self.rmlvo
args = [
'xkbcli-compile-keymap', # this is run in the builddir
'--verbose',
'--rules', r,
'--model', m,
'--layout', l,
]
if v is not None:
args += ['--variant', v]
if o is not None:
args += ['--options', o]
self.command = " ".join(args)
try:
output = subprocess.check_output(args, stderr=subprocess.STDOUT,
universal_newlines=True)
if "unrecognized keysym" in output:
for line in output.split('\n'):
if "unrecognized keysym" 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:
self.error = "failed to compile keymap"
self.exitstatus = err.returncode
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)
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')
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})
variants = l.findall('variantList/variant')
for v in variants:
variant = v.find('configItem/name').text
combos.append({'l': layout, 'v': variant})
for option in options:
combos.append({'l': layout, 'v': variant, 'o': option})
return combos
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 multiprocessing.Pool(njobs) as p:
results = p.imap_unordered(tool, combos)
for invocation in progress_bar(results, total=len(combos)):
if invocation.exitstatus != 0:
failed = True
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.'
)
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]
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__':
try:
main(sys.argv)
except KeyboardInterrupt:
print('# Exiting after Ctrl+C')