2 # SPDX-License-Identifier: GPL-2.0
3 # -*- coding: utf-8 -*-
5 # Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>
6 # Copyright (c) 2017 Red Hat, Inc.
17 from .base_device
import BaseDevice
, EvdevMatch
, SysfsFile
18 from pathlib
import Path
19 from typing
import Final
, List
, Tuple
21 logger
= logging
.getLogger("hidtools.test.base")
23 # application to matches
24 application_matches
: Final
= {
26 "Accelerometer": EvdevMatch(
28 libevdev
.INPUT_PROP_ACCELEROMETER
,
31 "Game Pad": EvdevMatch( # in systemd, this is a lot more complex, but that will do
33 libevdev
.EV_ABS
.ABS_X
,
34 libevdev
.EV_ABS
.ABS_Y
,
35 libevdev
.EV_ABS
.ABS_RX
,
36 libevdev
.EV_ABS
.ABS_RY
,
37 libevdev
.EV_KEY
.BTN_START
,
40 libevdev
.INPUT_PROP_ACCELEROMETER
,
43 "Joystick": EvdevMatch( # in systemd, this is a lot more complex, but that will do
45 libevdev
.EV_ABS
.ABS_RX
,
46 libevdev
.EV_ABS
.ABS_RY
,
47 libevdev
.EV_KEY
.BTN_START
,
50 libevdev
.INPUT_PROP_ACCELEROMETER
,
55 libevdev
.EV_KEY
.KEY_A
,
58 libevdev
.INPUT_PROP_ACCELEROMETER
,
59 libevdev
.INPUT_PROP_DIRECT
,
60 libevdev
.INPUT_PROP_POINTER
,
65 libevdev
.EV_REL
.REL_X
,
66 libevdev
.EV_REL
.REL_Y
,
67 libevdev
.EV_KEY
.BTN_LEFT
,
70 libevdev
.INPUT_PROP_ACCELEROMETER
,
75 libevdev
.EV_KEY
.BTN_0
,
78 libevdev
.EV_KEY
.BTN_TOOL_PEN
,
79 libevdev
.EV_KEY
.BTN_TOUCH
,
80 libevdev
.EV_ABS
.ABS_DISTANCE
,
83 libevdev
.INPUT_PROP_ACCELEROMETER
,
88 libevdev
.EV_KEY
.BTN_STYLUS
,
89 libevdev
.EV_ABS
.ABS_X
,
90 libevdev
.EV_ABS
.ABS_Y
,
93 libevdev
.INPUT_PROP_ACCELEROMETER
,
98 libevdev
.EV_KEY
.BTN_STYLUS
,
99 libevdev
.EV_ABS
.ABS_X
,
100 libevdev
.EV_ABS
.ABS_Y
,
103 libevdev
.INPUT_PROP_ACCELEROMETER
,
106 "Touch Pad": EvdevMatch(
108 libevdev
.EV_KEY
.BTN_LEFT
,
109 libevdev
.EV_ABS
.ABS_X
,
110 libevdev
.EV_ABS
.ABS_Y
,
112 excludes
=[libevdev
.EV_KEY
.BTN_TOOL_PEN
, libevdev
.EV_KEY
.BTN_STYLUS
],
114 libevdev
.INPUT_PROP_POINTER
,
117 libevdev
.INPUT_PROP_ACCELEROMETER
,
120 "Touch Screen": EvdevMatch(
122 libevdev
.EV_KEY
.BTN_TOUCH
,
123 libevdev
.EV_ABS
.ABS_X
,
124 libevdev
.EV_ABS
.ABS_Y
,
126 excludes
=[libevdev
.EV_KEY
.BTN_TOOL_PEN
, libevdev
.EV_KEY
.BTN_STYLUS
],
128 libevdev
.INPUT_PROP_DIRECT
,
131 libevdev
.INPUT_PROP_ACCELEROMETER
,
137 class UHIDTestDevice(BaseDevice
):
138 def __init__(self
, name
, application
, rdesc_str
=None, rdesc
=None, input_info
=None):
139 super().__init
__(name
, application
, rdesc_str
, rdesc
, input_info
)
140 self
.application_matches
= application_matches
142 name
= f
"uhid test {self.__class__.__name__}"
143 if not name
.startswith("uhid test "):
144 name
= "uhid test " + self
.name
149 class TestUhid(object):
150 syn_event
= libevdev
.InputEvent(libevdev
.EV_SYN
.SYN_REPORT
) # type: ignore
151 key_event
= libevdev
.InputEvent(libevdev
.EV_KEY
) # type: ignore
152 abs_event
= libevdev
.InputEvent(libevdev
.EV_ABS
) # type: ignore
153 rel_event
= libevdev
.InputEvent(libevdev
.EV_REL
) # type: ignore
154 msc_event
= libevdev
.InputEvent(libevdev
.EV_MSC
.MSC_SCAN
) # type: ignore
156 # List of kernel modules to load before starting the test
157 # if any module is not available (not compiled), the test will skip.
158 # Each element is a tuple '(kernel driver name, kernel module)',
159 # for example ("playstation", "hid-playstation")
160 kernel_modules
: List
[Tuple
[str, str]] = []
162 # List of in kernel HID-BPF object files to load
163 # before starting the test
164 # Any existing pre-loaded HID-BPF module will be removed
165 # before the ones in this list will be manually loaded.
166 # Each Element is a tuple '(hid_bpf_object, rdesc_fixup_present)',
167 # for example '("xppen-ArtistPro16Gen2.bpf.o", True)'
168 # If 'rdesc_fixup_present' is True, the test needs to wait
169 # for one unbind and rebind before it can be sure the kernel is
171 hid_bpfs
: List
[Tuple
[str, bool]] = []
173 def assertInputEventsIn(self
, expected_events
, effective_events
):
174 effective_events
= effective_events
.copy()
175 for ev
in expected_events
:
176 assert ev
in effective_events
177 effective_events
.remove(ev
)
178 return effective_events
180 def assertInputEvents(self
, expected_events
, effective_events
):
181 remaining
= self
.assertInputEventsIn(expected_events
, effective_events
)
182 assert remaining
== []
185 def debug_reports(cls
, reports
, uhdev
=None, events
=None):
186 data
= [" ".join([f
"{v:02x}" for v
in r
]) for r
in reports
]
188 if uhdev
is not None:
190 uhdev
.parsed_rdesc
.format_report(r
, split_lines
=True)
195 f
'\n\t {" " * h.index("/")}'.join(h
.split("\n"))
199 # '/' not found: not a numbered report
200 human_data
= ["\n\t ".join(h
.split("\n")) for h
in human_data
]
201 data
= [f
"{d}\n\t ====> {h}" for d
, h
in zip(data
, human_data
)]
205 if len(reports
) == 1:
206 print("sending 1 report:")
208 print(f
"sending {len(reports)} reports:")
209 for report
in reports
:
212 if events
is not None:
213 print("events received:", events
)
215 def create_device(self
):
216 raise Exception("please reimplement me in subclasses")
218 def _load_kernel_module(self
, kernel_driver
, kernel_module
):
219 sysfs_path
= Path("/sys/bus/hid/drivers")
220 if kernel_driver
is not None:
221 sysfs_path
/= kernel_driver
223 # special case for when testing all available modules:
224 # we don't know beforehand the name of the module from modinfo
225 sysfs_path
= Path("/sys/module") / kernel_module
.replace("-", "_")
226 if not sysfs_path
.exists():
227 ret
= subprocess
.run(["/usr/sbin/modprobe", kernel_module
])
228 if ret
.returncode
!= 0:
230 f
"module {kernel_module} could not be loaded, skipping the test"
234 def load_kernel_module(self
):
235 for kernel_driver
, kernel_module
in self
.kernel_modules
:
236 self
._load
_kernel
_module
(kernel_driver
, kernel_module
)
239 def load_hid_bpfs(self
):
240 script_dir
= Path(os
.path
.dirname(os
.path
.realpath(__file__
)))
241 root_dir
= (script_dir
/ "../../../../..").resolve()
242 bpf_dir
= root_dir
/ "drivers/hid/bpf/progs"
244 udev_hid_bpf
= shutil
.which("udev-hid-bpf")
246 pytest
.skip("udev-hid-bpf not found in $PATH, skipping")
249 for _
, rdesc_fixup
in self
.hid_bpfs
:
253 for hid_bpf
, _
in self
.hid_bpfs
:
254 # We need to start `udev-hid-bpf` in the background
255 # and dispatch uhid events in case the kernel needs
256 # to fetch features on the device
257 process
= subprocess
.Popen(
262 str(self
.uhdev
.sys_path
),
263 str(bpf_dir
/ hid_bpf
),
266 while process
.poll() is None:
267 self
.uhdev
.dispatch(1)
269 if process
.poll() != 0:
271 f
"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed"
275 # the HID-BPF program exports a rdesc fixup, so it needs to be
276 # unbound by the kernel and then rebound.
277 # Ensure we get the bound event exactly 2 times (one for the normal
278 # uhid loading, and then the reload from HID-BPF)
280 while self
.uhdev
.kernel_ready_count
< 2 and time
.time() - now
< 2:
281 self
.uhdev
.dispatch(1)
283 if self
.uhdev
.kernel_ready_count
< 2:
285 f
"Couldn't insert hid-bpf programs, marking the test as failed"
288 def unload_hid_bpfs(self
):
289 ret
= subprocess
.run(
290 ["udev-hid-bpf", "--verbose", "remove", str(self
.uhdev
.sys_path
)],
292 if ret
.returncode
!= 0:
294 f
"Couldn't unload hid-bpf programs, marking the test as failed"
298 def new_uhdev(self
, load_kernel_module
):
299 return self
.create_device()
301 def assertName(self
, uhdev
):
302 evdev
= uhdev
.get_evdev()
303 assert uhdev
.name
in evdev
.name
305 @pytest.fixture(autouse
=True)
306 def context(self
, new_uhdev
, request
):
308 with HIDTestUdevRule
.instance():
309 with new_uhdev
as self
.uhdev
:
310 for skip_cond
in request
.node
.iter_markers("skip_if_uhdev"):
311 test
, message
, *rest
= skip_cond
.args
316 self
.uhdev
.create_kernel_device()
318 while not self
.uhdev
.is_ready() and time
.time() - now
< 5:
319 self
.uhdev
.dispatch(1)
324 if self
.uhdev
.get_evdev() is None:
326 f
"available list of input nodes: (default application is '{self.uhdev.application}')"
328 logger
.warning(self
.uhdev
.input_nodes
)
331 self
.unload_hid_bpfs()
333 except PermissionError
:
334 pytest
.skip("Insufficient permissions, run me as root")
336 @pytest.fixture(autouse
=True)
337 def check_taint(self
):
338 # we are abusing SysfsFile here, it's in /proc, but meh
339 taint_file
= SysfsFile("/proc/sys/kernel/tainted")
340 taint
= taint_file
.int_value
344 assert taint_file
.int_value
== taint
346 def test_creation(self
):
347 """Make sure the device gets processed by the kernel and creates
348 the expected application input node.
350 If this fail, there is something wrong in the device report
353 assert uhdev
is not None
354 assert uhdev
.get_evdev() is not None
355 self
.assertName(uhdev
)
356 assert len(uhdev
.next_sync_events()) == 0
357 assert uhdev
.get_evdev() is not None
360 class HIDTestUdevRule(object):
363 A context-manager compatible class that sets up our udev rules file and
364 deletes it on context exit.
366 This class is tailored to our test setup: it only sets up the udev rule
367 on the **second** context and it cleans it up again on the last context
368 removed. This matches the expected pytest setup: we enter a context for
369 the session once, then once for each test (the first of which will
370 trigger the udev rule) and once the last test exited and the session
371 exited, we clean up after ourselves.
376 self
.rulesfile
= None
380 if self
.refs
== 2 and self
.rulesfile
is None:
381 self
.create_udev_rule()
382 self
.reload_udev_rules()
384 def __exit__(self
, exc_type
, exc_value
, traceback
):
386 if self
.refs
== 0 and self
.rulesfile
:
387 os
.remove(self
.rulesfile
.name
)
388 self
.reload_udev_rules()
390 def reload_udev_rules(self
):
391 subprocess
.run("udevadm control --reload-rules".split())
392 subprocess
.run("systemd-hwdb update".split())
394 def create_udev_rule(self
):
397 os
.makedirs("/run/udev/rules.d", exist_ok
=True)
398 with tempfile
.NamedTemporaryFile(
399 prefix
="91-uhid-test-device-REMOVEME-",
402 dir="/run/udev/rules.d",
407 KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"
408 KERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1"
409 KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"
416 if not cls
._instance
:
417 cls
._instance
= HIDTestUdevRule()