brintos

brintos / linux-shallow public Read only

0
0
Text · 14.6 KiB · 3a46576 Raw
419 lines · python
1#!/bin/env python32# SPDX-License-Identifier: GPL-2.03# -*- coding: utf-8 -*-4#5# Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>6# Copyright (c) 2017 Red Hat, Inc.7 8import libevdev9import os10import pytest11import shutil12import subprocess13import time14 15import logging16 17from .base_device import BaseDevice, EvdevMatch, SysfsFile18from pathlib import Path19from typing import Final, List, Tuple20 21logger = logging.getLogger("hidtools.test.base")22 23# application to matches24application_matches: Final = {25    # pyright: ignore26    "Accelerometer": EvdevMatch(27        req_properties=[28            libevdev.INPUT_PROP_ACCELEROMETER,29        ]30    ),31    "Game Pad": EvdevMatch(  # in systemd, this is a lot more complex, but that will do32        requires=[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,38        ],39        excl_properties=[40            libevdev.INPUT_PROP_ACCELEROMETER,41        ],42    ),43    "Joystick": EvdevMatch(  # in systemd, this is a lot more complex, but that will do44        requires=[45            libevdev.EV_ABS.ABS_RX,46            libevdev.EV_ABS.ABS_RY,47            libevdev.EV_KEY.BTN_START,48        ],49        excl_properties=[50            libevdev.INPUT_PROP_ACCELEROMETER,51        ],52    ),53    "Key": EvdevMatch(54        requires=[55            libevdev.EV_KEY.KEY_A,56        ],57        excl_properties=[58            libevdev.INPUT_PROP_ACCELEROMETER,59            libevdev.INPUT_PROP_DIRECT,60            libevdev.INPUT_PROP_POINTER,61        ],62    ),63    "Mouse": EvdevMatch(64        requires=[65            libevdev.EV_REL.REL_X,66            libevdev.EV_REL.REL_Y,67            libevdev.EV_KEY.BTN_LEFT,68        ],69        excl_properties=[70            libevdev.INPUT_PROP_ACCELEROMETER,71        ],72    ),73    "Pad": EvdevMatch(74        requires=[75            libevdev.EV_KEY.BTN_0,76        ],77        excludes=[78            libevdev.EV_KEY.BTN_TOOL_PEN,79            libevdev.EV_KEY.BTN_TOUCH,80            libevdev.EV_ABS.ABS_DISTANCE,81        ],82        excl_properties=[83            libevdev.INPUT_PROP_ACCELEROMETER,84        ],85    ),86    "Pen": EvdevMatch(87        requires=[88            libevdev.EV_KEY.BTN_STYLUS,89            libevdev.EV_ABS.ABS_X,90            libevdev.EV_ABS.ABS_Y,91        ],92        excl_properties=[93            libevdev.INPUT_PROP_ACCELEROMETER,94        ],95    ),96    "Stylus": EvdevMatch(97        requires=[98            libevdev.EV_KEY.BTN_STYLUS,99            libevdev.EV_ABS.ABS_X,100            libevdev.EV_ABS.ABS_Y,101        ],102        excl_properties=[103            libevdev.INPUT_PROP_ACCELEROMETER,104        ],105    ),106    "Touch Pad": EvdevMatch(107        requires=[108            libevdev.EV_KEY.BTN_LEFT,109            libevdev.EV_ABS.ABS_X,110            libevdev.EV_ABS.ABS_Y,111        ],112        excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],113        req_properties=[114            libevdev.INPUT_PROP_POINTER,115        ],116        excl_properties=[117            libevdev.INPUT_PROP_ACCELEROMETER,118        ],119    ),120    "Touch Screen": EvdevMatch(121        requires=[122            libevdev.EV_KEY.BTN_TOUCH,123            libevdev.EV_ABS.ABS_X,124            libevdev.EV_ABS.ABS_Y,125        ],126        excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],127        req_properties=[128            libevdev.INPUT_PROP_DIRECT,129        ],130        excl_properties=[131            libevdev.INPUT_PROP_ACCELEROMETER,132        ],133    ),134}135 136 137class 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_matches141        if name is None:142            name = f"uhid test {self.__class__.__name__}"143        if not name.startswith("uhid test "):144            name = "uhid test " + self.name145        self.name = name146 147 148class BaseTestCase:149    class TestUhid(object):150        syn_event = libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT)  # type: ignore151        key_event = libevdev.InputEvent(libevdev.EV_KEY)  # type: ignore152        abs_event = libevdev.InputEvent(libevdev.EV_ABS)  # type: ignore153        rel_event = libevdev.InputEvent(libevdev.EV_REL)  # type: ignore154        msc_event = libevdev.InputEvent(libevdev.EV_MSC.MSC_SCAN)  # type: ignore155 156        # List of kernel modules to load before starting the test157        # 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]] = []161 162        # List of in kernel HID-BPF object files to load163        # before starting the test164        # Any existing pre-loaded HID-BPF module will be removed165        # 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 wait169        # for one unbind and rebind before it can be sure the kernel is170        # ready171        hid_bpfs: List[Tuple[str, bool]] = []172 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_events177                effective_events.remove(ev)178            return effective_events179 180        def assertInputEvents(self, expected_events, effective_events):181            remaining = self.assertInputEventsIn(expected_events, effective_events)182            assert remaining == []183 184        @classmethod185        def debug_reports(cls, reports, uhdev=None, events=None):186            data = [" ".join([f"{v:02x}" for v in r]) for r in reports]187 188            if uhdev is not None:189                human_data = [190                    uhdev.parsed_rdesc.format_report(r, split_lines=True)191                    for r in reports192                ]193                try:194                    human_data = [195                        f'\n\t       {" " * h.index("/")}'.join(h.split("\n"))196                        for h in human_data197                    ]198                except ValueError:199                    # '/' not found: not a numbered report200                    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)]202 203            reports = data204 205            if len(reports) == 1:206                print("sending 1 report:")207            else:208                print(f"sending {len(reports)} reports:")209            for report in reports:210                print("\t", report)211 212            if events is not None:213                print("events received:", events)214 215        def create_device(self):216            raise Exception("please reimplement me in subclasses")217 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_driver222            else:223                # special case for when testing all available modules:224                # we don't know beforehand the name of the module from modinfo225                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:229                    pytest.skip(230                        f"module {kernel_module} could not be loaded, skipping the test"231                    )232 233        @pytest.fixture()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)237            yield238 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"243 244            udev_hid_bpf = shutil.which("udev-hid-bpf")245            if not udev_hid_bpf:246                pytest.skip("udev-hid-bpf not found in $PATH, skipping")247 248            wait = False249            for _, rdesc_fixup in self.hid_bpfs:250                if rdesc_fixup:251                    wait = True252 253            for hid_bpf, _ in self.hid_bpfs:254                # We need to start `udev-hid-bpf` in the background255                # and dispatch uhid events in case the kernel needs256                # to fetch features on the device257                process = subprocess.Popen(258                    [259                        "udev-hid-bpf",260                        "--verbose",261                        "add",262                        str(self.uhdev.sys_path),263                        str(bpf_dir / hid_bpf),264                    ],265                )266                while process.poll() is None:267                    self.uhdev.dispatch(1)268 269                if process.poll() != 0:270                    pytest.fail(271                        f"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed"272                    )273 274            if wait:275                # the HID-BPF program exports a rdesc fixup, so it needs to be276                # unbound by the kernel and then rebound.277                # Ensure we get the bound event exactly 2 times (one for the normal278                # uhid loading, and then the reload from HID-BPF)279                now = time.time()280                while self.uhdev.kernel_ready_count < 2 and time.time() - now < 2:281                    self.uhdev.dispatch(1)282 283                if self.uhdev.kernel_ready_count < 2:284                    pytest.fail(285                        f"Couldn't insert hid-bpf programs, marking the test as failed"286                    )287 288        def unload_hid_bpfs(self):289            ret = subprocess.run(290                ["udev-hid-bpf", "--verbose", "remove", str(self.uhdev.sys_path)],291            )292            if ret.returncode != 0:293                pytest.fail(294                    f"Couldn't unload hid-bpf programs, marking the test as failed"295                )296 297        @pytest.fixture()298        def new_uhdev(self, load_kernel_module):299            return self.create_device()300 301        def assertName(self, uhdev):302            evdev = uhdev.get_evdev()303            assert uhdev.name in evdev.name304 305        @pytest.fixture(autouse=True)306        def context(self, new_uhdev, request):307            try: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.args312 313                            if test(self.uhdev):314                                pytest.skip(message)315 316                        self.uhdev.create_kernel_device()317                        now = time.time()318                        while not self.uhdev.is_ready() and time.time() - now < 5:319                            self.uhdev.dispatch(1)320 321                        if self.hid_bpfs:322                            self.load_hid_bpfs()323 324                        if self.uhdev.get_evdev() is None:325                            logger.warning(326                                f"available list of input nodes: (default application is '{self.uhdev.application}')"327                            )328                            logger.warning(self.uhdev.input_nodes)329                        yield330                        if self.hid_bpfs:331                            self.unload_hid_bpfs()332                        self.uhdev = None333            except PermissionError:334                pytest.skip("Insufficient permissions, run me as root")335 336        @pytest.fixture(autouse=True)337        def check_taint(self):338            # we are abusing SysfsFile here, it's in /proc, but meh339            taint_file = SysfsFile("/proc/sys/kernel/tainted")340            taint = taint_file.int_value341 342            yield343 344            assert taint_file.int_value == taint345 346        def test_creation(self):347            """Make sure the device gets processed by the kernel and creates348            the expected application input node.349 350            If this fail, there is something wrong in the device report351            descriptors."""352            uhdev = self.uhdev353            assert uhdev is not None354            assert uhdev.get_evdev() is not None355            self.assertName(uhdev)356            assert len(uhdev.next_sync_events()) == 0357            assert uhdev.get_evdev() is not None358 359 360class HIDTestUdevRule(object):361    _instance = None362    """363    A context-manager compatible class that sets up our udev rules file and364    deletes it on context exit.365 366    This class is tailored to our test setup: it only sets up the udev rule367    on the **second** context and it cleans it up again on the last context368    removed. This matches the expected pytest setup: we enter a context for369    the session once, then once for each test (the first of which will370    trigger the udev rule) and once the last test exited and the session371    exited, we clean up after ourselves.372    """373 374    def __init__(self):375        self.refs = 0376        self.rulesfile = None377 378    def __enter__(self):379        self.refs += 1380        if self.refs == 2 and self.rulesfile is None:381            self.create_udev_rule()382            self.reload_udev_rules()383 384    def __exit__(self, exc_type, exc_value, traceback):385        self.refs -= 1386        if self.refs == 0 and self.rulesfile:387            os.remove(self.rulesfile.name)388            self.reload_udev_rules()389 390    def reload_udev_rules(self):391        subprocess.run("udevadm control --reload-rules".split())392        subprocess.run("systemd-hwdb update".split())393 394    def create_udev_rule(self):395        import tempfile396 397        os.makedirs("/run/udev/rules.d", exist_ok=True)398        with tempfile.NamedTemporaryFile(399            prefix="91-uhid-test-device-REMOVEME-",400            suffix=".rules",401            mode="w+",402            dir="/run/udev/rules.d",403            delete=False,404        ) as f:405            f.write(406                """407KERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"408KERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1"409KERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"410"""411            )412            self.rulesfile = f413 414    @classmethod415    def instance(cls):416        if not cls._instance:417            cls._instance = HIDTestUdevRule()418        return cls._instance419