test: allow skipping errors in certain conditions
[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=(), 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, (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
127 class 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
297 if __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         # libxkbcommon has fallbacks when XDG_CONFIG_HOME isn't set so we need
302         # to override it with a known (empty) directory. Otherwise our test
303         # behavior depends on the system the test is run on.
304         os.environ['XDG_CONFIG_HOME'] = tmpdir
305         # Prevent the legacy $HOME/.xkb from kicking in.
306         del os.environ['HOME']
307         # This needs to be separated if we do specific extra path testing
308         os.environ['XKB_CONFIG_EXTRA_PATH'] = tmpdir
309
310         unittest.main()