]> Git Repo - J-linux.git/blob - tools/testing/selftests/hid/tests/base.py
Merge tag 'amd-drm-next-6.5-2023-06-09' of https://gitlab.freedesktop.org/agd5f/linux...
[J-linux.git] / tools / testing / selftests / hid / tests / base.py
1 #!/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8 -*-
4 #
5 # Copyright (c) 2017 Benjamin Tissoires <[email protected]>
6 # Copyright (c) 2017 Red Hat, Inc.
7
8 import libevdev
9 import os
10 import pytest
11 import time
12
13 import logging
14
15 from hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile
16 from pathlib import Path
17 from typing import Final
18
19 logger = logging.getLogger("hidtools.test.base")
20
21 # application to matches
22 application_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
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
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
146 class 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
286 class 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
This page took 0.051759 seconds and 4 git commands to generate.