Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
torvalds
GitHub Repository: torvalds/linux
Path: blob/master/tools/testing/selftests/hid/tests/base.py
26308 views
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 dataclasses
9
import libevdev
10
import os
11
import pytest
12
import shutil
13
import subprocess
14
import time
15
16
import logging
17
18
from .base_device import BaseDevice, EvdevMatch, SysfsFile
19
from pathlib import Path
20
from typing import Final, List, Tuple
21
22
logger = logging.getLogger("hidtools.test.base")
23
24
# application to matches
25
application_matches: Final = {
26
# pyright: ignore
27
"Accelerometer": EvdevMatch(
28
req_properties=[
29
libevdev.INPUT_PROP_ACCELEROMETER,
30
]
31
),
32
"Game Pad": EvdevMatch( # in systemd, this is a lot more complex, but that will do
33
requires=[
34
libevdev.EV_ABS.ABS_X,
35
libevdev.EV_ABS.ABS_Y,
36
libevdev.EV_ABS.ABS_RX,
37
libevdev.EV_ABS.ABS_RY,
38
libevdev.EV_KEY.BTN_START,
39
],
40
excl_properties=[
41
libevdev.INPUT_PROP_ACCELEROMETER,
42
],
43
),
44
"Joystick": EvdevMatch( # in systemd, this is a lot more complex, but that will do
45
requires=[
46
libevdev.EV_ABS.ABS_RX,
47
libevdev.EV_ABS.ABS_RY,
48
libevdev.EV_KEY.BTN_START,
49
],
50
excl_properties=[
51
libevdev.INPUT_PROP_ACCELEROMETER,
52
],
53
),
54
"Key": EvdevMatch(
55
requires=[
56
libevdev.EV_KEY.KEY_A,
57
],
58
excl_properties=[
59
libevdev.INPUT_PROP_ACCELEROMETER,
60
libevdev.INPUT_PROP_DIRECT,
61
libevdev.INPUT_PROP_POINTER,
62
],
63
),
64
"Mouse": EvdevMatch(
65
requires=[
66
libevdev.EV_REL.REL_X,
67
libevdev.EV_REL.REL_Y,
68
libevdev.EV_KEY.BTN_LEFT,
69
],
70
excl_properties=[
71
libevdev.INPUT_PROP_ACCELEROMETER,
72
],
73
),
74
"Pad": EvdevMatch(
75
requires=[
76
libevdev.EV_KEY.BTN_0,
77
],
78
excludes=[
79
libevdev.EV_KEY.BTN_TOOL_PEN,
80
libevdev.EV_KEY.BTN_TOUCH,
81
libevdev.EV_ABS.ABS_DISTANCE,
82
],
83
excl_properties=[
84
libevdev.INPUT_PROP_ACCELEROMETER,
85
],
86
),
87
"Pen": EvdevMatch(
88
requires=[
89
libevdev.EV_KEY.BTN_STYLUS,
90
libevdev.EV_ABS.ABS_X,
91
libevdev.EV_ABS.ABS_Y,
92
],
93
excl_properties=[
94
libevdev.INPUT_PROP_ACCELEROMETER,
95
],
96
),
97
"Stylus": EvdevMatch(
98
requires=[
99
libevdev.EV_KEY.BTN_STYLUS,
100
libevdev.EV_ABS.ABS_X,
101
libevdev.EV_ABS.ABS_Y,
102
],
103
excl_properties=[
104
libevdev.INPUT_PROP_ACCELEROMETER,
105
],
106
),
107
"Touch Pad": EvdevMatch(
108
requires=[
109
libevdev.EV_KEY.BTN_LEFT,
110
libevdev.EV_ABS.ABS_X,
111
libevdev.EV_ABS.ABS_Y,
112
],
113
excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
114
req_properties=[
115
libevdev.INPUT_PROP_POINTER,
116
],
117
excl_properties=[
118
libevdev.INPUT_PROP_ACCELEROMETER,
119
],
120
),
121
"Touch Screen": EvdevMatch(
122
requires=[
123
libevdev.EV_KEY.BTN_TOUCH,
124
libevdev.EV_ABS.ABS_X,
125
libevdev.EV_ABS.ABS_Y,
126
],
127
excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
128
req_properties=[
129
libevdev.INPUT_PROP_DIRECT,
130
],
131
excl_properties=[
132
libevdev.INPUT_PROP_ACCELEROMETER,
133
],
134
),
135
}
136
137
138
class UHIDTestDevice(BaseDevice):
139
def __init__(self, name, application, rdesc_str=None, rdesc=None, input_info=None):
140
super().__init__(name, application, rdesc_str, rdesc, input_info)
141
self.application_matches = application_matches
142
if name is None:
143
name = f"uhid test {self.__class__.__name__}"
144
if not name.startswith("uhid test "):
145
name = "uhid test " + self.name
146
self.name = name
147
148
149
@dataclasses.dataclass
150
class HidBpf:
151
object_name: str
152
has_rdesc_fixup: bool
153
154
155
@dataclasses.dataclass
156
class KernelModule:
157
driver_name: str
158
module_name: str
159
160
161
class BaseTestCase:
162
class TestUhid(object):
163
syn_event = libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT) # type: ignore
164
key_event = libevdev.InputEvent(libevdev.EV_KEY) # type: ignore
165
abs_event = libevdev.InputEvent(libevdev.EV_ABS) # type: ignore
166
rel_event = libevdev.InputEvent(libevdev.EV_REL) # type: ignore
167
msc_event = libevdev.InputEvent(libevdev.EV_MSC.MSC_SCAN) # type: ignore
168
169
# List of kernel modules to load before starting the test
170
# if any module is not available (not compiled), the test will skip.
171
# Each element is a KernelModule object, for example
172
# KernelModule("playstation", "hid-playstation")
173
kernel_modules: List[KernelModule] = []
174
175
# List of in kernel HID-BPF object files to load
176
# before starting the test
177
# Any existing pre-loaded HID-BPF module will be removed
178
# before the ones in this list will be manually loaded.
179
# Each Element is a HidBpf object, for example
180
# 'HidBpf("xppen-ArtistPro16Gen2.bpf.o", True)'
181
# If 'has_rdesc_fixup' is True, the test needs to wait
182
# for one unbind and rebind before it can be sure the kernel is
183
# ready
184
hid_bpfs: List[HidBpf] = []
185
186
def assertInputEventsIn(self, expected_events, effective_events):
187
effective_events = effective_events.copy()
188
for ev in expected_events:
189
assert ev in effective_events
190
effective_events.remove(ev)
191
return effective_events
192
193
def assertInputEvents(self, expected_events, effective_events):
194
remaining = self.assertInputEventsIn(expected_events, effective_events)
195
assert remaining == []
196
197
@classmethod
198
def debug_reports(cls, reports, uhdev=None, events=None):
199
data = [" ".join([f"{v:02x}" for v in r]) for r in reports]
200
201
if uhdev is not None:
202
human_data = [
203
uhdev.parsed_rdesc.format_report(r, split_lines=True)
204
for r in reports
205
]
206
try:
207
human_data = [
208
f'\n\t {" " * h.index("/")}'.join(h.split("\n"))
209
for h in human_data
210
]
211
except ValueError:
212
# '/' not found: not a numbered report
213
human_data = ["\n\t ".join(h.split("\n")) for h in human_data]
214
data = [f"{d}\n\t ====> {h}" for d, h in zip(data, human_data)]
215
216
reports = data
217
218
if len(reports) == 1:
219
print("sending 1 report:")
220
else:
221
print(f"sending {len(reports)} reports:")
222
for report in reports:
223
print("\t", report)
224
225
if events is not None:
226
print("events received:", events)
227
228
def create_device(self):
229
raise Exception("please reimplement me in subclasses")
230
231
def _load_kernel_module(self, kernel_driver, kernel_module):
232
sysfs_path = Path("/sys/bus/hid/drivers")
233
if kernel_driver is not None:
234
sysfs_path /= kernel_driver
235
else:
236
# special case for when testing all available modules:
237
# we don't know beforehand the name of the module from modinfo
238
sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_")
239
if not sysfs_path.exists():
240
ret = subprocess.run(["/usr/sbin/modprobe", kernel_module])
241
if ret.returncode != 0:
242
pytest.skip(
243
f"module {kernel_module} could not be loaded, skipping the test"
244
)
245
246
@pytest.fixture()
247
def load_kernel_module(self):
248
for k in self.kernel_modules:
249
self._load_kernel_module(k.driver_name, k.module_name)
250
yield
251
252
def load_hid_bpfs(self):
253
# this function will only work when run in the kernel tree
254
script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
255
root_dir = (script_dir / "../../../../..").resolve()
256
bpf_dir = root_dir / "drivers/hid/bpf/progs"
257
258
if not bpf_dir.exists():
259
pytest.skip("looks like we are not in the kernel tree, skipping")
260
261
udev_hid_bpf = shutil.which("udev-hid-bpf")
262
if not udev_hid_bpf:
263
pytest.skip("udev-hid-bpf not found in $PATH, skipping")
264
265
wait = any(b.has_rdesc_fixup for b in self.hid_bpfs)
266
267
for hid_bpf in self.hid_bpfs:
268
# We need to start `udev-hid-bpf` in the background
269
# and dispatch uhid events in case the kernel needs
270
# to fetch features on the device
271
process = subprocess.Popen(
272
[
273
"udev-hid-bpf",
274
"--verbose",
275
"add",
276
str(self.uhdev.sys_path),
277
str(bpf_dir / hid_bpf.object_name),
278
],
279
)
280
while process.poll() is None:
281
self.uhdev.dispatch(1)
282
283
if process.returncode != 0:
284
pytest.fail(
285
f"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed"
286
)
287
288
if wait:
289
# the HID-BPF program exports a rdesc fixup, so it needs to be
290
# unbound by the kernel and then rebound.
291
# Ensure we get the bound event exactly 2 times (one for the normal
292
# uhid loading, and then the reload from HID-BPF)
293
now = time.time()
294
while self.uhdev.kernel_ready_count < 2 and time.time() - now < 2:
295
self.uhdev.dispatch(1)
296
297
if self.uhdev.kernel_ready_count < 2:
298
pytest.fail(
299
f"Couldn't insert hid-bpf programs, marking the test as failed"
300
)
301
302
def unload_hid_bpfs(self):
303
ret = subprocess.run(
304
["udev-hid-bpf", "--verbose", "remove", str(self.uhdev.sys_path)],
305
)
306
if ret.returncode != 0:
307
pytest.fail(
308
f"Couldn't unload hid-bpf programs, marking the test as failed"
309
)
310
311
@pytest.fixture()
312
def new_uhdev(self, load_kernel_module):
313
return self.create_device()
314
315
def assertName(self, uhdev):
316
evdev = uhdev.get_evdev()
317
assert uhdev.name in evdev.name
318
319
@pytest.fixture(autouse=True)
320
def context(self, new_uhdev, request):
321
try:
322
with HIDTestUdevRule.instance():
323
with new_uhdev as self.uhdev:
324
for skip_cond in request.node.iter_markers("skip_if_uhdev"):
325
test, message, *rest = skip_cond.args
326
327
if test(self.uhdev):
328
pytest.skip(message)
329
330
self.uhdev.create_kernel_device()
331
now = time.time()
332
while not self.uhdev.is_ready() and time.time() - now < 5:
333
self.uhdev.dispatch(1)
334
335
if self.hid_bpfs:
336
self.load_hid_bpfs()
337
338
if self.uhdev.get_evdev() is None:
339
logger.warning(
340
f"available list of input nodes: (default application is '{self.uhdev.application}')"
341
)
342
logger.warning(self.uhdev.input_nodes)
343
yield
344
if self.hid_bpfs:
345
self.unload_hid_bpfs()
346
self.uhdev = None
347
except PermissionError:
348
pytest.skip("Insufficient permissions, run me as root")
349
350
@pytest.fixture(autouse=True)
351
def check_taint(self):
352
# we are abusing SysfsFile here, it's in /proc, but meh
353
taint_file = SysfsFile("/proc/sys/kernel/tainted")
354
taint = taint_file.int_value
355
356
yield
357
358
assert taint_file.int_value == taint
359
360
def test_creation(self):
361
"""Make sure the device gets processed by the kernel and creates
362
the expected application input node.
363
364
If this fail, there is something wrong in the device report
365
descriptors."""
366
uhdev = self.uhdev
367
assert uhdev is not None
368
assert uhdev.get_evdev() is not None
369
self.assertName(uhdev)
370
assert len(uhdev.next_sync_events()) == 0
371
assert uhdev.get_evdev() is not None
372
373
374
class HIDTestUdevRule(object):
375
_instance = None
376
"""
377
A context-manager compatible class that sets up our udev rules file and
378
deletes it on context exit.
379
380
This class is tailored to our test setup: it only sets up the udev rule
381
on the **second** context and it cleans it up again on the last context
382
removed. This matches the expected pytest setup: we enter a context for
383
the session once, then once for each test (the first of which will
384
trigger the udev rule) and once the last test exited and the session
385
exited, we clean up after ourselves.
386
"""
387
388
def __init__(self):
389
self.refs = 0
390
self.rulesfile = None
391
392
def __enter__(self):
393
self.refs += 1
394
if self.refs == 2 and self.rulesfile is None:
395
self.create_udev_rule()
396
self.reload_udev_rules()
397
398
def __exit__(self, exc_type, exc_value, traceback):
399
self.refs -= 1
400
if self.refs == 0 and self.rulesfile:
401
os.remove(self.rulesfile.name)
402
self.reload_udev_rules()
403
404
def reload_udev_rules(self):
405
subprocess.run("udevadm control --reload-rules".split())
406
subprocess.run("systemd-hwdb update".split())
407
408
def create_udev_rule(self):
409
import tempfile
410
411
os.makedirs("/run/udev/rules.d", exist_ok=True)
412
with tempfile.NamedTemporaryFile(
413
prefix="91-uhid-test-device-REMOVEME-",
414
suffix=".rules",
415
mode="w+",
416
dir="/run/udev/rules.d",
417
delete=False,
418
) as f:
419
f.write(
420
"""
421
KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"
422
KERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1"
423
KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"
424
"""
425
)
426
self.rulesfile = f
427
428
@classmethod
429
def instance(cls):
430
if not cls._instance:
431
cls._instance = HIDTestUdevRule()
432
return cls._instance
433
434