Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/hid: add support for HID-BPF pre-loading before starting a test

few required changes:
- we need to count how many times a udev 'bind' event happens
- we need to tell `udev-hid-bpf` to not automatically attach the
provided HID-BPF objects
- we need to manually attach the ones from the kernel tree, and wait
for the second udev 'bind' event to happen

Link: https://lore.kernel.org/r/20240410-bpf_sources-v1-11-a8bf16033ef8@kernel.org
Reviewed-by: Peter Hutterer <peter.hutterer@who-t.net>
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>

+93 -15
+77 -8
tools/testing/selftests/hid/tests/base.py
··· 8 8 import libevdev 9 9 import os 10 10 import pytest 11 + import subprocess 11 12 import time 12 13 13 14 import logging ··· 158 157 # for example ("playstation", "hid-playstation") 159 158 kernel_modules: List[Tuple[str, str]] = [] 160 159 160 + # List of in kernel HID-BPF object files to load 161 + # before starting the test 162 + # Any existing pre-loaded HID-BPF module will be removed 163 + # before the ones in this list will be manually loaded. 164 + # Each Element is a tuple '(hid_bpf_object, rdesc_fixup_present)', 165 + # for example '("xppen-ArtistPro16Gen2.bpf.o", True)' 166 + # If 'rdesc_fixup_present' is True, the test needs to wait 167 + # for one unbind and rebind before it can be sure the kernel is 168 + # ready 169 + hid_bpfs: List[Tuple[str, bool]] = [] 170 + 161 171 def assertInputEventsIn(self, expected_events, effective_events): 162 172 effective_events = effective_events.copy() 163 173 for ev in expected_events: ··· 223 211 # we don't know beforehand the name of the module from modinfo 224 212 sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_") 225 213 if not sysfs_path.exists(): 226 - import subprocess 227 - 228 214 ret = subprocess.run(["/usr/sbin/modprobe", kernel_module]) 229 215 if ret.returncode != 0: 230 216 pytest.skip( ··· 234 224 for kernel_driver, kernel_module in self.kernel_modules: 235 225 self._load_kernel_module(kernel_driver, kernel_module) 236 226 yield 227 + 228 + def load_hid_bpfs(self): 229 + script_dir = Path(os.path.dirname(os.path.realpath(__file__))) 230 + root_dir = (script_dir / "../../../../..").resolve() 231 + bpf_dir = root_dir / "drivers/hid/bpf/progs" 232 + 233 + wait = False 234 + for _, rdesc_fixup in self.hid_bpfs: 235 + if rdesc_fixup: 236 + wait = True 237 + 238 + for hid_bpf, _ in self.hid_bpfs: 239 + # We need to start `udev-hid-bpf` in the background 240 + # and dispatch uhid events in case the kernel needs 241 + # to fetch features on the device 242 + process = subprocess.Popen( 243 + [ 244 + "udev-hid-bpf", 245 + "--verbose", 246 + "add", 247 + str(self.uhdev.sys_path), 248 + str(bpf_dir / hid_bpf), 249 + ], 250 + ) 251 + while process.poll() is None: 252 + self.uhdev.dispatch(1) 253 + 254 + if process.poll() != 0: 255 + pytest.fail( 256 + f"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed" 257 + ) 258 + 259 + if wait: 260 + # the HID-BPF program exports a rdesc fixup, so it needs to be 261 + # unbound by the kernel and then rebound. 262 + # Ensure we get the bound event exactly 2 times (one for the normal 263 + # uhid loading, and then the reload from HID-BPF) 264 + now = time.time() 265 + while self.uhdev.kernel_ready_count < 2 and time.time() - now < 2: 266 + self.uhdev.dispatch(1) 267 + 268 + if self.uhdev.kernel_ready_count < 2: 269 + pytest.fail( 270 + f"Couldn't insert hid-bpf programs, marking the test as failed" 271 + ) 272 + 273 + def unload_hid_bpfs(self): 274 + ret = subprocess.run( 275 + ["udev-hid-bpf", "--verbose", "remove", str(self.uhdev.sys_path)], 276 + ) 277 + if ret.returncode != 0: 278 + pytest.fail( 279 + f"Couldn't unload hid-bpf programs, marking the test as failed" 280 + ) 237 281 238 282 @pytest.fixture() 239 283 def new_uhdev(self, load_kernel_module): ··· 312 248 now = time.time() 313 249 while not self.uhdev.is_ready() and time.time() - now < 5: 314 250 self.uhdev.dispatch(1) 251 + 252 + if self.hid_bpfs: 253 + self.load_hid_bpfs() 254 + 315 255 if self.uhdev.get_evdev() is None: 316 256 logger.warning( 317 257 f"available list of input nodes: (default application is '{self.uhdev.application}')" 318 258 ) 319 259 logger.warning(self.uhdev.input_nodes) 320 260 yield 261 + if self.hid_bpfs: 262 + self.unload_hid_bpfs() 321 263 self.uhdev = None 322 264 except PermissionError: 323 265 pytest.skip("Insufficient permissions, run me as root") ··· 383 313 self.reload_udev_rules() 384 314 385 315 def reload_udev_rules(self): 386 - import subprocess 387 - 388 316 subprocess.run("udevadm control --reload-rules".split()) 389 317 subprocess.run("systemd-hwdb update".split()) 390 318 ··· 398 330 delete=False, 399 331 ) as f: 400 332 f.write( 401 - 'KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"\n' 402 - ) 403 - f.write( 404 - 'KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"\n' 333 + """ 334 + KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1" 335 + KERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1" 336 + KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1" 337 + """ 405 338 ) 406 339 self.rulesfile = f 407 340
+16 -7
tools/testing/selftests/hid/tests/base_device.py
··· 35 35 from hidtools.util import BusType 36 36 37 37 from pathlib import Path 38 - from typing import Any, ClassVar, Dict, List, Optional, Type, Union 38 + from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union 39 39 40 40 logger = logging.getLogger("hidtools.device.base_device") 41 41 ··· 126 126 class UdevHIDIsReady(HIDIsReady): 127 127 _pyudev_context: ClassVar[Optional[pyudev.Context]] = None 128 128 _pyudev_monitor: ClassVar[Optional[pyudev.Monitor]] = None 129 - _uhid_devices: ClassVar[Dict[int, bool]] = {} 129 + _uhid_devices: ClassVar[Dict[int, Tuple[bool, int]]] = {} 130 130 131 131 def __init__(self: "UdevHIDIsReady", uhid: UHIDDevice) -> None: 132 132 super().__init__(uhid) ··· 150 150 return 151 151 event: pyudev.Device 152 152 for event in iter(functools.partial(cls._pyudev_monitor.poll, 0.02), None): 153 - if event.action not in ["bind", "remove"]: 153 + if event.action not in ["bind", "remove", "unbind"]: 154 154 return 155 155 156 156 logger.debug(f"udev event: {event.action} -> {event}") 157 157 158 158 id = int(event.sys_path.strip().split(".")[-1], 16) 159 159 160 - cls._uhid_devices[id] = event.action == "bind" 160 + device_ready, count = cls._uhid_devices.get(id, (False, 0)) 161 161 162 - def is_ready(self: "UdevHIDIsReady") -> bool: 162 + ready = event.action == "bind" 163 + if not device_ready and ready: 164 + count += 1 165 + cls._uhid_devices[id] = (ready, count) 166 + 167 + def is_ready(self: "UdevHIDIsReady") -> Tuple[bool, int]: 163 168 try: 164 169 return self._uhid_devices[self.uhid.hid_id] 165 170 except KeyError: 166 - return False 171 + return (False, 0) 167 172 168 173 169 174 class EvdevMatch(object): ··· 322 317 323 318 @property 324 319 def kernel_is_ready(self: "BaseDevice") -> bool: 325 - return self._kernel_is_ready.is_ready() and self.started 320 + return self._kernel_is_ready.is_ready()[0] and self.started 321 + 322 + @property 323 + def kernel_ready_count(self: "BaseDevice") -> int: 324 + return self._kernel_is_ready.is_ready()[1] 326 325 327 326 @property 328 327 def input_nodes(self: "BaseDevice") -> List[EvdevDevice]: