Skip to content

Commit ffb85d5

Browse files
committed
selftests: hid: import hid-tools hid-core tests
These tests have been developed in the hid-tools[0] tree for a while. Now that we have a proper selftests/hid kernel entry and that the tests are more reliable, it is time to directly include those in the kernel tree. I haven't imported all of hid-tools, the python module, but only the tests related to the kernel. We can rely on pip to fetch the latest hid-tools release, and then run the tests directly from the tree. This should now be easier to request tests when something is not behaving properly in the HID subsystem. [0] https://gitlab.freedesktop.org/libevdev/hid-tools Cc: Peter Hutterer <[email protected]> Signed-off-by: Peter Hutterer <[email protected]> Signed-off-by: Benjamin Tissoires <[email protected]>
1 parent 7d0b3f1 commit ffb85d5

File tree

8 files changed

+620
-1
lines changed

8 files changed

+620
-1
lines changed

tools/testing/selftests/hid/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ include ../../../build/Build.include
55
include ../../../scripts/Makefile.arch
66
include ../../../scripts/Makefile.include
77

8+
TEST_PROGS := hid-core.sh
9+
810
CXX ?= $(CROSS_COMPILE)g++
911

1012
HOSTPKG_CONFIG := pkg-config
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
# SPDX-License-Identifier: GPL-2.0
3+
# Runs tests for the HID subsystem
4+
5+
export TARGET=test_hid_core.py
6+
7+
bash ./run-hid-tools-tests.sh
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/sh
2+
# SPDX-License-Identifier: GPL-2.0
3+
# Runs tests for the HID subsystem
4+
5+
if ! command -v python3 > /dev/null 2>&1; then
6+
echo "hid-tools: [SKIP] python3 not installed"
7+
exit 77
8+
fi
9+
10+
if ! python3 -c "import pytest" > /dev/null 2>&1; then
11+
echo "hid: [SKIP/ pytest module not installed"
12+
exit 77
13+
fi
14+
15+
if ! python3 -c "import pytest_tap" > /dev/null 2>&1; then
16+
echo "hid: [SKIP/ pytest_tap module not installed"
17+
exit 77
18+
fi
19+
20+
if ! python3 -c "import hidtools" > /dev/null 2>&1; then
21+
echo "hid: [SKIP/ hid-tools module not installed"
22+
exit 77
23+
fi
24+
25+
TARGET=${TARGET:=.}
26+
27+
echo TAP version 13
28+
python3 -u -m pytest $PYTEST_XDIST ./tests/$TARGET --tap-stream --udevd
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
# Just to make sphinx-apidoc document this directory
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
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

0 commit comments

Comments
 (0)