162306a36Sopenharmony_ci#!/bin/env python3 262306a36Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0 362306a36Sopenharmony_ci# -*- coding: utf-8 -*- 462306a36Sopenharmony_ci# 562306a36Sopenharmony_ci# Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com> 662306a36Sopenharmony_ci# Copyright (c) 2017 Red Hat, Inc. 762306a36Sopenharmony_ci 862306a36Sopenharmony_ciimport libevdev 962306a36Sopenharmony_ciimport os 1062306a36Sopenharmony_ciimport pytest 1162306a36Sopenharmony_ciimport time 1262306a36Sopenharmony_ci 1362306a36Sopenharmony_ciimport logging 1462306a36Sopenharmony_ci 1562306a36Sopenharmony_cifrom hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile 1662306a36Sopenharmony_cifrom pathlib import Path 1762306a36Sopenharmony_cifrom typing import Final 1862306a36Sopenharmony_ci 1962306a36Sopenharmony_cilogger = logging.getLogger("hidtools.test.base") 2062306a36Sopenharmony_ci 2162306a36Sopenharmony_ci# application to matches 2262306a36Sopenharmony_ciapplication_matches: Final = { 2362306a36Sopenharmony_ci # pyright: ignore 2462306a36Sopenharmony_ci "Accelerometer": EvdevMatch( 2562306a36Sopenharmony_ci req_properties=[ 2662306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 2762306a36Sopenharmony_ci ] 2862306a36Sopenharmony_ci ), 2962306a36Sopenharmony_ci "Game Pad": EvdevMatch( # in systemd, this is a lot more complex, but that will do 3062306a36Sopenharmony_ci requires=[ 3162306a36Sopenharmony_ci libevdev.EV_ABS.ABS_X, 3262306a36Sopenharmony_ci libevdev.EV_ABS.ABS_Y, 3362306a36Sopenharmony_ci libevdev.EV_ABS.ABS_RX, 3462306a36Sopenharmony_ci libevdev.EV_ABS.ABS_RY, 3562306a36Sopenharmony_ci libevdev.EV_KEY.BTN_START, 3662306a36Sopenharmony_ci ], 3762306a36Sopenharmony_ci excl_properties=[ 3862306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 3962306a36Sopenharmony_ci ], 4062306a36Sopenharmony_ci ), 4162306a36Sopenharmony_ci "Joystick": EvdevMatch( # in systemd, this is a lot more complex, but that will do 4262306a36Sopenharmony_ci requires=[ 4362306a36Sopenharmony_ci libevdev.EV_ABS.ABS_RX, 4462306a36Sopenharmony_ci libevdev.EV_ABS.ABS_RY, 4562306a36Sopenharmony_ci libevdev.EV_KEY.BTN_START, 4662306a36Sopenharmony_ci ], 4762306a36Sopenharmony_ci excl_properties=[ 4862306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 4962306a36Sopenharmony_ci ], 5062306a36Sopenharmony_ci ), 5162306a36Sopenharmony_ci "Key": EvdevMatch( 5262306a36Sopenharmony_ci requires=[ 5362306a36Sopenharmony_ci libevdev.EV_KEY.KEY_A, 5462306a36Sopenharmony_ci ], 5562306a36Sopenharmony_ci excl_properties=[ 5662306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 5762306a36Sopenharmony_ci libevdev.INPUT_PROP_DIRECT, 5862306a36Sopenharmony_ci libevdev.INPUT_PROP_POINTER, 5962306a36Sopenharmony_ci ], 6062306a36Sopenharmony_ci ), 6162306a36Sopenharmony_ci "Mouse": EvdevMatch( 6262306a36Sopenharmony_ci requires=[ 6362306a36Sopenharmony_ci libevdev.EV_REL.REL_X, 6462306a36Sopenharmony_ci libevdev.EV_REL.REL_Y, 6562306a36Sopenharmony_ci libevdev.EV_KEY.BTN_LEFT, 6662306a36Sopenharmony_ci ], 6762306a36Sopenharmony_ci excl_properties=[ 6862306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 6962306a36Sopenharmony_ci ], 7062306a36Sopenharmony_ci ), 7162306a36Sopenharmony_ci "Pad": EvdevMatch( 7262306a36Sopenharmony_ci requires=[ 7362306a36Sopenharmony_ci libevdev.EV_KEY.BTN_0, 7462306a36Sopenharmony_ci ], 7562306a36Sopenharmony_ci excludes=[ 7662306a36Sopenharmony_ci libevdev.EV_KEY.BTN_TOOL_PEN, 7762306a36Sopenharmony_ci libevdev.EV_KEY.BTN_TOUCH, 7862306a36Sopenharmony_ci libevdev.EV_ABS.ABS_DISTANCE, 7962306a36Sopenharmony_ci ], 8062306a36Sopenharmony_ci excl_properties=[ 8162306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 8262306a36Sopenharmony_ci ], 8362306a36Sopenharmony_ci ), 8462306a36Sopenharmony_ci "Pen": EvdevMatch( 8562306a36Sopenharmony_ci requires=[ 8662306a36Sopenharmony_ci libevdev.EV_KEY.BTN_STYLUS, 8762306a36Sopenharmony_ci libevdev.EV_ABS.ABS_X, 8862306a36Sopenharmony_ci libevdev.EV_ABS.ABS_Y, 8962306a36Sopenharmony_ci ], 9062306a36Sopenharmony_ci excl_properties=[ 9162306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 9262306a36Sopenharmony_ci ], 9362306a36Sopenharmony_ci ), 9462306a36Sopenharmony_ci "Stylus": EvdevMatch( 9562306a36Sopenharmony_ci requires=[ 9662306a36Sopenharmony_ci libevdev.EV_KEY.BTN_STYLUS, 9762306a36Sopenharmony_ci libevdev.EV_ABS.ABS_X, 9862306a36Sopenharmony_ci libevdev.EV_ABS.ABS_Y, 9962306a36Sopenharmony_ci ], 10062306a36Sopenharmony_ci excl_properties=[ 10162306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 10262306a36Sopenharmony_ci ], 10362306a36Sopenharmony_ci ), 10462306a36Sopenharmony_ci "Touch Pad": EvdevMatch( 10562306a36Sopenharmony_ci requires=[ 10662306a36Sopenharmony_ci libevdev.EV_KEY.BTN_LEFT, 10762306a36Sopenharmony_ci libevdev.EV_ABS.ABS_X, 10862306a36Sopenharmony_ci libevdev.EV_ABS.ABS_Y, 10962306a36Sopenharmony_ci ], 11062306a36Sopenharmony_ci excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS], 11162306a36Sopenharmony_ci req_properties=[ 11262306a36Sopenharmony_ci libevdev.INPUT_PROP_POINTER, 11362306a36Sopenharmony_ci ], 11462306a36Sopenharmony_ci excl_properties=[ 11562306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 11662306a36Sopenharmony_ci ], 11762306a36Sopenharmony_ci ), 11862306a36Sopenharmony_ci "Touch Screen": EvdevMatch( 11962306a36Sopenharmony_ci requires=[ 12062306a36Sopenharmony_ci libevdev.EV_KEY.BTN_TOUCH, 12162306a36Sopenharmony_ci libevdev.EV_ABS.ABS_X, 12262306a36Sopenharmony_ci libevdev.EV_ABS.ABS_Y, 12362306a36Sopenharmony_ci ], 12462306a36Sopenharmony_ci excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS], 12562306a36Sopenharmony_ci req_properties=[ 12662306a36Sopenharmony_ci libevdev.INPUT_PROP_DIRECT, 12762306a36Sopenharmony_ci ], 12862306a36Sopenharmony_ci excl_properties=[ 12962306a36Sopenharmony_ci libevdev.INPUT_PROP_ACCELEROMETER, 13062306a36Sopenharmony_ci ], 13162306a36Sopenharmony_ci ), 13262306a36Sopenharmony_ci} 13362306a36Sopenharmony_ci 13462306a36Sopenharmony_ci 13562306a36Sopenharmony_ciclass UHIDTestDevice(BaseDevice): 13662306a36Sopenharmony_ci def __init__(self, name, application, rdesc_str=None, rdesc=None, input_info=None): 13762306a36Sopenharmony_ci super().__init__(name, application, rdesc_str, rdesc, input_info) 13862306a36Sopenharmony_ci self.application_matches = application_matches 13962306a36Sopenharmony_ci if name is None: 14062306a36Sopenharmony_ci name = f"uhid test {self.__class__.__name__}" 14162306a36Sopenharmony_ci if not name.startswith("uhid test "): 14262306a36Sopenharmony_ci name = "uhid test " + self.name 14362306a36Sopenharmony_ci self.name = name 14462306a36Sopenharmony_ci 14562306a36Sopenharmony_ci 14662306a36Sopenharmony_ciclass BaseTestCase: 14762306a36Sopenharmony_ci class TestUhid(object): 14862306a36Sopenharmony_ci syn_event = libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT) # type: ignore 14962306a36Sopenharmony_ci key_event = libevdev.InputEvent(libevdev.EV_KEY) # type: ignore 15062306a36Sopenharmony_ci abs_event = libevdev.InputEvent(libevdev.EV_ABS) # type: ignore 15162306a36Sopenharmony_ci rel_event = libevdev.InputEvent(libevdev.EV_REL) # type: ignore 15262306a36Sopenharmony_ci msc_event = libevdev.InputEvent(libevdev.EV_MSC.MSC_SCAN) # type: ignore 15362306a36Sopenharmony_ci 15462306a36Sopenharmony_ci # List of kernel modules to load before starting the test 15562306a36Sopenharmony_ci # if any module is not available (not compiled), the test will skip. 15662306a36Sopenharmony_ci # Each element is a tuple '(kernel driver name, kernel module)', 15762306a36Sopenharmony_ci # for example ("playstation", "hid-playstation") 15862306a36Sopenharmony_ci kernel_modules = [] 15962306a36Sopenharmony_ci 16062306a36Sopenharmony_ci def assertInputEventsIn(self, expected_events, effective_events): 16162306a36Sopenharmony_ci effective_events = effective_events.copy() 16262306a36Sopenharmony_ci for ev in expected_events: 16362306a36Sopenharmony_ci assert ev in effective_events 16462306a36Sopenharmony_ci effective_events.remove(ev) 16562306a36Sopenharmony_ci return effective_events 16662306a36Sopenharmony_ci 16762306a36Sopenharmony_ci def assertInputEvents(self, expected_events, effective_events): 16862306a36Sopenharmony_ci remaining = self.assertInputEventsIn(expected_events, effective_events) 16962306a36Sopenharmony_ci assert remaining == [] 17062306a36Sopenharmony_ci 17162306a36Sopenharmony_ci @classmethod 17262306a36Sopenharmony_ci def debug_reports(cls, reports, uhdev=None, events=None): 17362306a36Sopenharmony_ci data = [" ".join([f"{v:02x}" for v in r]) for r in reports] 17462306a36Sopenharmony_ci 17562306a36Sopenharmony_ci if uhdev is not None: 17662306a36Sopenharmony_ci human_data = [ 17762306a36Sopenharmony_ci uhdev.parsed_rdesc.format_report(r, split_lines=True) 17862306a36Sopenharmony_ci for r in reports 17962306a36Sopenharmony_ci ] 18062306a36Sopenharmony_ci try: 18162306a36Sopenharmony_ci human_data = [ 18262306a36Sopenharmony_ci f'\n\t {" " * h.index("/")}'.join(h.split("\n")) 18362306a36Sopenharmony_ci for h in human_data 18462306a36Sopenharmony_ci ] 18562306a36Sopenharmony_ci except ValueError: 18662306a36Sopenharmony_ci # '/' not found: not a numbered report 18762306a36Sopenharmony_ci human_data = ["\n\t ".join(h.split("\n")) for h in human_data] 18862306a36Sopenharmony_ci data = [f"{d}\n\t ====> {h}" for d, h in zip(data, human_data)] 18962306a36Sopenharmony_ci 19062306a36Sopenharmony_ci reports = data 19162306a36Sopenharmony_ci 19262306a36Sopenharmony_ci if len(reports) == 1: 19362306a36Sopenharmony_ci print("sending 1 report:") 19462306a36Sopenharmony_ci else: 19562306a36Sopenharmony_ci print(f"sending {len(reports)} reports:") 19662306a36Sopenharmony_ci for report in reports: 19762306a36Sopenharmony_ci print("\t", report) 19862306a36Sopenharmony_ci 19962306a36Sopenharmony_ci if events is not None: 20062306a36Sopenharmony_ci print("events received:", events) 20162306a36Sopenharmony_ci 20262306a36Sopenharmony_ci def create_device(self): 20362306a36Sopenharmony_ci raise Exception("please reimplement me in subclasses") 20462306a36Sopenharmony_ci 20562306a36Sopenharmony_ci def _load_kernel_module(self, kernel_driver, kernel_module): 20662306a36Sopenharmony_ci sysfs_path = Path("/sys/bus/hid/drivers") 20762306a36Sopenharmony_ci if kernel_driver is not None: 20862306a36Sopenharmony_ci sysfs_path /= kernel_driver 20962306a36Sopenharmony_ci else: 21062306a36Sopenharmony_ci # special case for when testing all available modules: 21162306a36Sopenharmony_ci # we don't know beforehand the name of the module from modinfo 21262306a36Sopenharmony_ci sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_") 21362306a36Sopenharmony_ci if not sysfs_path.exists(): 21462306a36Sopenharmony_ci import subprocess 21562306a36Sopenharmony_ci 21662306a36Sopenharmony_ci ret = subprocess.run(["/usr/sbin/modprobe", kernel_module]) 21762306a36Sopenharmony_ci if ret.returncode != 0: 21862306a36Sopenharmony_ci pytest.skip( 21962306a36Sopenharmony_ci f"module {kernel_module} could not be loaded, skipping the test" 22062306a36Sopenharmony_ci ) 22162306a36Sopenharmony_ci 22262306a36Sopenharmony_ci @pytest.fixture() 22362306a36Sopenharmony_ci def load_kernel_module(self): 22462306a36Sopenharmony_ci for kernel_driver, kernel_module in self.kernel_modules: 22562306a36Sopenharmony_ci self._load_kernel_module(kernel_driver, kernel_module) 22662306a36Sopenharmony_ci yield 22762306a36Sopenharmony_ci 22862306a36Sopenharmony_ci @pytest.fixture() 22962306a36Sopenharmony_ci def new_uhdev(self, load_kernel_module): 23062306a36Sopenharmony_ci return self.create_device() 23162306a36Sopenharmony_ci 23262306a36Sopenharmony_ci def assertName(self, uhdev): 23362306a36Sopenharmony_ci evdev = uhdev.get_evdev() 23462306a36Sopenharmony_ci assert uhdev.name in evdev.name 23562306a36Sopenharmony_ci 23662306a36Sopenharmony_ci @pytest.fixture(autouse=True) 23762306a36Sopenharmony_ci def context(self, new_uhdev, request): 23862306a36Sopenharmony_ci try: 23962306a36Sopenharmony_ci with HIDTestUdevRule.instance(): 24062306a36Sopenharmony_ci with new_uhdev as self.uhdev: 24162306a36Sopenharmony_ci skip_cond = request.node.get_closest_marker("skip_if_uhdev") 24262306a36Sopenharmony_ci if skip_cond: 24362306a36Sopenharmony_ci test, message, *rest = skip_cond.args 24462306a36Sopenharmony_ci 24562306a36Sopenharmony_ci if test(self.uhdev): 24662306a36Sopenharmony_ci pytest.skip(message) 24762306a36Sopenharmony_ci 24862306a36Sopenharmony_ci self.uhdev.create_kernel_device() 24962306a36Sopenharmony_ci now = time.time() 25062306a36Sopenharmony_ci while not self.uhdev.is_ready() and time.time() - now < 5: 25162306a36Sopenharmony_ci self.uhdev.dispatch(1) 25262306a36Sopenharmony_ci if self.uhdev.get_evdev() is None: 25362306a36Sopenharmony_ci logger.warning( 25462306a36Sopenharmony_ci f"available list of input nodes: (default application is '{self.uhdev.application}')" 25562306a36Sopenharmony_ci ) 25662306a36Sopenharmony_ci logger.warning(self.uhdev.input_nodes) 25762306a36Sopenharmony_ci yield 25862306a36Sopenharmony_ci self.uhdev = None 25962306a36Sopenharmony_ci except PermissionError: 26062306a36Sopenharmony_ci pytest.skip("Insufficient permissions, run me as root") 26162306a36Sopenharmony_ci 26262306a36Sopenharmony_ci @pytest.fixture(autouse=True) 26362306a36Sopenharmony_ci def check_taint(self): 26462306a36Sopenharmony_ci # we are abusing SysfsFile here, it's in /proc, but meh 26562306a36Sopenharmony_ci taint_file = SysfsFile("/proc/sys/kernel/tainted") 26662306a36Sopenharmony_ci taint = taint_file.int_value 26762306a36Sopenharmony_ci 26862306a36Sopenharmony_ci yield 26962306a36Sopenharmony_ci 27062306a36Sopenharmony_ci assert taint_file.int_value == taint 27162306a36Sopenharmony_ci 27262306a36Sopenharmony_ci def test_creation(self): 27362306a36Sopenharmony_ci """Make sure the device gets processed by the kernel and creates 27462306a36Sopenharmony_ci the expected application input node. 27562306a36Sopenharmony_ci 27662306a36Sopenharmony_ci If this fail, there is something wrong in the device report 27762306a36Sopenharmony_ci descriptors.""" 27862306a36Sopenharmony_ci uhdev = self.uhdev 27962306a36Sopenharmony_ci assert uhdev is not None 28062306a36Sopenharmony_ci assert uhdev.get_evdev() is not None 28162306a36Sopenharmony_ci self.assertName(uhdev) 28262306a36Sopenharmony_ci assert len(uhdev.next_sync_events()) == 0 28362306a36Sopenharmony_ci assert uhdev.get_evdev() is not None 28462306a36Sopenharmony_ci 28562306a36Sopenharmony_ci 28662306a36Sopenharmony_ciclass HIDTestUdevRule(object): 28762306a36Sopenharmony_ci _instance = None 28862306a36Sopenharmony_ci """ 28962306a36Sopenharmony_ci A context-manager compatible class that sets up our udev rules file and 29062306a36Sopenharmony_ci deletes it on context exit. 29162306a36Sopenharmony_ci 29262306a36Sopenharmony_ci This class is tailored to our test setup: it only sets up the udev rule 29362306a36Sopenharmony_ci on the **second** context and it cleans it up again on the last context 29462306a36Sopenharmony_ci removed. This matches the expected pytest setup: we enter a context for 29562306a36Sopenharmony_ci the session once, then once for each test (the first of which will 29662306a36Sopenharmony_ci trigger the udev rule) and once the last test exited and the session 29762306a36Sopenharmony_ci exited, we clean up after ourselves. 29862306a36Sopenharmony_ci """ 29962306a36Sopenharmony_ci 30062306a36Sopenharmony_ci def __init__(self): 30162306a36Sopenharmony_ci self.refs = 0 30262306a36Sopenharmony_ci self.rulesfile = None 30362306a36Sopenharmony_ci 30462306a36Sopenharmony_ci def __enter__(self): 30562306a36Sopenharmony_ci self.refs += 1 30662306a36Sopenharmony_ci if self.refs == 2 and self.rulesfile is None: 30762306a36Sopenharmony_ci self.create_udev_rule() 30862306a36Sopenharmony_ci self.reload_udev_rules() 30962306a36Sopenharmony_ci 31062306a36Sopenharmony_ci def __exit__(self, exc_type, exc_value, traceback): 31162306a36Sopenharmony_ci self.refs -= 1 31262306a36Sopenharmony_ci if self.refs == 0 and self.rulesfile: 31362306a36Sopenharmony_ci os.remove(self.rulesfile.name) 31462306a36Sopenharmony_ci self.reload_udev_rules() 31562306a36Sopenharmony_ci 31662306a36Sopenharmony_ci def reload_udev_rules(self): 31762306a36Sopenharmony_ci import subprocess 31862306a36Sopenharmony_ci 31962306a36Sopenharmony_ci subprocess.run("udevadm control --reload-rules".split()) 32062306a36Sopenharmony_ci subprocess.run("systemd-hwdb update".split()) 32162306a36Sopenharmony_ci 32262306a36Sopenharmony_ci def create_udev_rule(self): 32362306a36Sopenharmony_ci import tempfile 32462306a36Sopenharmony_ci 32562306a36Sopenharmony_ci os.makedirs("/run/udev/rules.d", exist_ok=True) 32662306a36Sopenharmony_ci with tempfile.NamedTemporaryFile( 32762306a36Sopenharmony_ci prefix="91-uhid-test-device-REMOVEME-", 32862306a36Sopenharmony_ci suffix=".rules", 32962306a36Sopenharmony_ci mode="w+", 33062306a36Sopenharmony_ci dir="/run/udev/rules.d", 33162306a36Sopenharmony_ci delete=False, 33262306a36Sopenharmony_ci ) as f: 33362306a36Sopenharmony_ci f.write( 33462306a36Sopenharmony_ci 'KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"\n' 33562306a36Sopenharmony_ci ) 33662306a36Sopenharmony_ci f.write( 33762306a36Sopenharmony_ci 'KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"\n' 33862306a36Sopenharmony_ci ) 33962306a36Sopenharmony_ci self.rulesfile = f 34062306a36Sopenharmony_ci 34162306a36Sopenharmony_ci @classmethod 34262306a36Sopenharmony_ci def instance(cls): 34362306a36Sopenharmony_ci if not cls._instance: 34462306a36Sopenharmony_ci cls._instance = HIDTestUdevRule() 34562306a36Sopenharmony_ci return cls._instance 346