test_tool_option_parsing.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. #!/usr/bin/env python3
  2. # vim: set expandtab shiftwidth=4:
  3. # -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
  4. #
  5. # Copyright © 2018 Red Hat, Inc.
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a
  8. # copy of this software and associated documentation files (the "Software"),
  9. # to deal in the Software without restriction, including without limitation
  10. # the rights to use, copy, modify, merge, publish, distribute, sublicense,
  11. # and/or sell copies of the Software, and to permit persons to whom the
  12. # Software is furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice (including the next
  15. # paragraph) shall be included in all copies or substantial portions of the
  16. # Software.
  17. #
  18. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  21. # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  23. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  24. # DEALINGS IN THE SOFTWARE.
  25. import logging
  26. import os
  27. import resource
  28. import subprocess
  29. import sys
  30. try:
  31. import pytest
  32. except ImportError:
  33. print("Failed to import pytest. Skipping.", file=sys.stderr)
  34. sys.exit(77)
  35. logger = logging.getLogger("test")
  36. logger.setLevel(logging.DEBUG)
  37. if "@DISABLE_WARNING@" != "yes": # noqa: PLR0133
  38. print("This is the source file, run the one in the meson builddir instead")
  39. sys.exit(1)
  40. def _disable_coredump():
  41. resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
  42. def run_command(args):
  43. logger.debug(f"run command: {' '.join(args)}")
  44. with subprocess.Popen(
  45. args,
  46. preexec_fn=_disable_coredump, # noqa: PLW1509
  47. stdout=subprocess.PIPE,
  48. stderr=subprocess.PIPE,
  49. ) as p:
  50. try:
  51. p.wait(0.7)
  52. except subprocess.TimeoutExpired:
  53. p.send_signal(3) # SIGQUIT
  54. stdout, stderr = p.communicate(timeout=5)
  55. if p.returncode == -3:
  56. p.returncode = 0
  57. return p.returncode, stdout.decode("UTF-8"), stderr.decode("UTF-8")
  58. class LibinputTool:
  59. libinput_tool = "libinput"
  60. subtool = None
  61. def __init__(self, subtool=None):
  62. self.libinput_tool = "@TOOL_PATH@"
  63. self.subtool = subtool
  64. def run_command(self, args):
  65. args = [self.libinput_tool] + args
  66. if self.subtool is not None:
  67. args.insert(1, self.subtool)
  68. return run_command(args)
  69. def run_command_success(self, args):
  70. rc, stdout, stderr = self.run_command(args)
  71. # if we're running as user, we might fail the command but we should
  72. # never get rc 2 (invalid usage)
  73. assert rc in [0, 1], (stdout, stderr)
  74. return stdout, stderr
  75. def run_command_invalid(self, args):
  76. rc, stdout, stderr = self.run_command(args)
  77. assert rc == 2, (rc, stdout, stderr)
  78. return rc, stdout, stderr
  79. def run_command_unrecognized_option(self, args):
  80. rc, stdout, stderr = self.run_command(args)
  81. assert rc == 2, (rc, stdout, stderr)
  82. assert stdout.startswith("Usage") or stdout == ""
  83. assert "unrecognized option" in stderr
  84. def run_command_missing_arg(self, args):
  85. rc, stdout, stderr = self.run_command(args)
  86. assert rc == 2, (rc, stdout, stderr)
  87. assert stdout.startswith("Usage") or stdout == ""
  88. assert "requires an argument" in stderr
  89. def run_command_unrecognized_tool(self, args):
  90. rc, stdout, stderr = self.run_command(args)
  91. assert rc == 2, (rc, stdout, stderr)
  92. assert stdout.startswith("Usage") or stdout == ""
  93. assert "is not installed" in stderr
  94. class LibinputDebugGui(LibinputTool):
  95. def __init__(self, subtool="debug-gui"):
  96. assert subtool == "debug-gui"
  97. super().__init__(subtool)
  98. debug_gui_enabled = "@MESON_ENABLED_DEBUG_GUI@".lower() == "true"
  99. if not debug_gui_enabled:
  100. pytest.skip()
  101. if not os.getenv("DISPLAY") and not os.getenv("WAYLAND_DISPLAY"):
  102. pytest.skip()
  103. # 77 means gtk_init() failed, which is probably because you can't
  104. # connect to the display server.
  105. rc, _, _ = self.run_command(["--help"])
  106. if rc == 77:
  107. pytest.skip()
  108. def get_tool(subtool=None):
  109. if subtool == "debug-gui":
  110. return LibinputDebugGui()
  111. else:
  112. return LibinputTool(subtool)
  113. @pytest.fixture
  114. def libinput():
  115. return get_tool()
  116. @pytest.fixture(params=["debug-events", "debug-gui"])
  117. def libinput_debug_tool(request):
  118. yield get_tool(request.param)
  119. @pytest.fixture
  120. def libinput_debug_events():
  121. return get_tool("debug-events")
  122. @pytest.fixture
  123. def libinput_debug_gui():
  124. return get_tool("debug-gui")
  125. @pytest.fixture
  126. def libinput_record():
  127. return get_tool("record")
  128. def test_help(libinput):
  129. stdout, stderr = libinput.run_command_success(["--help"])
  130. assert stdout.startswith("Usage:")
  131. assert stderr == ""
  132. def test_version(libinput):
  133. stdout, stderr = libinput.run_command_success(["--version"])
  134. assert stdout.startswith("1")
  135. assert stderr == ""
  136. @pytest.mark.parametrize("argument", ["--banana", "--foo", "--quiet", "--verbose"])
  137. def test_invalid_arguments(libinput, argument):
  138. libinput.run_command_unrecognized_option([argument])
  139. @pytest.mark.parametrize("tool", [["foo"], ["debug"], ["foo", "--quiet"]])
  140. def test_invalid_tool(libinput, tool):
  141. libinput.run_command_unrecognized_tool(tool)
  142. def test_udev_seat(libinput_debug_tool):
  143. libinput_debug_tool.run_command_missing_arg(["--udev"])
  144. libinput_debug_tool.run_command_success(["--udev", "seat0"])
  145. libinput_debug_tool.run_command_success(["--udev", "seat1"])
  146. @pytest.mark.skipif(os.environ.get("UDEV_NOT_AVAILABLE"), reason="udev required")
  147. def test_device_arg(libinput_debug_tool):
  148. libinput_debug_tool.run_command_missing_arg(["--device"])
  149. libinput_debug_tool.run_command_success(["--device", "/dev/input/event0"])
  150. libinput_debug_tool.run_command_success(["--device", "/dev/input/event1"])
  151. libinput_debug_tool.run_command_success(["/dev/input/event0"])
  152. options = {
  153. "pattern": ["sendevents"],
  154. # enable/disable options
  155. "enable-disable": [
  156. "tap",
  157. "drag",
  158. "drag-lock",
  159. "middlebutton",
  160. "natural-scrolling",
  161. "left-handed",
  162. "dwt",
  163. "dwtp",
  164. "scroll-button-lock",
  165. "plugins",
  166. ],
  167. # options with distinct values
  168. "enums": {
  169. "set-click-method": ["none", "clickfinger", "buttonareas"],
  170. "set-scroll-method": ["none", "twofinger", "edge", "button"],
  171. "set-profile": ["adaptive", "flat", "custom"],
  172. "set-tap-map": ["lrm", "lmr"],
  173. "set-clickfinger-map": ["lrm", "lmr"],
  174. "enable-drag-lock": ["sticky", "timeout"],
  175. "set-sendevents": ["disabled", "enabled", "disabled-on-external-mouse"],
  176. "enable-3fg-drag": ["3fg", "4fg", "disabled"],
  177. "set-eraser-button-mode": ["default", "button"],
  178. "set-eraser-button-button": ["BTN_STYLUS", "BTN_STYLUS2", "BTN_STYLUS3"],
  179. "set-custom-type": ["fallback", "motion", "scroll"],
  180. },
  181. # options with a range (and increment)
  182. "ranges": {
  183. "set-speed": (-1.0, +1.0, 0.1),
  184. "set-rotation": (0, 360, 10),
  185. },
  186. }
  187. # Options that allow for glob patterns
  188. @pytest.mark.parametrize("option", options["pattern"])
  189. def test_options_pattern(libinput_debug_tool, option):
  190. libinput_debug_tool.run_command_success([f"--disable-{option}", "*"])
  191. libinput_debug_tool.run_command_success([f"--disable-{option}", "abc*"])
  192. @pytest.mark.parametrize("option", options["enable-disable"])
  193. def test_options_enable_disable(libinput_debug_tool, option):
  194. libinput_debug_tool.run_command_success([f"--enable-{option}"])
  195. libinput_debug_tool.run_command_success([f"--disable-{option}"])
  196. @pytest.mark.parametrize("option", options["enums"].items())
  197. def test_options_enums(libinput_debug_tool, option):
  198. name, values = option
  199. for v in values:
  200. libinput_debug_tool.run_command_success([f"--{name}", v])
  201. libinput_debug_tool.run_command_success([f"--{name}={v}"])
  202. @pytest.mark.parametrize("option", options["ranges"].items())
  203. def test_options_ranges(libinput_debug_tool, option):
  204. name, values = option
  205. minimum, maximum, step = values
  206. value = minimum
  207. while value < maximum:
  208. libinput_debug_tool.run_command_success([f"--{name}", str(value)])
  209. libinput_debug_tool.run_command_success([f"--{name}={value}"])
  210. value += step
  211. libinput_debug_tool.run_command_success([f"--{name}", str(maximum)])
  212. libinput_debug_tool.run_command_success([f"--{name}={maximum}"])
  213. def test_apply_to(libinput_debug_tool):
  214. libinput_debug_tool.run_command_missing_arg(["--apply-to"])
  215. libinput_debug_tool.run_command_success(["--apply-to", "*foo*"])
  216. libinput_debug_tool.run_command_success(["--apply-to", "foobar"])
  217. libinput_debug_tool.run_command_success(["--apply-to", "any"])
  218. def test_set_scroll_button(libinput_debug_tool):
  219. libinput_debug_tool.run_command_missing_arg(["--set-scroll-button"])
  220. libinput_debug_tool.run_command_success(["--set-scroll-button", "BTN_LEFT"])
  221. libinput_debug_tool.run_command_success(["--set-scroll-button=BTN_RIGHT"])
  222. def test_set_custom_points(libinput_debug_tool):
  223. libinput_debug_tool.run_command_missing_arg(["--set-custom-points"])
  224. libinput_debug_tool.run_command_success(["--set-custom-points", "0.0;1.0"])
  225. libinput_debug_tool.run_command_success(["--set-custom-points=0.0;0.5;1.0"])
  226. def test_set_custom_step(libinput_debug_tool):
  227. libinput_debug_tool.run_command_missing_arg(["--set-custom-step"])
  228. libinput_debug_tool.run_command_success(["--set-custom-step", "0.5"])
  229. libinput_debug_tool.run_command_success(["--set-custom-step=1.0"])
  230. def test_set_pressure_range(libinput_debug_tool):
  231. libinput_debug_tool.run_command_missing_arg(["--set-pressure-range"])
  232. libinput_debug_tool.run_command_success(["--set-pressure-range", "0.1:0.9"])
  233. libinput_debug_tool.run_command_success(["--set-pressure-range=0.2:0.8"])
  234. def test_set_calibration(libinput_debug_tool):
  235. libinput_debug_tool.run_command_missing_arg(["--set-calibration"])
  236. libinput_debug_tool.run_command_success(
  237. ["--set-calibration", "1.0 0.0 0.0 0.0 1.0 0.0"]
  238. )
  239. def test_set_area(libinput_debug_tool):
  240. libinput_debug_tool.run_command_missing_arg(["--set-area"])
  241. libinput_debug_tool.run_command_success(["--set-area", "0.0/0.0 1.0/1.0"])
  242. def test_set_plugin_path(libinput_debug_tool):
  243. libinput_debug_tool.run_command_missing_arg(["--set-plugin-path"])
  244. libinput_debug_tool.run_command_success(["--set-plugin-path", "/usr/lib/libinput"])
  245. libinput_debug_tool.run_command_success(
  246. ["--set-plugin-path=/usr/lib/libinput:/usr/local/lib/libinput"]
  247. )
  248. @pytest.mark.parametrize(
  249. "args",
  250. [["--verbose"], ["--quiet"], ["--verbose", "--quiet"], ["--quiet", "--verbose"]],
  251. )
  252. def test_debug_events_verbose_quiet(libinput_debug_events, args):
  253. libinput_debug_events.run_command_success(args)
  254. def test_debug_events_compress_motion_events(libinput_debug_events):
  255. libinput_debug_events.run_command_success(["--compress-motion-events"])
  256. def test_debug_events_grab(libinput_debug_events):
  257. libinput_debug_events.run_command_success(["--grab"])
  258. def test_debug_gui_grab(libinput_debug_gui):
  259. libinput_debug_gui.run_command_success(["--grab"])
  260. @pytest.mark.parametrize("arg", ["--banana", "--foo", "--version"])
  261. def test_invalid_args(libinput_debug_tool, arg):
  262. libinput_debug_tool.run_command_unrecognized_option([arg])
  263. def test_libinput_debug_events_multiple_devices(libinput_debug_events):
  264. libinput_debug_events.run_command_success(
  265. ["--device", "/dev/input/event0", "/dev/input/event1"]
  266. )
  267. # same event path multiple times? meh, your problem
  268. libinput_debug_events.run_command_success(
  269. ["--device", "/dev/input/event0", "/dev/input/event0"]
  270. )
  271. libinput_debug_events.run_command_success(
  272. ["/dev/input/event0", "/dev/input/event1"]
  273. )
  274. def test_libinput_debug_events_too_many_devices(libinput_debug_events):
  275. # Too many arguments just bails with the usage message
  276. rc, stdout, stderr = libinput_debug_events.run_command(["/dev/input/event0"] * 61)
  277. assert rc == 2, (stdout, stderr)
  278. @pytest.mark.parametrize("arg", ["--quiet"])
  279. def test_libinput_debug_gui_invalid_arg(libinput_debug_gui, arg):
  280. libinput_debug_gui.run_command_unrecognized_option([arg])
  281. def test_libinput_debug_gui_verbose(libinput_debug_gui):
  282. libinput_debug_gui.run_command_success(["--verbose"])
  283. @pytest.mark.parametrize(
  284. "arg",
  285. [
  286. "--help",
  287. "--show-keycodes",
  288. "--with-libinput",
  289. "--with-hidraw",
  290. "--grab",
  291. "--no-events",
  292. ],
  293. )
  294. def test_libinput_record_args(libinput_record, arg):
  295. libinput_record.run_command_success([arg])
  296. def test_libinput_record_multiple_arg(libinput_record):
  297. # this arg is deprecated and a noop
  298. libinput_record.run_command_success(["--multiple"])
  299. @pytest.fixture
  300. def recording(tmp_path):
  301. return str((tmp_path / "record.out").resolve())
  302. def test_libinput_record_all(libinput_record, recording):
  303. libinput_record.run_command_success(["--all", "-o", recording])
  304. libinput_record.run_command_success(["--all", recording])
  305. def test_libinput_record_outfile(libinput_record, recording):
  306. libinput_record.run_command_success(["-o", recording])
  307. libinput_record.run_command_success(["--output-file", recording])
  308. libinput_record.run_command_success([f"--output-file={recording}"])
  309. def test_libinput_record_single(libinput_record, recording):
  310. libinput_record.run_command_success(["/dev/input/event0"])
  311. libinput_record.run_command_success(["-o", recording, "/dev/input/event0"])
  312. libinput_record.run_command_success(["/dev/input/event0", recording])
  313. libinput_record.run_command_success([recording, "/dev/input/event0"])
  314. def test_libinput_record_multiple(libinput_record, recording):
  315. libinput_record.run_command_success(
  316. ["-o", recording, "/dev/input/event0", "/dev/input/event1"]
  317. )
  318. libinput_record.run_command_success(
  319. [recording, "/dev/input/event0", "/dev/input/event1"]
  320. )
  321. libinput_record.run_command_success(
  322. ["/dev/input/event0", "/dev/input/event1", recording]
  323. )
  324. def test_libinput_record_autorestart(libinput_record, recording):
  325. libinput_record.run_command_invalid(["--autorestart"])
  326. libinput_record.run_command_success(["--autorestart=2"])
  327. libinput_record.run_command_success(["-o", recording, "--autorestart=2"])
  328. def test_libinput_record_no_events_autorestart(libinput_record):
  329. libinput_record.run_command_invalid(["--no-events", "--autorestart=2"])
  330. def main():
  331. args = ["-m", "pytest"]
  332. try:
  333. import xdist # noqa
  334. ncores = os.environ.get("FDO_CI_CONCURRENT", "auto")
  335. args += ["-n", ncores]
  336. except ImportError:
  337. logger.info("python-xdist missing, this test will be slow")
  338. args += ["@MESON_BUILD_ROOT@"]
  339. os.environ["LIBINPUT_RUNNING_TEST_SUITE"] = "1"
  340. return subprocess.run([sys.executable] + args, check=False).returncode
  341. if __name__ == "__main__":
  342. raise SystemExit(main())