Merge tag 'trace-printf-v6.13' of git://git.kernel.org/pub/scm/linux/kernel/git/trace...
[drm/drm-misc.git] / tools / testing / selftests / hid / tests / base.py
blob3a465768e507dd8fe540c60ebc2dd3a133e6f19b
1 #!/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8 -*-
5 # Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>
6 # Copyright (c) 2017 Red Hat, Inc.
8 import libevdev
9 import os
10 import pytest
11 import shutil
12 import subprocess
13 import time
15 import logging
17 from .base_device import BaseDevice, EvdevMatch, SysfsFile
18 from pathlib import Path
19 from typing import Final, List, Tuple
21 logger = logging.getLogger("hidtools.test.base")
23 # application to matches
24 application_matches: Final = {
25 # pyright: ignore
26 "Accelerometer": EvdevMatch(
27 req_properties=[
28 libevdev.INPUT_PROP_ACCELEROMETER,
31 "Game Pad": EvdevMatch( # in systemd, this is a lot more complex, but that will do
32 requires=[
33 libevdev.EV_ABS.ABS_X,
34 libevdev.EV_ABS.ABS_Y,
35 libevdev.EV_ABS.ABS_RX,
36 libevdev.EV_ABS.ABS_RY,
37 libevdev.EV_KEY.BTN_START,
39 excl_properties=[
40 libevdev.INPUT_PROP_ACCELEROMETER,
43 "Joystick": EvdevMatch( # in systemd, this is a lot more complex, but that will do
44 requires=[
45 libevdev.EV_ABS.ABS_RX,
46 libevdev.EV_ABS.ABS_RY,
47 libevdev.EV_KEY.BTN_START,
49 excl_properties=[
50 libevdev.INPUT_PROP_ACCELEROMETER,
53 "Key": EvdevMatch(
54 requires=[
55 libevdev.EV_KEY.KEY_A,
57 excl_properties=[
58 libevdev.INPUT_PROP_ACCELEROMETER,
59 libevdev.INPUT_PROP_DIRECT,
60 libevdev.INPUT_PROP_POINTER,
63 "Mouse": EvdevMatch(
64 requires=[
65 libevdev.EV_REL.REL_X,
66 libevdev.EV_REL.REL_Y,
67 libevdev.EV_KEY.BTN_LEFT,
69 excl_properties=[
70 libevdev.INPUT_PROP_ACCELEROMETER,
73 "Pad": EvdevMatch(
74 requires=[
75 libevdev.EV_KEY.BTN_0,
77 excludes=[
78 libevdev.EV_KEY.BTN_TOOL_PEN,
79 libevdev.EV_KEY.BTN_TOUCH,
80 libevdev.EV_ABS.ABS_DISTANCE,
82 excl_properties=[
83 libevdev.INPUT_PROP_ACCELEROMETER,
86 "Pen": EvdevMatch(
87 requires=[
88 libevdev.EV_KEY.BTN_STYLUS,
89 libevdev.EV_ABS.ABS_X,
90 libevdev.EV_ABS.ABS_Y,
92 excl_properties=[
93 libevdev.INPUT_PROP_ACCELEROMETER,
96 "Stylus": EvdevMatch(
97 requires=[
98 libevdev.EV_KEY.BTN_STYLUS,
99 libevdev.EV_ABS.ABS_X,
100 libevdev.EV_ABS.ABS_Y,
102 excl_properties=[
103 libevdev.INPUT_PROP_ACCELEROMETER,
106 "Touch Pad": EvdevMatch(
107 requires=[
108 libevdev.EV_KEY.BTN_LEFT,
109 libevdev.EV_ABS.ABS_X,
110 libevdev.EV_ABS.ABS_Y,
112 excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
113 req_properties=[
114 libevdev.INPUT_PROP_POINTER,
116 excl_properties=[
117 libevdev.INPUT_PROP_ACCELEROMETER,
120 "Touch Screen": EvdevMatch(
121 requires=[
122 libevdev.EV_KEY.BTN_TOUCH,
123 libevdev.EV_ABS.ABS_X,
124 libevdev.EV_ABS.ABS_Y,
126 excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
127 req_properties=[
128 libevdev.INPUT_PROP_DIRECT,
130 excl_properties=[
131 libevdev.INPUT_PROP_ACCELEROMETER,
137 class UHIDTestDevice(BaseDevice):
138 def __init__(self, name, application, rdesc_str=None, rdesc=None, input_info=None):
139 super().__init__(name, application, rdesc_str, rdesc, input_info)
140 self.application_matches = application_matches
141 if name is None:
142 name = f"uhid test {self.__class__.__name__}"
143 if not name.startswith("uhid test "):
144 name = "uhid test " + self.name
145 self.name = name
148 class BaseTestCase:
149 class TestUhid(object):
150 syn_event = libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT) # type: ignore
151 key_event = libevdev.InputEvent(libevdev.EV_KEY) # type: ignore
152 abs_event = libevdev.InputEvent(libevdev.EV_ABS) # type: ignore
153 rel_event = libevdev.InputEvent(libevdev.EV_REL) # type: ignore
154 msc_event = libevdev.InputEvent(libevdev.EV_MSC.MSC_SCAN) # type: ignore
156 # List of kernel modules to load before starting the test
157 # if any module is not available (not compiled), the test will skip.
158 # Each element is a tuple '(kernel driver name, kernel module)',
159 # for example ("playstation", "hid-playstation")
160 kernel_modules: List[Tuple[str, str]] = []
162 # List of in kernel HID-BPF object files to load
163 # before starting the test
164 # Any existing pre-loaded HID-BPF module will be removed
165 # before the ones in this list will be manually loaded.
166 # Each Element is a tuple '(hid_bpf_object, rdesc_fixup_present)',
167 # for example '("xppen-ArtistPro16Gen2.bpf.o", True)'
168 # If 'rdesc_fixup_present' is True, the test needs to wait
169 # for one unbind and rebind before it can be sure the kernel is
170 # ready
171 hid_bpfs: List[Tuple[str, bool]] = []
173 def assertInputEventsIn(self, expected_events, effective_events):
174 effective_events = effective_events.copy()
175 for ev in expected_events:
176 assert ev in effective_events
177 effective_events.remove(ev)
178 return effective_events
180 def assertInputEvents(self, expected_events, effective_events):
181 remaining = self.assertInputEventsIn(expected_events, effective_events)
182 assert remaining == []
184 @classmethod
185 def debug_reports(cls, reports, uhdev=None, events=None):
186 data = [" ".join([f"{v:02x}" for v in r]) for r in reports]
188 if uhdev is not None:
189 human_data = [
190 uhdev.parsed_rdesc.format_report(r, split_lines=True)
191 for r in reports
193 try:
194 human_data = [
195 f'\n\t {" " * h.index("/")}'.join(h.split("\n"))
196 for h in human_data
198 except ValueError:
199 # '/' not found: not a numbered report
200 human_data = ["\n\t ".join(h.split("\n")) for h in human_data]
201 data = [f"{d}\n\t ====> {h}" for d, h in zip(data, human_data)]
203 reports = data
205 if len(reports) == 1:
206 print("sending 1 report:")
207 else:
208 print(f"sending {len(reports)} reports:")
209 for report in reports:
210 print("\t", report)
212 if events is not None:
213 print("events received:", events)
215 def create_device(self):
216 raise Exception("please reimplement me in subclasses")
218 def _load_kernel_module(self, kernel_driver, kernel_module):
219 sysfs_path = Path("/sys/bus/hid/drivers")
220 if kernel_driver is not None:
221 sysfs_path /= kernel_driver
222 else:
223 # special case for when testing all available modules:
224 # we don't know beforehand the name of the module from modinfo
225 sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_")
226 if not sysfs_path.exists():
227 ret = subprocess.run(["/usr/sbin/modprobe", kernel_module])
228 if ret.returncode != 0:
229 pytest.skip(
230 f"module {kernel_module} could not be loaded, skipping the test"
233 @pytest.fixture()
234 def load_kernel_module(self):
235 for kernel_driver, kernel_module in self.kernel_modules:
236 self._load_kernel_module(kernel_driver, kernel_module)
237 yield
239 def load_hid_bpfs(self):
240 script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
241 root_dir = (script_dir / "../../../../..").resolve()
242 bpf_dir = root_dir / "drivers/hid/bpf/progs"
244 udev_hid_bpf = shutil.which("udev-hid-bpf")
245 if not udev_hid_bpf:
246 pytest.skip("udev-hid-bpf not found in $PATH, skipping")
248 wait = False
249 for _, rdesc_fixup in self.hid_bpfs:
250 if rdesc_fixup:
251 wait = True
253 for hid_bpf, _ in self.hid_bpfs:
254 # We need to start `udev-hid-bpf` in the background
255 # and dispatch uhid events in case the kernel needs
256 # to fetch features on the device
257 process = subprocess.Popen(
259 "udev-hid-bpf",
260 "--verbose",
261 "add",
262 str(self.uhdev.sys_path),
263 str(bpf_dir / hid_bpf),
266 while process.poll() is None:
267 self.uhdev.dispatch(1)
269 if process.poll() != 0:
270 pytest.fail(
271 f"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed"
274 if wait:
275 # the HID-BPF program exports a rdesc fixup, so it needs to be
276 # unbound by the kernel and then rebound.
277 # Ensure we get the bound event exactly 2 times (one for the normal
278 # uhid loading, and then the reload from HID-BPF)
279 now = time.time()
280 while self.uhdev.kernel_ready_count < 2 and time.time() - now < 2:
281 self.uhdev.dispatch(1)
283 if self.uhdev.kernel_ready_count < 2:
284 pytest.fail(
285 f"Couldn't insert hid-bpf programs, marking the test as failed"
288 def unload_hid_bpfs(self):
289 ret = subprocess.run(
290 ["udev-hid-bpf", "--verbose", "remove", str(self.uhdev.sys_path)],
292 if ret.returncode != 0:
293 pytest.fail(
294 f"Couldn't unload hid-bpf programs, marking the test as failed"
297 @pytest.fixture()
298 def new_uhdev(self, load_kernel_module):
299 return self.create_device()
301 def assertName(self, uhdev):
302 evdev = uhdev.get_evdev()
303 assert uhdev.name in evdev.name
305 @pytest.fixture(autouse=True)
306 def context(self, new_uhdev, request):
307 try:
308 with HIDTestUdevRule.instance():
309 with new_uhdev as self.uhdev:
310 for skip_cond in request.node.iter_markers("skip_if_uhdev"):
311 test, message, *rest = skip_cond.args
313 if test(self.uhdev):
314 pytest.skip(message)
316 self.uhdev.create_kernel_device()
317 now = time.time()
318 while not self.uhdev.is_ready() and time.time() - now < 5:
319 self.uhdev.dispatch(1)
321 if self.hid_bpfs:
322 self.load_hid_bpfs()
324 if self.uhdev.get_evdev() is None:
325 logger.warning(
326 f"available list of input nodes: (default application is '{self.uhdev.application}')"
328 logger.warning(self.uhdev.input_nodes)
329 yield
330 if self.hid_bpfs:
331 self.unload_hid_bpfs()
332 self.uhdev = None
333 except PermissionError:
334 pytest.skip("Insufficient permissions, run me as root")
336 @pytest.fixture(autouse=True)
337 def check_taint(self):
338 # we are abusing SysfsFile here, it's in /proc, but meh
339 taint_file = SysfsFile("/proc/sys/kernel/tainted")
340 taint = taint_file.int_value
342 yield
344 assert taint_file.int_value == taint
346 def test_creation(self):
347 """Make sure the device gets processed by the kernel and creates
348 the expected application input node.
350 If this fail, there is something wrong in the device report
351 descriptors."""
352 uhdev = self.uhdev
353 assert uhdev is not None
354 assert uhdev.get_evdev() is not None
355 self.assertName(uhdev)
356 assert len(uhdev.next_sync_events()) == 0
357 assert uhdev.get_evdev() is not None
360 class HIDTestUdevRule(object):
361 _instance = None
363 A context-manager compatible class that sets up our udev rules file and
364 deletes it on context exit.
366 This class is tailored to our test setup: it only sets up the udev rule
367 on the **second** context and it cleans it up again on the last context
368 removed. This matches the expected pytest setup: we enter a context for
369 the session once, then once for each test (the first of which will
370 trigger the udev rule) and once the last test exited and the session
371 exited, we clean up after ourselves.
374 def __init__(self):
375 self.refs = 0
376 self.rulesfile = None
378 def __enter__(self):
379 self.refs += 1
380 if self.refs == 2 and self.rulesfile is None:
381 self.create_udev_rule()
382 self.reload_udev_rules()
384 def __exit__(self, exc_type, exc_value, traceback):
385 self.refs -= 1
386 if self.refs == 0 and self.rulesfile:
387 os.remove(self.rulesfile.name)
388 self.reload_udev_rules()
390 def reload_udev_rules(self):
391 subprocess.run("udevadm control --reload-rules".split())
392 subprocess.run("systemd-hwdb update".split())
394 def create_udev_rule(self):
395 import tempfile
397 os.makedirs("/run/udev/rules.d", exist_ok=True)
398 with tempfile.NamedTemporaryFile(
399 prefix="91-uhid-test-device-REMOVEME-",
400 suffix=".rules",
401 mode="w+",
402 dir="/run/udev/rules.d",
403 delete=False,
404 ) as f:
405 f.write(
407 KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"
408 KERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1"
409 KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"
412 self.rulesfile = f
414 @classmethod
415 def instance(cls):
416 if not cls._instance:
417 cls._instance = HIDTestUdevRule()
418 return cls._instance