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 34top_builddir = os.environ['top_builddir'] 35top_srcdir = os.environ['top_srcdir'] 36 37logging.basicConfig(level=logging.DEBUG) 38logger = logging.getLogger('test') 39logger.setLevel(logging.DEBUG) 40 41# Permutation of RMLVO that we use in multiple tests 42rmlvos = [list(x) for x in itertools.permutations( 43 ['--rules=evdev', '--model=pc104', 44 '--layout=ch', '--options=eurosign:5'] 45)] 46 47 48def _disable_coredump(): 49 resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) 50 51 52def run_command(args): 53 logger.debug('run command: {}'.format(' '.join(args))) 54 55 try: 56 p = subprocess.run(args, preexec_fn=_disable_coredump, 57 capture_output=True, text=True, 58 timeout=0.7) 59 return p.returncode, p.stdout, p.stderr 60 except subprocess.TimeoutExpired as e: 61 return 0, e.stdout, e.stderr 62 63 64class XkbcliTool: 65 xkbcli_tool = 'xkbcli' 66 subtool = None 67 68 def __init__(self, subtool=None, skipIf=()): 69 self.tool_path = top_builddir 70 self.subtool = subtool 71 self.skipIf = skipIf 72 73 def run_command(self, args): 74 for condition, reason in self.skipIf: 75 if condition: 76 raise unittest.SkipTest(reason) 77 if self.subtool is not None: 78 tool = '{}-{}'.format(self.xkbcli_tool, self.subtool) 79 else: 80 tool = self.xkbcli_tool 81 args = [os.path.join(self.tool_path, tool)] + args 82 83 return run_command(args) 84 85 def run_command_success(self, args): 86 rc, stdout, stderr = self.run_command(args) 87 assert rc == 0, (stdout, stderr) 88 return stdout, stderr 89 90 def run_command_invalid(self, args): 91 rc, stdout, stderr = self.run_command(args) 92 assert rc == 2, (rc, stdout, stderr) 93 return rc, stdout, stderr 94 95 def run_command_unrecognized_option(self, args): 96 rc, stdout, stderr = self.run_command(args) 97 assert rc == 2, (rc, stdout, stderr) 98 assert stdout.startswith('Usage') or stdout == '' 99 assert 'unrecognized option' in stderr 100 101 def run_command_missing_arg(self, args): 102 rc, stdout, stderr = self.run_command(args) 103 assert rc == 2, (rc, stdout, stderr) 104 assert stdout.startswith('Usage') or stdout == '' 105 assert 'requires an argument' in stderr 106 107 def __str__(self): 108 return str(self.subtool) 109 110 111class TestXkbcli(unittest.TestCase): 112 @classmethod 113 def setUpClass(cls): 114 cls.xkbcli = XkbcliTool() 115 cls.xkbcli_list = XkbcliTool('list', skipIf=( 116 (not int(os.getenv('HAVE_XKBCLI_LIST', '1')), 'xkbregistory not enabled'), 117 )) 118 cls.xkbcli_how_to_type = XkbcliTool('how-to-type') 119 cls.xkbcli_compile_keymap = XkbcliTool('compile-keymap') 120 cls.xkbcli_interactive_evdev = XkbcliTool('interactive-evdev', skipIf=( 121 (not int(os.getenv('HAVE_XKBCLI_INTERACTIVE_EVDEV', '1')), 'evdev not enabled'), 122 (not os.path.exists('/dev/input/event0'), 'event node required'), 123 (not os.access('/dev/input/event0', os.R_OK), 'insufficient permissions'), 124 )) 125 cls.xkbcli_interactive_x11 = XkbcliTool('interactive-x11', skipIf=( 126 (not int(os.getenv('HAVE_XKBCLI_INTERACTIVE_X11', '1')), 'x11 not enabled'), 127 (not os.getenv('DISPLAY'), 'DISPLAY not set'), 128 )) 129 cls.xkbcli_interactive_wayland = XkbcliTool('interactive-wayland', skipIf=( 130 (not int(os.getenv('HAVE_XKBCLI_INTERACTIVE_WAYLAND', '1')), 'wayland not enabled'), 131 (not os.getenv('WAYLAND_DISPLAY'), 'WAYLAND_DISPLAY not set'), 132 )) 133 cls.all_tools = [ 134 cls.xkbcli, 135 cls.xkbcli_list, 136 cls.xkbcli_how_to_type, 137 cls.xkbcli_compile_keymap, 138 cls.xkbcli_interactive_evdev, 139 cls.xkbcli_interactive_x11, 140 cls.xkbcli_interactive_wayland, 141 ] 142 143 def test_help(self): 144 # --help is supported by all tools 145 for tool in self.all_tools: 146 with self.subTest(tool=tool): 147 stdout, stderr = tool.run_command_success(['--help']) 148 assert stdout.startswith('Usage:') 149 assert stderr == '' 150 151 def test_invalid_option(self): 152 # --foobar generates "Usage:" for all tools 153 for tool in self.all_tools: 154 with self.subTest(tool=tool): 155 tool.run_command_unrecognized_option(['--foobar']) 156 157 def test_xkbcli_version(self): 158 # xkbcli --version 159 stdout, stderr = self.xkbcli.run_command_success(['--version']) 160 assert stdout.startswith('1') 161 assert stderr == '' 162 163 def test_xkbcli_too_many_args(self): 164 self.xkbcli.run_command_invalid(['a'] * 64) 165 166 def test_compile_keymap_args(self): 167 for args in ( 168 ['--verbose'], 169 ['--rmlvo'], 170 # ['--kccgst'], 171 ['--verbose', '--rmlvo'], 172 # ['--verbose', '--kccgst'], 173 ): 174 with self.subTest(args=args): 175 self.xkbcli_compile_keymap.run_command_success(args) 176 177 def test_compile_keymap_rmlvo(self): 178 for rmlvo in rmlvos: 179 with self.subTest(rmlvo=rmlvo): 180 self.xkbcli_compile_keymap.run_command_success(rmlvo) 181 182 def test_compile_keymap_include(self): 183 for args in ( 184 ['--include', '.', '--include-defaults'], 185 ['--include', '/tmp', '--include-defaults'], 186 ): 187 with self.subTest(args=args): 188 # Succeeds thanks to include-defaults 189 self.xkbcli_compile_keymap.run_command_success(args) 190 191 def test_compile_keymap_include_invalid(self): 192 # A non-directory is rejected by default 193 args = ['--include', '/proc/version'] 194 rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) 195 assert rc == 1, (stdout, stderr) 196 assert "There are no include paths to search" in stderr 197 198 # A non-existing directory is rejected by default 199 args = ['--include', '/tmp/does/not/exist'] 200 rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) 201 assert rc == 1, (stdout, stderr) 202 assert "There are no include paths to search" in stderr 203 204 # Valid dir, but missing files 205 args = ['--include', '/tmp'] 206 rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args) 207 assert rc == 1, (stdout, stderr) 208 assert "Couldn't look up rules" in stderr 209 210 def test_how_to_type(self): 211 # Unicode codepoint conversions, we support whatever strtol does 212 for args in (['123'], ['0x123'], ['0123']): 213 with self.subTest(args=args): 214 self.xkbcli_how_to_type.run_command_success(args) 215 216 def test_how_to_type_rmlvo(self): 217 for rmlvo in rmlvos: 218 with self.subTest(rmlvo=rmlvo): 219 args = rmlvo + ['0x1234'] 220 self.xkbcli_how_to_type.run_command_success(args) 221 222 def test_list_rmlvo(self): 223 for args in ( 224 ['--verbose'], 225 ['-v'], 226 ['--verbose', '--load-exotic'], 227 ['--load-exotic'], 228 ['--ruleset=evdev'], 229 ['--ruleset=base'], 230 ): 231 with self.subTest(args=args): 232 self.xkbcli_list.run_command_success(args) 233 234 def test_list_rmlvo_includes(self): 235 args = ['/tmp/'] 236 self.xkbcli_list.run_command_success(args) 237 238 def test_list_rmlvo_includes_invalid(self): 239 args = ['/proc/version'] 240 rc, stdout, stderr = self.xkbcli_list.run_command(args) 241 assert rc == 1 242 assert "Failed to append include path" in stderr 243 244 def test_list_rmlvo_includes_no_defaults(self): 245 args = ['--skip-default-paths', '/tmp'] 246 rc, stdout, stderr = self.xkbcli_list.run_command(args) 247 assert rc == 1 248 assert "Failed to parse XKB description" in stderr 249 250 def test_interactive_evdev_rmlvo(self): 251 for rmlvo in rmlvos: 252 with self.subTest(rmlvo=rmlvo): 253 self.xkbcli_interactive_evdev.run_command_success(rmlvos) 254 255 def test_interactive_evdev(self): 256 # Note: --enable-compose fails if $prefix doesn't have the compose tables 257 # installed 258 for args in ( 259 ['--report-state-changes'], 260 ['--enable-compose'], 261 ['--consumed-mode=xkb'], 262 ['--consumed-mode=gtk'], 263 ['--without-x11-offset'], 264 ): 265 with self.subTest(args=args): 266 self.xkbcli_interactive_evdev.run_command_success(args) 267 268 def test_interactive_x11(self): 269 # To be filled in if we handle something other than --help 270 pass 271 272 def test_interactive_wayland(self): 273 # To be filled in if we handle something other than --help 274 pass 275 276 277if __name__ == '__main__': 278 with tempfile.TemporaryDirectory() as tmpdir: 279 # Use our own test xkeyboard-config copy. 280 os.environ['XKB_CONFIG_ROOT'] = top_srcdir + '/test/data' 281 # libxkbcommon has fallbacks when XDG_CONFIG_HOME isn't set so we need 282 # to override it with a known (empty) directory. Otherwise our test 283 # behavior depends on the system the test is run on. 284 os.environ['XDG_CONFIG_HOME'] = tmpdir 285 # Prevent the legacy $HOME/.xkb from kicking in. 286 del os.environ['HOME'] 287 # This needs to be separated if we do specific extra path testing 288 os.environ['XKB_CONFIG_EXTRA_PATH'] = tmpdir 289 290 unittest.main() 291