tool-option-parsing.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright © 2020 Red Hat, Inc.
  4. # SPDX-License-Identifier: MIT
  5. import concurrent.futures
  6. import itertools
  7. import logging
  8. import os
  9. import re
  10. import resource
  11. import subprocess
  12. import sys
  13. import tempfile
  14. import unittest
  15. from abc import ABCMeta, abstractmethod
  16. from collections.abc import Callable, Generator
  17. from dataclasses import dataclass
  18. from functools import reduce
  19. from pathlib import Path
  20. from typing import ClassVar
  21. try:
  22. top_builddir = Path(os.environ["top_builddir"])
  23. top_srcdir = Path(os.environ["top_srcdir"])
  24. except KeyError:
  25. print(
  26. "Required environment variables not found: top_srcdir/top_builddir",
  27. file=sys.stderr,
  28. )
  29. top_srcdir = Path(".")
  30. try:
  31. top_builddir = next(Path(".").glob("**/meson-logs/")).parent
  32. except StopIteration:
  33. sys.exit(1)
  34. print(
  35. 'Using srcdir "{}", builddir "{}"'.format(top_srcdir, top_builddir),
  36. file=sys.stderr,
  37. )
  38. TIMEOUT = 10.0 # seconds
  39. INVALID_OPTION_ERROR_RE = re.compile(
  40. r"(unrecognized|unknown|illegal|invalid) option|usage|try --help",
  41. re.IGNORECASE,
  42. )
  43. # Unset some environment variables, so that testing --enable-environment-names
  44. # does not actually depend on the current environment.
  45. for key in (
  46. "XKB_DEFAULT_RULES",
  47. "XKB_DEFAULT_MODEL",
  48. "XKB_DEFAULT_LAYOUT",
  49. "XKB_DEFAULT_VARIANT",
  50. "XKB_DEFAULT_OPTIONS",
  51. ):
  52. if key in os.environ:
  53. del os.environ[key]
  54. # Ensure locale is C, so we can check error messages in English
  55. os.environ["LC_ALL"] = "C.UTF-8"
  56. logging.basicConfig(level=logging.DEBUG)
  57. logger = logging.getLogger("test")
  58. logger.setLevel(logging.DEBUG)
  59. def powerset(iterable):
  60. "Subsequences of the iterable from shortest to longest."
  61. # powerset([1,2,3]) → () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)
  62. s = tuple(iterable)
  63. return itertools.chain.from_iterable(
  64. itertools.combinations(s, r) for r in range(len(s) + 1)
  65. )
  66. # Permutation of RMLVO that we use in multiple tests
  67. rmlvo_options = (
  68. "--rules=evdev",
  69. "--model=pc104",
  70. "--layout=ch",
  71. "--options=eurosign:5",
  72. "--enable-environment-names",
  73. )
  74. rmlvos = tuple(
  75. map(
  76. list,
  77. itertools.chain(
  78. itertools.permutations(rmlvo_options[:-1]),
  79. powerset(rmlvo_options),
  80. ),
  81. )
  82. )
  83. @dataclass
  84. class RMLVO:
  85. rules: str | None = None
  86. model: str | None = None
  87. layout: str | None = None
  88. variant: str | None = None
  89. options: str | None = None
  90. env: bool = False
  91. def __iter__(self) -> Generator[tuple[str, ...]]:
  92. if self.rules is not None:
  93. yield "--rules", self.rules
  94. if self.model is not None:
  95. yield "--model", self.model
  96. if self.layout is not None:
  97. yield "--layout", self.layout
  98. if self.variant is not None:
  99. yield "--variant", self.variant
  100. if self.options is not None:
  101. yield "--options", self.options
  102. if self.env:
  103. yield ("--enable-environment-names",)
  104. @property
  105. def args(self) -> list[str]:
  106. return list(itertools.chain.from_iterable(self))
  107. class Target(metaclass=ABCMeta):
  108. @property
  109. @abstractmethod
  110. def args(self) -> list[str]: ...
  111. @property
  112. def stdin(self) -> str | None:
  113. return None
  114. class RmlvoTarget(Target):
  115. @property
  116. def args(self) -> list[str]:
  117. return ["--rmlvo"]
  118. class KccgstTarget(Target):
  119. @property
  120. def args(self) -> list[str]:
  121. return ["--kccgst"]
  122. class KccgstYamlTarget(Target):
  123. @property
  124. def args(self) -> list[str]:
  125. return ["--kccgst-yaml"]
  126. @dataclass
  127. class KeymapTarget(Target, RMLVO):
  128. arg: bool = False
  129. path: Path | None = None
  130. stdin: str | None = None
  131. def _args(self) -> Generator[str]:
  132. yield from itertools.chain.from_iterable(self)
  133. if self.arg:
  134. yield "--keymap"
  135. if self.path:
  136. yield str(self.path)
  137. @property
  138. def args(self) -> list[str]:
  139. return list(self._args())
  140. @property
  141. def use_rmlvo(self) -> bool:
  142. return (
  143. not self.arg
  144. and not self.path
  145. and (
  146. bool(self.rules)
  147. or bool(self.model)
  148. or bool(self.layout)
  149. or bool(self.variant)
  150. or bool(self.options)
  151. or bool(self.env)
  152. or not self.stdin
  153. )
  154. )
  155. def _disable_coredump():
  156. resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
  157. def run_command(args, input: str | None = None) -> tuple[int, str, str]:
  158. logger.debug("run command: {}".format(" ".join(args)))
  159. try:
  160. p = subprocess.run(
  161. args,
  162. preexec_fn=_disable_coredump,
  163. capture_output=True,
  164. input=input,
  165. text=True,
  166. timeout=0.7,
  167. )
  168. return p.returncode, p.stdout, p.stderr
  169. except subprocess.TimeoutExpired as e:
  170. return (
  171. 0,
  172. e.stdout.decode("utf-8") if e.stdout else "",
  173. e.stderr.decode("utf-8") if e.stderr else "",
  174. )
  175. @dataclass
  176. class XkbcliTool:
  177. xkbcli_tool: ClassVar[str] = "xkbcli"
  178. tool_path: ClassVar[Path] = top_builddir / "tools"
  179. subtool: str | None = None
  180. skipIf: tuple[tuple[bool, str], ...] = ()
  181. skipError: tuple[tuple[Callable[[int, str, str], bool], str], ...] = ()
  182. def run_command(self, args, input: str | None = None) -> tuple[int, str, str]:
  183. for condition, reason in self.skipIf:
  184. if condition:
  185. raise unittest.SkipTest(reason)
  186. if self.subtool is not None:
  187. tool = "{}-{}".format(self.xkbcli_tool, self.subtool)
  188. else:
  189. tool = self.xkbcli_tool
  190. args = [os.path.join(self.tool_path, tool)] + args
  191. return run_command(args, input=input)
  192. def run_command_success(self, args, input: str | None = None) -> tuple[str, str]:
  193. rc, stdout, stderr = self.run_command(args, input=input)
  194. if rc != 0:
  195. for testfunc, reason in self.skipError:
  196. if testfunc(rc, stdout, stderr):
  197. raise unittest.SkipTest(reason)
  198. assert rc == 0, (rc, stdout, stderr)
  199. return stdout, stderr
  200. def run_command_invalid(
  201. self, args, input: str | None = None
  202. ) -> tuple[int, str, str]:
  203. rc, stdout, stderr = self.run_command(args, input=input)
  204. assert rc == 2, (rc, stdout, stderr)
  205. return rc, stdout, stderr
  206. def run_command_unrecognized_option(self, args):
  207. rc, stdout, stderr = self.run_command(args)
  208. assert rc == 2, (rc, stdout, stderr)
  209. assert stdout.startswith("Usage") or stdout == ""
  210. # getopt/argument parsing diagnostics vary across libc/platforms
  211. # (for example GNU/Linux vs. Solaris), even when the option is rejected.
  212. assert INVALID_OPTION_ERROR_RE.search(f"{stdout}\n{stderr}"), (
  213. rc,
  214. stdout,
  215. stderr,
  216. )
  217. def run_command_missing_arg(self, args):
  218. rc, stdout, stderr = self.run_command(args)
  219. assert rc == 2, (rc, stdout, stderr)
  220. assert stdout.startswith("Usage") or stdout == ""
  221. assert "requires an argument" in stderr
  222. def __str__(self):
  223. return str(self.subtool)
  224. class TestXkbcli(unittest.TestCase):
  225. xkbcli: ClassVar[XkbcliTool]
  226. xkbcli_list: ClassVar[XkbcliTool]
  227. xkbcli_how_to_type: ClassVar[XkbcliTool]
  228. xkbcli_compile_keymap: ClassVar[XkbcliTool]
  229. xkbcli_compile_compose: ClassVar[XkbcliTool]
  230. xkbcli_interactive_evdev: ClassVar[XkbcliTool]
  231. xkbcli_interactive_x11: ClassVar[XkbcliTool]
  232. xkbcli_interactive_wayland: ClassVar[XkbcliTool]
  233. xkbcli_interactive: ClassVar[XkbcliTool]
  234. xkbcli_dump_keymap_x11: ClassVar[XkbcliTool]
  235. xkbcli_dump_keymap_wayland: ClassVar[XkbcliTool]
  236. xkbcli_dump_keymap: ClassVar[XkbcliTool]
  237. all_tools: ClassVar[list[XkbcliTool]]
  238. @classmethod
  239. def setUpClass(cls):
  240. cls.xkbcli = XkbcliTool()
  241. cls.xkbcli_list = XkbcliTool(
  242. "list",
  243. skipIf=(
  244. (
  245. not int(os.getenv("HAVE_XKBCLI_LIST", "1")),
  246. "xkbregistory not enabled",
  247. ),
  248. ),
  249. )
  250. cls.xkbcli_how_to_type = XkbcliTool("how-to-type")
  251. cls.xkbcli_compile_keymap = XkbcliTool("compile-keymap")
  252. cls.xkbcli_compile_compose = XkbcliTool("compile-compose")
  253. no_interactive_evdev = (
  254. (
  255. not int(os.getenv("HAVE_XKBCLI_INTERACTIVE_EVDEV", "1")),
  256. "evdev not enabled",
  257. ),
  258. (not os.path.exists("/dev/input/event0"), "event node required"),
  259. (
  260. not os.access("/dev/input/event0", os.R_OK),
  261. "insufficient permissions",
  262. ),
  263. )
  264. cls.xkbcli_interactive_evdev = XkbcliTool(
  265. "interactive-evdev",
  266. skipIf=no_interactive_evdev,
  267. skipError=(
  268. (
  269. lambda rc, stdout, stderr: "Couldn't find any keyboards" in stderr,
  270. "No keyboards available",
  271. ),
  272. ),
  273. )
  274. no_interactive_x11 = (
  275. (
  276. not int(os.getenv("HAVE_XKBCLI_INTERACTIVE_X11", "1")),
  277. "x11 not enabled",
  278. ),
  279. (not os.getenv("DISPLAY"), "DISPLAY not set"),
  280. )
  281. cls.xkbcli_interactive_x11 = XkbcliTool(
  282. "interactive-x11",
  283. skipIf=no_interactive_x11,
  284. )
  285. no_interactive_wayland = (
  286. (
  287. not int(os.getenv("HAVE_XKBCLI_INTERACTIVE_WAYLAND", "1")),
  288. "wayland not enabled",
  289. ),
  290. (not os.getenv("WAYLAND_DISPLAY"), "WAYLAND_DISPLAY not set"),
  291. )
  292. cls.xkbcli_interactive_wayland = XkbcliTool(
  293. "interactive-wayland",
  294. skipIf=no_interactive_wayland,
  295. )
  296. # NOTE: `interactive` cannot be tested, because it hardcodes the paths
  297. # of the `xkbcli-interactive-{wayland,x11}` tools it calls to the
  298. # *install* directory, while we need to use those of the *build*
  299. # directory.
  300. cls.xkbcli_dump_keymap_x11 = XkbcliTool(
  301. "dump-keymap-x11",
  302. skipIf=no_interactive_x11,
  303. )
  304. cls.xkbcli_dump_keymap_wayland = XkbcliTool(
  305. "dump-keymap-wayland",
  306. skipIf=no_interactive_wayland,
  307. )
  308. # NOTE: `dump-keymap` cannot be tested, because it hardcodes the paths
  309. # of the `xkbcli-dump-keymap-{wayland,x11}` tools it calls to the
  310. # *install* directory, while we need to use those of the *build*
  311. # directory.
  312. cls.all_tools = [
  313. cls.xkbcli,
  314. cls.xkbcli_list,
  315. cls.xkbcli_how_to_type,
  316. cls.xkbcli_compile_keymap,
  317. cls.xkbcli_compile_compose,
  318. cls.xkbcli_interactive_evdev,
  319. cls.xkbcli_interactive_x11,
  320. cls.xkbcli_interactive_wayland,
  321. cls.xkbcli_dump_keymap_x11,
  322. cls.xkbcli_dump_keymap_wayland,
  323. ]
  324. def test_help(self):
  325. # --help is supported by all tools
  326. for tool in self.all_tools:
  327. with self.subTest(tool=tool):
  328. stdout, stderr = tool.run_command_success(["--help"])
  329. assert stdout.startswith("Usage:")
  330. assert stderr == ""
  331. def test_invalid_option(self):
  332. # --foobar generates "Usage:" for all tools
  333. for tool in self.all_tools:
  334. with self.subTest(tool=tool):
  335. tool.run_command_unrecognized_option(["--foobar"])
  336. def test_xkbcli_version(self):
  337. # xkbcli --version
  338. stdout, stderr = self.xkbcli.run_command_success(["--version"])
  339. assert stdout.startswith("1")
  340. assert stderr == ""
  341. def test_xkbcli_too_many_args(self):
  342. self.xkbcli.run_command_invalid(["a"] * 64)
  343. def test_compile_keymap(self):
  344. for args in (
  345. ["--verbose", "-h"],
  346. ["--format=v2", "-h"],
  347. ["--input-format=xkb_v1", "-h"],
  348. ["--output-format=xkb_v2", "-h"],
  349. ["--input-strict", "-h"],
  350. ["--output-strict", "-h"],
  351. ["--strict", "-h"],
  352. ["--no-pretty", "-h"],
  353. ["--drop-unused", "-h"],
  354. ["--explicit-defaults", "-h"],
  355. ["--explicit-vmods", "-h"],
  356. ["--explicit-keys", "-h"],
  357. ["--explicit-values", "-h"],
  358. ["--layouts-mask", "0x1", "-h"],
  359. ):
  360. with self.subTest(args=args):
  361. self.xkbcli_compile_keymap.run_command_success(args)
  362. def test_compile_keymap_args(self):
  363. xkb_root = Path(os.environ["XKB_CONFIG_ROOT"])
  364. keymap_path = xkb_root / "keymaps/masks.xkb"
  365. keymap_string = keymap_path.read_text(encoding="utf-8")
  366. test_option = "--test"
  367. target: Target
  368. for target in (
  369. RmlvoTarget(),
  370. KccgstTarget(),
  371. KccgstYamlTarget(),
  372. # Keymap from RMLVO
  373. KeymapTarget(),
  374. # Keymap from RMLVO (stdin ignored)
  375. KeymapTarget(layout="us", stdin=keymap_string),
  376. KeymapTarget(env=True, stdin=keymap_string),
  377. # Keymap parsed from keymap file
  378. KeymapTarget(arg=False, path=keymap_path),
  379. KeymapTarget(arg=True, path=keymap_path),
  380. # Keymap parsed from stdin
  381. KeymapTarget(arg=False, stdin=keymap_string),
  382. KeymapTarget(arg=True, stdin=keymap_string),
  383. ):
  384. for options in powerset(("--verbose", test_option)):
  385. args = list(options) + target.args
  386. with self.subTest(args=args):
  387. stdout, stderr = self.xkbcli_compile_keymap.run_command_success(
  388. args, input=target.stdin
  389. )
  390. if test_option in options:
  391. # Expect no output
  392. assert stdout == "", (target, stdout)
  393. if isinstance(target, KeymapTarget) and test_option not in options:
  394. # Check keymap output for excepted bits:
  395. # - <AB01> is not defined in mask.xkb
  396. # - virtual_modifiers Test01 is only defined in masks.xkb
  397. assert ("<AB01>" in stdout) ^ (not target.use_rmlvo), (
  398. target,
  399. stdout,
  400. stderr,
  401. )
  402. assert ("virtual_modifiers Test01" in stdout) ^ (
  403. target.use_rmlvo
  404. ), (target, stdout, stderr)
  405. def test_compile_keymap_mutually_exclusive_args(self):
  406. xkb_root = Path(os.environ["XKB_CONFIG_ROOT"])
  407. keymap_path = xkb_root / "keymaps/basic.xkb"
  408. keymap_string = keymap_path.read_text(encoding="utf-8")
  409. keymap_from_stdin = KeymapTarget(arg=True, stdin=keymap_string)
  410. keymap_from_path1 = KeymapTarget(arg=True, path=Path(keymap_path))
  411. keymap_from_path2 = KeymapTarget(arg=False, path=Path(keymap_path))
  412. rmlvo = RmlvoTarget()
  413. kccgst = KccgstTarget()
  414. kccgstYaml = KccgstYamlTarget()
  415. for entry in (
  416. # --keymap does not use RMLVO options
  417. ("--rules", "some-rules", keymap_from_stdin),
  418. ("--model", "some-model", keymap_from_stdin),
  419. ("--layout", "some-layout", keymap_from_stdin),
  420. ("--variant", "some-variant", keymap_from_stdin),
  421. ("--options", "some-option", keymap_from_stdin),
  422. ("--rules", "some-rules", keymap_from_path1),
  423. ("--model", "some-model", keymap_from_path1),
  424. ("--layout", "some-layout", keymap_from_path1),
  425. ("--variant", "some-variant", keymap_from_path1),
  426. ("--options", "some-option", keymap_from_path1),
  427. # Trailing keymap file with RMLVO options
  428. ("--rules", "some-rules", keymap_from_path2),
  429. ("--model", "some-model", keymap_from_path2),
  430. ("--layout", "some-layout", keymap_from_path2),
  431. ("--variant", "some-variant", keymap_from_path2),
  432. ("--options", "some-option", keymap_from_path2),
  433. # Incompatible output types
  434. (rmlvo, keymap_from_stdin),
  435. (rmlvo, keymap_from_path1),
  436. (rmlvo, keymap_from_path2),
  437. (kccgst, kccgstYaml),
  438. (kccgst, keymap_from_stdin),
  439. (kccgst, keymap_from_path1),
  440. (kccgst, keymap_from_path2),
  441. (kccgst, rmlvo),
  442. (kccgst, rmlvo, keymap_from_stdin),
  443. (kccgst, rmlvo, keymap_from_path1),
  444. (kccgst, rmlvo, keymap_from_path2),
  445. (kccgstYaml, keymap_from_stdin),
  446. (kccgstYaml, keymap_from_path1),
  447. (kccgstYaml, keymap_from_path2),
  448. (kccgstYaml, rmlvo),
  449. (kccgstYaml, rmlvo, keymap_from_stdin),
  450. (kccgstYaml, rmlvo, keymap_from_path1),
  451. (kccgstYaml, rmlvo, keymap_from_path2),
  452. ):
  453. with self.subTest(args=entry):
  454. args: list[str] = list(
  455. itertools.chain.from_iterable(
  456. arg.args if isinstance(arg, Target) else arg for arg in entry
  457. )
  458. )
  459. input: str | None = reduce(
  460. lambda acc, arg: (
  461. acc or (arg.stdin if isinstance(arg, Target) else None)
  462. ),
  463. args,
  464. None,
  465. )
  466. self.xkbcli_compile_keymap.run_command_invalid(args, input=input)
  467. def test_compile_keymap_rmlvo(self):
  468. def run(target, rmlvo):
  469. return self.xkbcli_compile_keymap.run_command_success(target + rmlvo)
  470. with concurrent.futures.ThreadPoolExecutor() as executor:
  471. futures = {
  472. executor.submit(run, target.args, rmlvo): (target, rmlvo)
  473. for target in (
  474. RmlvoTarget(),
  475. KccgstTarget(),
  476. KccgstYamlTarget(),
  477. KeymapTarget(),
  478. )
  479. for rmlvo in rmlvos
  480. }
  481. for future in concurrent.futures.as_completed(futures, TIMEOUT):
  482. target, rmlvo = futures[future]
  483. with self.subTest(target=target, rmlvo=rmlvo):
  484. future.result()
  485. def test_compile_keymap_include(self):
  486. for args in (
  487. ["--include", ".", "--include-defaults"],
  488. ["--include", "/tmp", "--include-defaults"],
  489. ):
  490. with self.subTest(args=args):
  491. # Succeeds thanks to include-defaults
  492. self.xkbcli_compile_keymap.run_command_success(args)
  493. def test_compile_keymap_include_invalid(self):
  494. # A non-directory is rejected by default
  495. args = ["--include", "/proc/version"]
  496. rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args)
  497. assert rc == 1, (stdout, stderr)
  498. assert "There are no include paths to search" in stderr
  499. # A non-existing directory is rejected by default
  500. args = ["--include", "/tmp/does/not/exist"]
  501. rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args)
  502. assert rc == 1, (stdout, stderr)
  503. assert "There are no include paths to search" in stderr
  504. # Valid dir, but missing files
  505. args = ["--include", "/tmp"]
  506. rc, stdout, stderr = self.xkbcli_compile_keymap.run_command(args)
  507. assert rc == 1, (stdout, stderr)
  508. assert "Couldn't look up rules" in stderr
  509. def test_compile_compose(self):
  510. for args in (["--verbose"],):
  511. with self.subTest(args=args):
  512. self.xkbcli_compile_compose.run_command_success(args)
  513. def test_how_to_type(self):
  514. for args in (["--verbose", "1"],):
  515. with self.subTest(args=args):
  516. self.xkbcli_how_to_type.run_command_success(args)
  517. @dataclass
  518. class Entry:
  519. args: list[str]
  520. name: str
  521. value: int
  522. for entry in (
  523. # Unicode codepoint conversions, we support whatever strtol does
  524. Entry(args=["123"], name="braceleft", value=0x007B),
  525. Entry(args=["0123"], name="braceleft", value=0x007B),
  526. Entry(args=["0a"], name="Linefeed", value=0xFF0A),
  527. Entry(args=["0x123"], name="gcedilla", value=0x03BB),
  528. Entry(args=["U+123"], name="gcedilla", value=0x03BB),
  529. # Characters
  530. Entry(args=["1"], name="1", value=0x0031),
  531. Entry(args=["a"], name="a", value=0x0061),
  532. Entry(args=["á"], name="aacute", value=0x00E1),
  533. # Keysyms names (fallback without --keysym option)
  534. Entry(args=["acute"], name="acute", value=0x00B4),
  535. Entry(args=["U123"], name="U0123", value=0x1000123),
  536. # Keysyms names (with --keysym)
  537. Entry(args=["--keysym", "1"], name="1", value=0x0031),
  538. Entry(args=["--keysym", "a"], name="a", value=0x0061),
  539. Entry(args=["--keysym", "acute"], name="acute", value=0x00B4),
  540. Entry(args=["--keysym", "U123"], name="U0123", value=0x1000123),
  541. # Keysym values
  542. Entry(args=["--keysym", "123"], name="braceleft", value=0x007B),
  543. Entry(args=["--keysym", "0x123"], name="0x00000123", value=0x0123),
  544. ):
  545. with self.subTest(args=args):
  546. stdout, _stderr = self.xkbcli_how_to_type.run_command_success(
  547. entry.args
  548. )
  549. expected = f"keysym: {entry.name} (0x{entry.value:04x})"
  550. lines = stdout.splitlines()
  551. assert len(lines) >= 1
  552. assert lines[0] == expected, (
  553. entry,
  554. f'expected: "{expected}", but got: "{lines[0]}"',
  555. )
  556. def test_how_to_type_rmlvo(self):
  557. def run(rmlvo):
  558. args = rmlvo + ["0x1234"]
  559. return self.xkbcli_how_to_type.run_command_success(args)
  560. with concurrent.futures.ThreadPoolExecutor() as executor:
  561. futures = {executor.submit(run, rmlvo): rmlvo for rmlvo in rmlvos}
  562. for future in concurrent.futures.as_completed(futures, TIMEOUT):
  563. rmlvo = futures[future]
  564. with self.subTest(rmlvo=rmlvo):
  565. future.result()
  566. def test_list_rmlvo(self):
  567. for args in (
  568. ["--verbose"],
  569. ["-v"],
  570. ["--verbose", "--load-exotic"],
  571. ["--load-exotic"],
  572. ["--ruleset=evdev"],
  573. ["--ruleset=base"],
  574. ):
  575. with self.subTest(args=args):
  576. self.xkbcli_list.run_command_success(args)
  577. def test_list_rmlvo_includes(self):
  578. args = ["/tmp/"]
  579. self.xkbcli_list.run_command_success(args)
  580. def test_list_rmlvo_includes_invalid(self):
  581. args = ["/proc/version"]
  582. rc, stdout, stderr = self.xkbcli_list.run_command(args)
  583. assert rc == 1
  584. assert "Failed to append include path" in stderr
  585. def test_list_rmlvo_includes_no_defaults(self):
  586. args = ["--skip-default-paths", "/tmp"]
  587. rc, stdout, stderr = self.xkbcli_list.run_command(args)
  588. assert rc == 1
  589. assert "Failed to parse XKB description" in stderr
  590. def test_interactive_evdev_rmlvo(self):
  591. def run(rmlvo):
  592. return self.xkbcli_interactive_evdev.run_command_success(rmlvo)
  593. with concurrent.futures.ThreadPoolExecutor() as executor:
  594. futures = {executor.submit(run, rmlvo): rmlvo for rmlvo in rmlvos}
  595. for future in concurrent.futures.as_completed(futures, TIMEOUT):
  596. rmlvo = futures[future]
  597. with self.subTest(rmlvo=rmlvo):
  598. future.result()
  599. # Note: use -h in the interactive tools to speedup the tests
  600. def test_interactive_evdev(self):
  601. # Note: --enable-compose fails if $prefix doesn't have the compose tables
  602. # installed
  603. for args in (
  604. ["--verbose", "-h"],
  605. ["--uniline", "-h"],
  606. ["--multiline", "-h"],
  607. ["--report-state-changes", "-h"],
  608. ["--no-state-report", "-h"],
  609. ["--consumed-mode=xkb", "-h"],
  610. ["--consumed-mode=gtk", "-h"],
  611. ["--without-x11-offset", "-h"],
  612. ["--format=xkb_v2", "-h"],
  613. ["--strict", "-h"],
  614. ["--enable-compose", "-h"],
  615. ["--legacy-state-api", "-h"],
  616. ["--legacy-state-api=false", "-h"],
  617. ["--legacy-state-api=true", "-h"],
  618. ["--controls=+sticky-keys,-sticky-keys-latch-to-lock", "-h"],
  619. ["--modifiers-mapping=Control+Alt:Level3", "-h"],
  620. ["--shortcuts-mask=Control+Alt+Super", "-h"],
  621. ["--shortcuts-mapping=2:1", "-h"],
  622. ):
  623. with self.subTest(args=args):
  624. self.xkbcli_interactive_evdev.run_command_success(args)
  625. def test_interactive_x11(self):
  626. for args in (
  627. ["--verbose", "-h"],
  628. ["--uniline", "-h"],
  629. ["--multiline", "-h"],
  630. ["--no-state-report", "-h"],
  631. ["--consumed-mode=xkb", "-h"],
  632. ["--consumed-mode=gtk", "-h"],
  633. ["--format=xkb_v2", "-h"],
  634. ["--strict", "-h"],
  635. ["--enable-compose", "-h"],
  636. ["--local-state", "-h"],
  637. ["--legacy-state-api", "-h"],
  638. ["--legacy-state-api=false", "-h"],
  639. ["--legacy-state-api=true", "-h"],
  640. ["--controls=+sticky-keys,-sticky-keys-latch-to-lock", "-h"],
  641. ["--modifiers-mapping=Control+Alt:Level3", "-h"],
  642. ["--shortcuts-mask=Control+Alt+Super", "-h"],
  643. ["--shortcuts-mapping=2:1", "-h"],
  644. ):
  645. with self.subTest(args=args):
  646. self.xkbcli_interactive_x11.run_command_success(args)
  647. def test_interactive_wayland(self):
  648. for args in (
  649. ["--verbose", "-h"],
  650. ["--uniline", "-h"],
  651. ["--multiline", "-h"],
  652. ["--no-state-report", "-h"],
  653. ["--consumed-mode=xkb", "-h"],
  654. ["--consumed-mode=gtk", "-h"],
  655. ["--format=xkb_v2", "-h"],
  656. ["--strict", "-h"],
  657. ["--enable-compose", "-h"],
  658. ["--local-state", "-h"],
  659. ["--legacy-state-api", "-h"],
  660. ["--legacy-state-api=false", "-h"],
  661. ["--legacy-state-api=true", "-h"],
  662. ["--controls=+sticky-keys,-sticky-keys-latch-to-lock", "-h"],
  663. ["--modifiers-mapping=Control+Alt:Level3", "-h"],
  664. ["--shortcuts-mask=Control+Alt+Super", "-h"],
  665. ["--shortcuts-mapping=2:1", "-h"],
  666. ):
  667. with self.subTest(args=args):
  668. self.xkbcli_interactive_wayland.run_command_success(args)
  669. def test_dump_keymap_wayland(self):
  670. for args in (
  671. ["--verbose", "-h"],
  672. ["--format=v2", "-h"],
  673. ["--strict", "-h"],
  674. ["--no-pretty", "-h"],
  675. ["--drop-unused", "-h"],
  676. ):
  677. with self.subTest(args=args):
  678. self.xkbcli_dump_keymap_wayland.run_command_success(args)
  679. def test_dump_keymap_x11(self):
  680. for args in (
  681. ["--verbose", "-h"],
  682. ["--format=v2", "-h"],
  683. ["--strict", "-h"],
  684. ["--no-pretty", "-h"],
  685. ["--drop-unused", "-h"],
  686. ):
  687. with self.subTest(args=args):
  688. self.xkbcli_dump_keymap_x11.run_command_success(args)
  689. if __name__ == "__main__":
  690. with tempfile.TemporaryDirectory() as tmpdir:
  691. # Use our own test xkeyboard-config copy.
  692. os.environ["XKB_CONFIG_ROOT"] = str(top_srcdir / "test/data")
  693. # Use our own X11 locale copy.
  694. os.environ["XLOCALEDIR"] = str(top_srcdir / "test/data/locale")
  695. # Use our own locale.
  696. os.environ["LC_CTYPE"] = "en_US.UTF-8"
  697. # libxkbcommon has fallbacks when XDG_CONFIG_HOME isn't set so we need
  698. # to override it with a known (empty) directory. Otherwise our test
  699. # behavior depends on the system the test is run on.
  700. os.environ["XDG_CONFIG_HOME"] = tmpdir
  701. # Prevent the legacy $HOME/.xkb from kicking in.
  702. del os.environ["HOME"]
  703. # This needs to be separated if we do specific extra path testing
  704. os.environ["XKB_CONFIG_EXTRA_PATH"] = tmpdir
  705. os.environ["XKB_CONFIG_VERSIONED_EXTENSIONS_PATH"] = tmpdir
  706. os.environ["XKB_CONFIG_UNVERSIONED_EXTENSIONS_PATH"] = tmpdir
  707. unittest.main()