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