Path: blob/master/tools/testing/selftests/hid/tests/base.py
26308 views
#!/bin/env python31# SPDX-License-Identifier: GPL-2.02# -*- coding: utf-8 -*-3#4# Copyright (c) 2017 Benjamin Tissoires <[email protected]>5# Copyright (c) 2017 Red Hat, Inc.67import dataclasses8import libevdev9import os10import pytest11import shutil12import subprocess13import time1415import logging1617from .base_device import BaseDevice, EvdevMatch, SysfsFile18from pathlib import Path19from typing import Final, List, Tuple2021logger = logging.getLogger("hidtools.test.base")2223# application to matches24application_matches: Final = {25# pyright: ignore26"Accelerometer": EvdevMatch(27req_properties=[28libevdev.INPUT_PROP_ACCELEROMETER,29]30),31"Game Pad": EvdevMatch( # in systemd, this is a lot more complex, but that will do32requires=[33libevdev.EV_ABS.ABS_X,34libevdev.EV_ABS.ABS_Y,35libevdev.EV_ABS.ABS_RX,36libevdev.EV_ABS.ABS_RY,37libevdev.EV_KEY.BTN_START,38],39excl_properties=[40libevdev.INPUT_PROP_ACCELEROMETER,41],42),43"Joystick": EvdevMatch( # in systemd, this is a lot more complex, but that will do44requires=[45libevdev.EV_ABS.ABS_RX,46libevdev.EV_ABS.ABS_RY,47libevdev.EV_KEY.BTN_START,48],49excl_properties=[50libevdev.INPUT_PROP_ACCELEROMETER,51],52),53"Key": EvdevMatch(54requires=[55libevdev.EV_KEY.KEY_A,56],57excl_properties=[58libevdev.INPUT_PROP_ACCELEROMETER,59libevdev.INPUT_PROP_DIRECT,60libevdev.INPUT_PROP_POINTER,61],62),63"Mouse": EvdevMatch(64requires=[65libevdev.EV_REL.REL_X,66libevdev.EV_REL.REL_Y,67libevdev.EV_KEY.BTN_LEFT,68],69excl_properties=[70libevdev.INPUT_PROP_ACCELEROMETER,71],72),73"Pad": EvdevMatch(74requires=[75libevdev.EV_KEY.BTN_0,76],77excludes=[78libevdev.EV_KEY.BTN_TOOL_PEN,79libevdev.EV_KEY.BTN_TOUCH,80libevdev.EV_ABS.ABS_DISTANCE,81],82excl_properties=[83libevdev.INPUT_PROP_ACCELEROMETER,84],85),86"Pen": EvdevMatch(87requires=[88libevdev.EV_KEY.BTN_STYLUS,89libevdev.EV_ABS.ABS_X,90libevdev.EV_ABS.ABS_Y,91],92excl_properties=[93libevdev.INPUT_PROP_ACCELEROMETER,94],95),96"Stylus": EvdevMatch(97requires=[98libevdev.EV_KEY.BTN_STYLUS,99libevdev.EV_ABS.ABS_X,100libevdev.EV_ABS.ABS_Y,101],102excl_properties=[103libevdev.INPUT_PROP_ACCELEROMETER,104],105),106"Touch Pad": EvdevMatch(107requires=[108libevdev.EV_KEY.BTN_LEFT,109libevdev.EV_ABS.ABS_X,110libevdev.EV_ABS.ABS_Y,111],112excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],113req_properties=[114libevdev.INPUT_PROP_POINTER,115],116excl_properties=[117libevdev.INPUT_PROP_ACCELEROMETER,118],119),120"Touch Screen": EvdevMatch(121requires=[122libevdev.EV_KEY.BTN_TOUCH,123libevdev.EV_ABS.ABS_X,124libevdev.EV_ABS.ABS_Y,125],126excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],127req_properties=[128libevdev.INPUT_PROP_DIRECT,129],130excl_properties=[131libevdev.INPUT_PROP_ACCELEROMETER,132],133),134}135136137class UHIDTestDevice(BaseDevice):138def __init__(self, name, application, rdesc_str=None, rdesc=None, input_info=None):139super().__init__(name, application, rdesc_str, rdesc, input_info)140self.application_matches = application_matches141if name is None:142name = f"uhid test {self.__class__.__name__}"143if not name.startswith("uhid test "):144name = "uhid test " + self.name145self.name = name146147148@dataclasses.dataclass149class HidBpf:150object_name: str151has_rdesc_fixup: bool152153154@dataclasses.dataclass155class KernelModule:156driver_name: str157module_name: str158159160class BaseTestCase:161class TestUhid(object):162syn_event = libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT) # type: ignore163key_event = libevdev.InputEvent(libevdev.EV_KEY) # type: ignore164abs_event = libevdev.InputEvent(libevdev.EV_ABS) # type: ignore165rel_event = libevdev.InputEvent(libevdev.EV_REL) # type: ignore166msc_event = libevdev.InputEvent(libevdev.EV_MSC.MSC_SCAN) # type: ignore167168# List of kernel modules to load before starting the test169# if any module is not available (not compiled), the test will skip.170# Each element is a KernelModule object, for example171# KernelModule("playstation", "hid-playstation")172kernel_modules: List[KernelModule] = []173174# List of in kernel HID-BPF object files to load175# before starting the test176# Any existing pre-loaded HID-BPF module will be removed177# before the ones in this list will be manually loaded.178# Each Element is a HidBpf object, for example179# 'HidBpf("xppen-ArtistPro16Gen2.bpf.o", True)'180# If 'has_rdesc_fixup' is True, the test needs to wait181# for one unbind and rebind before it can be sure the kernel is182# ready183hid_bpfs: List[HidBpf] = []184185def assertInputEventsIn(self, expected_events, effective_events):186effective_events = effective_events.copy()187for ev in expected_events:188assert ev in effective_events189effective_events.remove(ev)190return effective_events191192def assertInputEvents(self, expected_events, effective_events):193remaining = self.assertInputEventsIn(expected_events, effective_events)194assert remaining == []195196@classmethod197def debug_reports(cls, reports, uhdev=None, events=None):198data = [" ".join([f"{v:02x}" for v in r]) for r in reports]199200if uhdev is not None:201human_data = [202uhdev.parsed_rdesc.format_report(r, split_lines=True)203for r in reports204]205try:206human_data = [207f'\n\t {" " * h.index("/")}'.join(h.split("\n"))208for h in human_data209]210except ValueError:211# '/' not found: not a numbered report212human_data = ["\n\t ".join(h.split("\n")) for h in human_data]213data = [f"{d}\n\t ====> {h}" for d, h in zip(data, human_data)]214215reports = data216217if len(reports) == 1:218print("sending 1 report:")219else:220print(f"sending {len(reports)} reports:")221for report in reports:222print("\t", report)223224if events is not None:225print("events received:", events)226227def create_device(self):228raise Exception("please reimplement me in subclasses")229230def _load_kernel_module(self, kernel_driver, kernel_module):231sysfs_path = Path("/sys/bus/hid/drivers")232if kernel_driver is not None:233sysfs_path /= kernel_driver234else:235# special case for when testing all available modules:236# we don't know beforehand the name of the module from modinfo237sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_")238if not sysfs_path.exists():239ret = subprocess.run(["/usr/sbin/modprobe", kernel_module])240if ret.returncode != 0:241pytest.skip(242f"module {kernel_module} could not be loaded, skipping the test"243)244245@pytest.fixture()246def load_kernel_module(self):247for k in self.kernel_modules:248self._load_kernel_module(k.driver_name, k.module_name)249yield250251def load_hid_bpfs(self):252# this function will only work when run in the kernel tree253script_dir = Path(os.path.dirname(os.path.realpath(__file__)))254root_dir = (script_dir / "../../../../..").resolve()255bpf_dir = root_dir / "drivers/hid/bpf/progs"256257if not bpf_dir.exists():258pytest.skip("looks like we are not in the kernel tree, skipping")259260udev_hid_bpf = shutil.which("udev-hid-bpf")261if not udev_hid_bpf:262pytest.skip("udev-hid-bpf not found in $PATH, skipping")263264wait = any(b.has_rdesc_fixup for b in self.hid_bpfs)265266for hid_bpf in self.hid_bpfs:267# We need to start `udev-hid-bpf` in the background268# and dispatch uhid events in case the kernel needs269# to fetch features on the device270process = subprocess.Popen(271[272"udev-hid-bpf",273"--verbose",274"add",275str(self.uhdev.sys_path),276str(bpf_dir / hid_bpf.object_name),277],278)279while process.poll() is None:280self.uhdev.dispatch(1)281282if process.returncode != 0:283pytest.fail(284f"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed"285)286287if wait:288# the HID-BPF program exports a rdesc fixup, so it needs to be289# unbound by the kernel and then rebound.290# Ensure we get the bound event exactly 2 times (one for the normal291# uhid loading, and then the reload from HID-BPF)292now = time.time()293while self.uhdev.kernel_ready_count < 2 and time.time() - now < 2:294self.uhdev.dispatch(1)295296if self.uhdev.kernel_ready_count < 2:297pytest.fail(298f"Couldn't insert hid-bpf programs, marking the test as failed"299)300301def unload_hid_bpfs(self):302ret = subprocess.run(303["udev-hid-bpf", "--verbose", "remove", str(self.uhdev.sys_path)],304)305if ret.returncode != 0:306pytest.fail(307f"Couldn't unload hid-bpf programs, marking the test as failed"308)309310@pytest.fixture()311def new_uhdev(self, load_kernel_module):312return self.create_device()313314def assertName(self, uhdev):315evdev = uhdev.get_evdev()316assert uhdev.name in evdev.name317318@pytest.fixture(autouse=True)319def context(self, new_uhdev, request):320try:321with HIDTestUdevRule.instance():322with new_uhdev as self.uhdev:323for skip_cond in request.node.iter_markers("skip_if_uhdev"):324test, message, *rest = skip_cond.args325326if test(self.uhdev):327pytest.skip(message)328329self.uhdev.create_kernel_device()330now = time.time()331while not self.uhdev.is_ready() and time.time() - now < 5:332self.uhdev.dispatch(1)333334if self.hid_bpfs:335self.load_hid_bpfs()336337if self.uhdev.get_evdev() is None:338logger.warning(339f"available list of input nodes: (default application is '{self.uhdev.application}')"340)341logger.warning(self.uhdev.input_nodes)342yield343if self.hid_bpfs:344self.unload_hid_bpfs()345self.uhdev = None346except PermissionError:347pytest.skip("Insufficient permissions, run me as root")348349@pytest.fixture(autouse=True)350def check_taint(self):351# we are abusing SysfsFile here, it's in /proc, but meh352taint_file = SysfsFile("/proc/sys/kernel/tainted")353taint = taint_file.int_value354355yield356357assert taint_file.int_value == taint358359def test_creation(self):360"""Make sure the device gets processed by the kernel and creates361the expected application input node.362363If this fail, there is something wrong in the device report364descriptors."""365uhdev = self.uhdev366assert uhdev is not None367assert uhdev.get_evdev() is not None368self.assertName(uhdev)369assert len(uhdev.next_sync_events()) == 0370assert uhdev.get_evdev() is not None371372373class HIDTestUdevRule(object):374_instance = None375"""376A context-manager compatible class that sets up our udev rules file and377deletes it on context exit.378379This class is tailored to our test setup: it only sets up the udev rule380on the **second** context and it cleans it up again on the last context381removed. This matches the expected pytest setup: we enter a context for382the session once, then once for each test (the first of which will383trigger the udev rule) and once the last test exited and the session384exited, we clean up after ourselves.385"""386387def __init__(self):388self.refs = 0389self.rulesfile = None390391def __enter__(self):392self.refs += 1393if self.refs == 2 and self.rulesfile is None:394self.create_udev_rule()395self.reload_udev_rules()396397def __exit__(self, exc_type, exc_value, traceback):398self.refs -= 1399if self.refs == 0 and self.rulesfile:400os.remove(self.rulesfile.name)401self.reload_udev_rules()402403def reload_udev_rules(self):404subprocess.run("udevadm control --reload-rules".split())405subprocess.run("systemd-hwdb update".split())406407def create_udev_rule(self):408import tempfile409410os.makedirs("/run/udev/rules.d", exist_ok=True)411with tempfile.NamedTemporaryFile(412prefix="91-uhid-test-device-REMOVEME-",413suffix=".rules",414mode="w+",415dir="/run/udev/rules.d",416delete=False,417) as f:418f.write(419"""420KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"421KERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1"422KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"423"""424)425self.rulesfile = f426427@classmethod428def instance(cls):429if not cls._instance:430cls._instance = HIDTestUdevRule()431return cls._instance432433434