1#!/usr/bin/env python3 2# 3# Copyright © 2020 Red Hat, Inc. 4# 5# Permission is hereby granted, free of charge, to any person obtaining a 6# copy of this software and associated documentation files (the "Software"), 7# to deal in the Software without restriction, including without limitation 8# the rights to use, copy, modify, merge, publish, distribute, sublicense, 9# and/or sell copies of the Software, and to permit persons to whom the 10# Software is furnished to do so, subject to the following conditions: 11# 12# The above copyright notice and this permission notice (including the next 13# paragraph) shall be included in all copies or substantial portions of the 14# Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22# DEALINGS IN THE SOFTWARE. 23 24import itertools 25import os 26import resource 27import sys 28import subprocess 29import logging 30import tempfile 31import unittest 32 33 34try: 35 top_builddir = os.environ['top_builddir'] 36 top_srcdir = os.environ['top_srcdir'] 37except KeyError: 38 print('Required environment variables not found: top_srcdir/top_builddir', file=sys.stderr) 39 from pathlib import Path 40 top_srcdir = '.' 41 try: 42 top_builddir = next(Path('.').glob('**/meson-logs/')).parent 43 except StopIteration: 44 sys.exit(1) 45 print('Using srcdir "{}", builddir "{}"'.format(top_srcdir, top_builddir), file=sys.stderr) 46 47 48logging.basicConfig(level=logging.DEBUG) 49logger = logging.getLogger('test') 50logger.setLevel(logging.DEBUG) 51 52# Permutation of RMLVO that we use in multiple tests 53rmlvos = [list(x) for x in itertools.permutations( 54 ['--rules=evdev', '--model=pc104', 55 '--layout=ch', '--options=eurosign:5'] 56)] 57 58 59def _disable_coredump(): 60 resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) 61 62 63def run_command(args): 64 logger.debug('run command: {}'.format(' '.join(args))) 65 66 try: 67 p = subprocess.run(args, preexec_fn=_disable_coredump, 68 capture_output=True, text=True, 69 timeout=0.7) 70 return p.returncode, p.stdout, p.stderr 71 except subprocess.TimeoutExpired as e: 72 return 0, e.stdout, e.stderr 73 74 75class XkbcliTool: 76 xkbcli_tool = 'xkbcli' 77 subtool = None 78 79 def __init__(self, subtool=None, skipIf=(), skipError=()): 80 self.tool_path = top_builddir 81 self.subtool = subtool 82 self.skipIf = skipIf 83 self.skipError = skipError 84 85 def run_command(self, args): 86 for condition, reason in self.skipIf: 87 if condition: 88 raise unittest.SkipTest(reason) 89 if self.subtool is not None: 90 tool = '{}-{}'.format(self.xkbcli_tool, self.subtool) 91 else: 92 tool = self.xkbcli_tool 93 args = [os.path.join(self.tool_path, tool)] + args 94 95 return run_command(args) 96 97 def run_command_success(self, args): 98 rc, stdout, stderr = self.run_command(args) 99 if rc != 0: 100 for testfunc, reason in self.skipError: 101 if testfunc(rc, stdout, stderr): 102 raise unittest.SkipTest(reason) 103 assert rc == 0, (rc, stdout, stderr) 104 return stdout, stderr 105 106 def run_command_invalid(self, args): 107 rc, stdout, stderr = self.run_command(args) 108 assert rc == 2, (rc, stdout, stderr) 109 return rc, stdout, stderr 110 111 def run_command_unrecognized_option(self, args): 112 rc, stdout, stderr = self.run_command(args) 113 assert rc == 2, (rc, stdout, stderr) 114 assert stdout.startswith('Usage') or stdout == '' 115 assert 'unrecognized option' in stderr 116 117 def run_command_missing_arg(self, args): 118 rc, stdout, stderr = self.run_command(args) 119 assert rc == 2, (rc, stdout, stderr) 120 assert stdout.startswith('Usage') or stdout == '' 121 assert 'requires an argument' in stderr 122 123 def __str__(self): 124 return str(self.subtool) 125 126 127class TestXkbcli(unittest.TestCase): 128 @classmethod 129 def setUpClass(cls): 130 cls.xkbcli = XkbcliTool() 131 cls.xkbcli_list = XkbcliTool('list', skipIf=( 132 (not int(os.getenv('HAVE_XKBCLI_LIST', '1')), 'xkbregistory not enabled'), 133 )) 134 cls.xkbcli_how_to_type = XkbcliTool('how-to-type') 135 cls.xkbcli_compile_keymap = XkbcliTool('compile-keymap') 136 cls.xkbcli_interactive_evdev = XkbcliTool('interactive-evdev', skipIf=( 137 (not int(os.getenv('HAVE_XKBCLI_INTERACTIVE_EVDEV', '1')), 'evdev not enabled'), 138 (not os.path.exists('/dev/input/event0'), 'event node required'), 139 (not os.access('/dev/input/event0', os.R_OK), 'insufficient permissions'), 140 ), skipError=( 141 (lambda rc, stdout, stderr: 'Couldn\'t find any keyboards' in stderr, 142 'No keyboards available'), 143 ), 144 ) 145 cls.xkbcli_interactive_x11 = XkbcliTool('interactive-x11', skipIf=( 146 (not int(os.getenv('HAVE_XKBCLI_INTERACTIVE_X11', '1')), 'x11 not enabled'), 147 (not os.getenv('DISPLAY'), 'DISPLAY not set'), 148 )) 149 cls.xkbcli_interactive_wayland = XkbcliTool('interactive-wayland', skipIf=( 150 (not int(os.getenv('HAVE_XKBCLI_INTERACTIVE_WAYLAND', '1')), 'wayland not enabled'), 151 (not os.getenv('WAYLAND_DISPLAY'), 'WAYLAND_DISPLAY not set'), 152 )) 153 cls.all_tools = [ 154 cls.xkbcli, 155 cls.xkbcli_list, 156 cls.xkbcli_how_to_type, 157 cls.xkbcli_compile_keymap, 158 cls.xkbcli_interactive_evdev, 159 cls.xkbcli_interactive_x11, 160 cls.xkbcli_interactive_wayland, 161 ] 162 163 def test_help(self): 164 # --help is supported by all tools 165 for tool in self.all_tools: 166 with self.subTest(tool=tool): 167 stdout, stderr = tool.run_command_success(['--help']) 168 assert stdout.startswith('Usage:') 169 assert stderr == '' 170 171 def test_invalid_option(self): 172 # --foobar generates "Usage:" for all tools 173 for tool in self.all_tools: 174 with self.subTest(tool=tool): 175 tool.run_command_unrecognized_option(['--foobar']) 176 177 def test_xkbcli_version(self): 178 # xkbcli --version 179 stdout, stderr = self.xkbcli.run_command_success(['--version']) 180 assert stdout.startswith('1') 181 assert stderr == '' 182 183 def test_xkbcli_too_many_args(self): 184 self.xkbcli.run_command_invalid(['a'] * 64) 185 186 def test_compile_keymap_args(self): 187 for args in ( 188 ['--verbose'], 189 ['--rmlvo'], 190 # ['--kccgst'], 191 ['--verbose', '--rmlvo'], 192 # ['--verbose', '--kccgst'], 193 ): 194 with self.subTest(args=args): 195 self.xkbcli_compile_keymap.run_command_success(args) 196 197 def test_compile_keymap_rmlvo(self): 198 for rmlvo in rmlvos: 199 with self.subTest(rmlvo=rmlvo): 200 self.xkbcli_compile_keymap.run_command_success(rmlvo) 201 202 def test_compile_keymap_include(self): 203 for args in ( 204 ['--include', '.', '--include-defaults'], 205 ['--include', '/tmp', '--include-defaults'], 206 ): 207 with self.subTest(args=args): 208 # Succeeds thanks to include-defaults 209 self.xkbcli_compile_keymap.run_command_success(args) 210 211 def test_compile_keymap_include_invalid(self): 212 # A non-directory is rejected by default 213 args = ['--include', '/proc/version'] 214 rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) 215 assert rc == 1, (stdout, stderr) 216 assert "There are no include paths to search" in stderr 217 218 # A non-existing directory is rejected by default 219 args = ['--include', '/tmp/does/not/exist'] 220 rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) 221 assert rc == 1, (stdout, stderr) 222 assert "There are no include paths to search" in stderr 223 224 # Valid dir, but missing files 225 args = ['--include', '/tmp'] 226 rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) 227 assert rc == 1, (stdout, stderr) 228 assert "Couldn't look up rules" in stderr 229 230 def test_how_to_type(self): 231 # Unicode codepoint conversions, we support whatever strtol does 232 for args in (['123'], ['0x123'], ['0123']): 233 with self.subTest(args=args): 234 self.xkbcli_how_to_type.run_command_success(args) 235 236 def test_how_to_type_rmlvo(self): 237 for rmlvo in rmlvos: 238 with self.subTest(rmlvo=rmlvo): 239 args = rmlvo + ['0x1234'] 240 self.xkbcli_how_to_type.run_command_success(args) 241 242 def test_list_rmlvo(self): 243 for args in ( 244 ['--verbose'], 245 ['-v'], 246 ['--verbose', '--load-exotic'], 247 ['--load-exotic'], 248 ['--ruleset=evdev'], 249 ['--ruleset=base'], 250 ): 251 with self.subTest(args=args): 252 self.xkbcli_list.run_command_success(args) 253 254 def test_list_rmlvo_includes(self): 255 args = ['/tmp/'] 256 self.xkbcli_list.run_command_success(args) 257 258 def test_list_rmlvo_includes_invalid(self): 259 args = ['/proc/version'] 260 rc, stdout, stderr = self.xkbcli_list.run_command(args) 261 assert rc == 1 262 assert "Failed to append include path" in stderr 263 264 def test_list_rmlvo_includes_no_defaults(self): 265 args = ['--skip-default-paths', '/tmp'] 266 rc, stdout, stderr = self.xkbcli_list.run_command(args) 267 assert rc == 1 268 assert "Failed to parse XKB description" in stderr 269 270 def test_interactive_evdev_rmlvo(self): 271 for rmlvo in rmlvos: 272 with self.subTest(rmlvo=rmlvo): 273 self.xkbcli_interactive_evdev.run_command_success(rmlvo) 274 275 def test_interactive_evdev(self): 276 # Note: --enable-compose fails if $prefix doesn't have the compose tables 277 # installed 278 for args in ( 279 ['--report-state-changes'], 280 ['--enable-compose'], 281 ['--consumed-mode=xkb'], 282 ['--consumed-mode=gtk'], 283 ['--without-x11-offset'], 284 ): 285 with self.subTest(args=args): 286 self.xkbcli_interactive_evdev.run_command_success(args) 287 288 def test_interactive_x11(self): 289 # To be filled in if we handle something other than --help 290 pass 291 292 def test_interactive_wayland(self): 293 # To be filled in if we handle something other than --help 294 pass 295 296 297if __name__ == '__main__': 298 with tempfile.TemporaryDirectory() as tmpdir: 299 # Use our own test xkeyboard-config copy. 300 os.environ['XKB_CONFIG_ROOT'] = top_srcdir + '/test/data' 301 # Use our own X11 locale copy. 302 os.environ['XLOCALEDIR'] = top_srcdir + '/test/data/locale' 303 # Use our own locale. 304 os.environ['LC_CTYPE'] = 'en_US.UTF-8' 305 # libxkbcommon has fallbacks when XDG_CONFIG_HOME isn't set so we need 306 # to override it with a known (empty) directory. Otherwise our test 307 # behavior depends on the system the test is run on. 308 os.environ['XDG_CONFIG_HOME'] = tmpdir 309 # Prevent the legacy $HOME/.xkb from kicking in. 310 del os.environ['HOME'] 311 # This needs to be separated if we do specific extra path testing 312 os.environ['XKB_CONFIG_EXTRA_PATH'] = tmpdir 313 314 unittest.main() 315