2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8 -*-
6 # Copyright (c) 2017 Red Hat, Inc.
15 from hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile
16 from pathlib import Path
17 from typing import Final
19 logger = logging.getLogger("hidtools.test.base")
21 # application to matches
22 application_matches: Final = {
24 "Accelerometer": EvdevMatch(
26 libevdev.INPUT_PROP_ACCELEROMETER,
29 "Game Pad": EvdevMatch( # in systemd, this is a lot more complex, but that will do
31 libevdev.EV_ABS.ABS_X,
32 libevdev.EV_ABS.ABS_Y,
33 libevdev.EV_ABS.ABS_RX,
34 libevdev.EV_ABS.ABS_RY,
35 libevdev.EV_KEY.BTN_START,
38 libevdev.INPUT_PROP_ACCELEROMETER,
41 "Joystick": EvdevMatch( # in systemd, this is a lot more complex, but that will do
43 libevdev.EV_ABS.ABS_RX,
44 libevdev.EV_ABS.ABS_RY,
45 libevdev.EV_KEY.BTN_START,
48 libevdev.INPUT_PROP_ACCELEROMETER,
53 libevdev.EV_KEY.KEY_A,
56 libevdev.INPUT_PROP_ACCELEROMETER,
57 libevdev.INPUT_PROP_DIRECT,
58 libevdev.INPUT_PROP_POINTER,
63 libevdev.EV_REL.REL_X,
64 libevdev.EV_REL.REL_Y,
65 libevdev.EV_KEY.BTN_LEFT,
68 libevdev.INPUT_PROP_ACCELEROMETER,
73 libevdev.EV_KEY.BTN_0,
76 libevdev.EV_KEY.BTN_TOOL_PEN,
77 libevdev.EV_KEY.BTN_TOUCH,
78 libevdev.EV_ABS.ABS_DISTANCE,
81 libevdev.INPUT_PROP_ACCELEROMETER,
86 libevdev.EV_KEY.BTN_STYLUS,
87 libevdev.EV_ABS.ABS_X,
88 libevdev.EV_ABS.ABS_Y,
91 libevdev.INPUT_PROP_ACCELEROMETER,
96 libevdev.EV_KEY.BTN_STYLUS,
97 libevdev.EV_ABS.ABS_X,
98 libevdev.EV_ABS.ABS_Y,
101 libevdev.INPUT_PROP_ACCELEROMETER,
104 "Touch Pad": EvdevMatch(
106 libevdev.EV_KEY.BTN_LEFT,
107 libevdev.EV_ABS.ABS_X,
108 libevdev.EV_ABS.ABS_Y,
110 excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
112 libevdev.INPUT_PROP_POINTER,
115 libevdev.INPUT_PROP_ACCELEROMETER,
118 "Touch Screen": EvdevMatch(
120 libevdev.EV_KEY.BTN_TOUCH,
121 libevdev.EV_ABS.ABS_X,
122 libevdev.EV_ABS.ABS_Y,
124 excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
126 libevdev.INPUT_PROP_DIRECT,
129 libevdev.INPUT_PROP_ACCELEROMETER,
135 class UHIDTestDevice(BaseDevice):
136 def __init__(self, name, application, rdesc_str=None, rdesc=None, input_info=None):
137 super().__init__(name, application, rdesc_str, rdesc, input_info)
138 self.application_matches = application_matches
140 name = f"uhid test {self.__class__.__name__}"
141 if not name.startswith("uhid test "):
142 name = "uhid test " + self.name
147 class TestUhid(object):
148 syn_event = libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT) # type: ignore
149 key_event = libevdev.InputEvent(libevdev.EV_KEY) # type: ignore
150 abs_event = libevdev.InputEvent(libevdev.EV_ABS) # type: ignore
151 rel_event = libevdev.InputEvent(libevdev.EV_REL) # type: ignore
152 msc_event = libevdev.InputEvent(libevdev.EV_MSC.MSC_SCAN) # type: ignore
154 # List of kernel modules to load before starting the test
155 # if any module is not available (not compiled), the test will skip.
156 # Each element is a tuple '(kernel driver name, kernel module)',
157 # for example ("playstation", "hid-playstation")
160 def assertInputEventsIn(self, expected_events, effective_events):
161 effective_events = effective_events.copy()
162 for ev in expected_events:
163 assert ev in effective_events
164 effective_events.remove(ev)
165 return effective_events
167 def assertInputEvents(self, expected_events, effective_events):
168 remaining = self.assertInputEventsIn(expected_events, effective_events)
169 assert remaining == []
172 def debug_reports(cls, reports, uhdev=None, events=None):
173 data = [" ".join([f"{v:02x}" for v in r]) for r in reports]
175 if uhdev is not None:
177 uhdev.parsed_rdesc.format_report(r, split_lines=True)
182 f'\n\t {" " * h.index("/")}'.join(h.split("\n"))
186 # '/' not found: not a numbered report
187 human_data = ["\n\t ".join(h.split("\n")) for h in human_data]
188 data = [f"{d}\n\t ====> {h}" for d, h in zip(data, human_data)]
192 if len(reports) == 1:
193 print("sending 1 report:")
195 print(f"sending {len(reports)} reports:")
196 for report in reports:
199 if events is not None:
200 print("events received:", events)
202 def create_device(self):
203 raise Exception("please reimplement me in subclasses")
205 def _load_kernel_module(self, kernel_driver, kernel_module):
206 sysfs_path = Path("/sys/bus/hid/drivers")
207 if kernel_driver is not None:
208 sysfs_path /= kernel_driver
210 # special case for when testing all available modules:
211 # we don't know beforehand the name of the module from modinfo
212 sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_")
213 if not sysfs_path.exists():
216 ret = subprocess.run(["/usr/sbin/modprobe", kernel_module])
217 if ret.returncode != 0:
219 f"module {kernel_module} could not be loaded, skipping the test"
223 def load_kernel_module(self):
224 for kernel_driver, kernel_module in self.kernel_modules:
225 self._load_kernel_module(kernel_driver, kernel_module)
229 def new_uhdev(self, load_kernel_module):
230 return self.create_device()
232 def assertName(self, uhdev):
233 evdev = uhdev.get_evdev()
234 assert uhdev.name in evdev.name
236 @pytest.fixture(autouse=True)
237 def context(self, new_uhdev, request):
239 with HIDTestUdevRule.instance():
240 with new_uhdev as self.uhdev:
241 skip_cond = request.node.get_closest_marker("skip_if_uhdev")
243 test, message, *rest = skip_cond.args
248 self.uhdev.create_kernel_device()
250 while not self.uhdev.is_ready() and time.time() - now < 5:
251 self.uhdev.dispatch(1)
252 if self.uhdev.get_evdev() is None:
254 f"available list of input nodes: (default application is '{self.uhdev.application}')"
256 logger.warning(self.uhdev.input_nodes)
259 except PermissionError:
260 pytest.skip("Insufficient permissions, run me as root")
262 @pytest.fixture(autouse=True)
263 def check_taint(self):
264 # we are abusing SysfsFile here, it's in /proc, but meh
265 taint_file = SysfsFile("/proc/sys/kernel/tainted")
266 taint = taint_file.int_value
270 assert taint_file.int_value == taint
272 def test_creation(self):
273 """Make sure the device gets processed by the kernel and creates
274 the expected application input node.
276 If this fail, there is something wrong in the device report
279 assert uhdev is not None
280 assert uhdev.get_evdev() is not None
281 self.assertName(uhdev)
282 assert len(uhdev.next_sync_events()) == 0
283 assert uhdev.get_evdev() is not None
286 class HIDTestUdevRule(object):
289 A context-manager compatible class that sets up our udev rules file and
290 deletes it on context exit.
292 This class is tailored to our test setup: it only sets up the udev rule
293 on the **second** context and it cleans it up again on the last context
294 removed. This matches the expected pytest setup: we enter a context for
295 the session once, then once for each test (the first of which will
296 trigger the udev rule) and once the last test exited and the session
297 exited, we clean up after ourselves.
302 self.rulesfile = None
306 if self.refs == 2 and self.rulesfile is None:
307 self.create_udev_rule()
308 self.reload_udev_rules()
310 def __exit__(self, exc_type, exc_value, traceback):
312 if self.refs == 0 and self.rulesfile:
313 os.remove(self.rulesfile.name)
314 self.reload_udev_rules()
316 def reload_udev_rules(self):
319 subprocess.run("udevadm control --reload-rules".split())
320 subprocess.run("systemd-hwdb update".split())
322 def create_udev_rule(self):
325 os.makedirs("/run/udev/rules.d", exist_ok=True)
326 with tempfile.NamedTemporaryFile(
327 prefix="91-uhid-test-device-REMOVEME-",
330 dir="/run/udev/rules.d",
334 'KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"\n'
337 'KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"\n'
343 if not cls._instance:
344 cls._instance = HIDTestUdevRule()