Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
1#!/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# -*- coding: utf-8 -*-
4#
5# Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>
6# Copyright (c) 2017 Red Hat, Inc.
7
8import libevdev
9import os
10import pytest
11import time
12
13import logging
14
15from hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile
16from pathlib import Path
17from typing import Final, List, Tuple
18
19logger = logging.getLogger("hidtools.test.base")
20
21# application to matches
22application_matches: Final = {
23 # pyright: ignore
24 "Accelerometer": EvdevMatch(
25 req_properties=[
26 libevdev.INPUT_PROP_ACCELEROMETER,
27 ]
28 ),
29 "Game Pad": EvdevMatch( # in systemd, this is a lot more complex, but that will do
30 requires=[
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,
36 ],
37 excl_properties=[
38 libevdev.INPUT_PROP_ACCELEROMETER,
39 ],
40 ),
41 "Joystick": EvdevMatch( # in systemd, this is a lot more complex, but that will do
42 requires=[
43 libevdev.EV_ABS.ABS_RX,
44 libevdev.EV_ABS.ABS_RY,
45 libevdev.EV_KEY.BTN_START,
46 ],
47 excl_properties=[
48 libevdev.INPUT_PROP_ACCELEROMETER,
49 ],
50 ),
51 "Key": EvdevMatch(
52 requires=[
53 libevdev.EV_KEY.KEY_A,
54 ],
55 excl_properties=[
56 libevdev.INPUT_PROP_ACCELEROMETER,
57 libevdev.INPUT_PROP_DIRECT,
58 libevdev.INPUT_PROP_POINTER,
59 ],
60 ),
61 "Mouse": EvdevMatch(
62 requires=[
63 libevdev.EV_REL.REL_X,
64 libevdev.EV_REL.REL_Y,
65 libevdev.EV_KEY.BTN_LEFT,
66 ],
67 excl_properties=[
68 libevdev.INPUT_PROP_ACCELEROMETER,
69 ],
70 ),
71 "Pad": EvdevMatch(
72 requires=[
73 libevdev.EV_KEY.BTN_0,
74 ],
75 excludes=[
76 libevdev.EV_KEY.BTN_TOOL_PEN,
77 libevdev.EV_KEY.BTN_TOUCH,
78 libevdev.EV_ABS.ABS_DISTANCE,
79 ],
80 excl_properties=[
81 libevdev.INPUT_PROP_ACCELEROMETER,
82 ],
83 ),
84 "Pen": EvdevMatch(
85 requires=[
86 libevdev.EV_KEY.BTN_STYLUS,
87 libevdev.EV_ABS.ABS_X,
88 libevdev.EV_ABS.ABS_Y,
89 ],
90 excl_properties=[
91 libevdev.INPUT_PROP_ACCELEROMETER,
92 ],
93 ),
94 "Stylus": EvdevMatch(
95 requires=[
96 libevdev.EV_KEY.BTN_STYLUS,
97 libevdev.EV_ABS.ABS_X,
98 libevdev.EV_ABS.ABS_Y,
99 ],
100 excl_properties=[
101 libevdev.INPUT_PROP_ACCELEROMETER,
102 ],
103 ),
104 "Touch Pad": EvdevMatch(
105 requires=[
106 libevdev.EV_KEY.BTN_LEFT,
107 libevdev.EV_ABS.ABS_X,
108 libevdev.EV_ABS.ABS_Y,
109 ],
110 excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
111 req_properties=[
112 libevdev.INPUT_PROP_POINTER,
113 ],
114 excl_properties=[
115 libevdev.INPUT_PROP_ACCELEROMETER,
116 ],
117 ),
118 "Touch Screen": EvdevMatch(
119 requires=[
120 libevdev.EV_KEY.BTN_TOUCH,
121 libevdev.EV_ABS.ABS_X,
122 libevdev.EV_ABS.ABS_Y,
123 ],
124 excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
125 req_properties=[
126 libevdev.INPUT_PROP_DIRECT,
127 ],
128 excl_properties=[
129 libevdev.INPUT_PROP_ACCELEROMETER,
130 ],
131 ),
132}
133
134
135class 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
139 if name is None:
140 name = f"uhid test {self.__class__.__name__}"
141 if not name.startswith("uhid test "):
142 name = "uhid test " + self.name
143 self.name = name
144
145
146class BaseTestCase:
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
153
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")
158 kernel_modules: List[Tuple[str, str]] = []
159
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
166
167 def assertInputEvents(self, expected_events, effective_events):
168 remaining = self.assertInputEventsIn(expected_events, effective_events)
169 assert remaining == []
170
171 @classmethod
172 def debug_reports(cls, reports, uhdev=None, events=None):
173 data = [" ".join([f"{v:02x}" for v in r]) for r in reports]
174
175 if uhdev is not None:
176 human_data = [
177 uhdev.parsed_rdesc.format_report(r, split_lines=True)
178 for r in reports
179 ]
180 try:
181 human_data = [
182 f'\n\t {" " * h.index("/")}'.join(h.split("\n"))
183 for h in human_data
184 ]
185 except ValueError:
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)]
189
190 reports = data
191
192 if len(reports) == 1:
193 print("sending 1 report:")
194 else:
195 print(f"sending {len(reports)} reports:")
196 for report in reports:
197 print("\t", report)
198
199 if events is not None:
200 print("events received:", events)
201
202 def create_device(self):
203 raise Exception("please reimplement me in subclasses")
204
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
209 else:
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():
214 import subprocess
215
216 ret = subprocess.run(["/usr/sbin/modprobe", kernel_module])
217 if ret.returncode != 0:
218 pytest.skip(
219 f"module {kernel_module} could not be loaded, skipping the test"
220 )
221
222 @pytest.fixture()
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)
226 yield
227
228 @pytest.fixture()
229 def new_uhdev(self, load_kernel_module):
230 return self.create_device()
231
232 def assertName(self, uhdev):
233 evdev = uhdev.get_evdev()
234 assert uhdev.name in evdev.name
235
236 @pytest.fixture(autouse=True)
237 def context(self, new_uhdev, request):
238 try:
239 with HIDTestUdevRule.instance():
240 with new_uhdev as self.uhdev:
241 for skip_cond in request.node.iter_markers("skip_if_uhdev"):
242 test, message, *rest = skip_cond.args
243
244 if test(self.uhdev):
245 pytest.skip(message)
246
247 self.uhdev.create_kernel_device()
248 now = time.time()
249 while not self.uhdev.is_ready() and time.time() - now < 5:
250 self.uhdev.dispatch(1)
251 if self.uhdev.get_evdev() is None:
252 logger.warning(
253 f"available list of input nodes: (default application is '{self.uhdev.application}')"
254 )
255 logger.warning(self.uhdev.input_nodes)
256 yield
257 self.uhdev = None
258 except PermissionError:
259 pytest.skip("Insufficient permissions, run me as root")
260
261 @pytest.fixture(autouse=True)
262 def check_taint(self):
263 # we are abusing SysfsFile here, it's in /proc, but meh
264 taint_file = SysfsFile("/proc/sys/kernel/tainted")
265 taint = taint_file.int_value
266
267 yield
268
269 assert taint_file.int_value == taint
270
271 def test_creation(self):
272 """Make sure the device gets processed by the kernel and creates
273 the expected application input node.
274
275 If this fail, there is something wrong in the device report
276 descriptors."""
277 uhdev = self.uhdev
278 assert uhdev is not None
279 assert uhdev.get_evdev() is not None
280 self.assertName(uhdev)
281 assert len(uhdev.next_sync_events()) == 0
282 assert uhdev.get_evdev() is not None
283
284
285class HIDTestUdevRule(object):
286 _instance = None
287 """
288 A context-manager compatible class that sets up our udev rules file and
289 deletes it on context exit.
290
291 This class is tailored to our test setup: it only sets up the udev rule
292 on the **second** context and it cleans it up again on the last context
293 removed. This matches the expected pytest setup: we enter a context for
294 the session once, then once for each test (the first of which will
295 trigger the udev rule) and once the last test exited and the session
296 exited, we clean up after ourselves.
297 """
298
299 def __init__(self):
300 self.refs = 0
301 self.rulesfile = None
302
303 def __enter__(self):
304 self.refs += 1
305 if self.refs == 2 and self.rulesfile is None:
306 self.create_udev_rule()
307 self.reload_udev_rules()
308
309 def __exit__(self, exc_type, exc_value, traceback):
310 self.refs -= 1
311 if self.refs == 0 and self.rulesfile:
312 os.remove(self.rulesfile.name)
313 self.reload_udev_rules()
314
315 def reload_udev_rules(self):
316 import subprocess
317
318 subprocess.run("udevadm control --reload-rules".split())
319 subprocess.run("systemd-hwdb update".split())
320
321 def create_udev_rule(self):
322 import tempfile
323
324 os.makedirs("/run/udev/rules.d", exist_ok=True)
325 with tempfile.NamedTemporaryFile(
326 prefix="91-uhid-test-device-REMOVEME-",
327 suffix=".rules",
328 mode="w+",
329 dir="/run/udev/rules.d",
330 delete=False,
331 ) as f:
332 f.write(
333 'KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"\n'
334 )
335 f.write(
336 'KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"\n'
337 )
338 self.rulesfile = f
339
340 @classmethod
341 def instance(cls):
342 if not cls._instance:
343 cls._instance = HIDTestUdevRule()
344 return cls._instance