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
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 = []
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 skip_cond = request.node.get_closest_marker("skip_if_uhdev")
242 if skip_cond:
243 test, message, *rest = skip_cond.args
244
245 if test(self.uhdev):
246 pytest.skip(message)
247
248 self.uhdev.create_kernel_device()
249 now = time.time()
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:
253 logger.warning(
254 f"available list of input nodes: (default application is '{self.uhdev.application}')"
255 )
256 logger.warning(self.uhdev.input_nodes)
257 yield
258 self.uhdev = None
259 except PermissionError:
260 pytest.skip("Insufficient permissions, run me as root")
261
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
267
268 yield
269
270 assert taint_file.int_value == taint
271
272 def test_creation(self):
273 """Make sure the device gets processed by the kernel and creates
274 the expected application input node.
275
276 If this fail, there is something wrong in the device report
277 descriptors."""
278 uhdev = self.uhdev
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
284
285
286class HIDTestUdevRule(object):
287 _instance = None
288 """
289 A context-manager compatible class that sets up our udev rules file and
290 deletes it on context exit.
291
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.
298 """
299
300 def __init__(self):
301 self.refs = 0
302 self.rulesfile = None
303
304 def __enter__(self):
305 self.refs += 1
306 if self.refs == 2 and self.rulesfile is None:
307 self.create_udev_rule()
308 self.reload_udev_rules()
309
310 def __exit__(self, exc_type, exc_value, traceback):
311 self.refs -= 1
312 if self.refs == 0 and self.rulesfile:
313 os.remove(self.rulesfile.name)
314 self.reload_udev_rules()
315
316 def reload_udev_rules(self):
317 import subprocess
318
319 subprocess.run("udevadm control --reload-rules".split())
320 subprocess.run("systemd-hwdb update".split())
321
322 def create_udev_rule(self):
323 import tempfile
324
325 os.makedirs("/run/udev/rules.d", exist_ok=True)
326 with tempfile.NamedTemporaryFile(
327 prefix="91-uhid-test-device-REMOVEME-",
328 suffix=".rules",
329 mode="w+",
330 dir="/run/udev/rules.d",
331 delete=False,
332 ) as f:
333 f.write(
334 'KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"\n'
335 )
336 f.write(
337 'KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"\n'
338 )
339 self.rulesfile = f
340
341 @classmethod
342 def instance(cls):
343 if not cls._instance:
344 cls._instance = HIDTestUdevRule()
345 return cls._instance