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