xref: /linux/tools/testing/selftests/hid/tests/base.py (revision 89ea968a)
1ffb85d5cSBenjamin Tissoires#!/bin/env python3
2ffb85d5cSBenjamin Tissoires# SPDX-License-Identifier: GPL-2.0
3ffb85d5cSBenjamin Tissoires# -*- coding: utf-8 -*-
4ffb85d5cSBenjamin Tissoires#
5ffb85d5cSBenjamin Tissoires# Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>
6ffb85d5cSBenjamin Tissoires# Copyright (c) 2017 Red Hat, Inc.
7ffb85d5cSBenjamin Tissoires
8ffb85d5cSBenjamin Tissoiresimport libevdev
9ffb85d5cSBenjamin Tissoiresimport os
10ffb85d5cSBenjamin Tissoiresimport pytest
11*89ea968aSBenjamin Tissoiresimport shutil
12e9064630SBenjamin Tissoiresimport subprocess
13ffb85d5cSBenjamin Tissoiresimport time
14ffb85d5cSBenjamin Tissoires
15ffb85d5cSBenjamin Tissoiresimport logging
16ffb85d5cSBenjamin Tissoires
17a7def2e5SBenjamin Tissoiresfrom .base_device import BaseDevice, EvdevMatch, SysfsFile
18ffb85d5cSBenjamin Tissoiresfrom pathlib import Path
19ed5bc56cSBenjamin Tissoiresfrom typing import Final, List, Tuple
20ffb85d5cSBenjamin Tissoires
21ffb85d5cSBenjamin Tissoireslogger = logging.getLogger("hidtools.test.base")
22ffb85d5cSBenjamin Tissoires
23ffb85d5cSBenjamin Tissoires# application to matches
24ffb85d5cSBenjamin Tissoiresapplication_matches: Final = {
25ffb85d5cSBenjamin Tissoires    # pyright: ignore
26ffb85d5cSBenjamin Tissoires    "Accelerometer": EvdevMatch(
27ffb85d5cSBenjamin Tissoires        req_properties=[
28ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
29ffb85d5cSBenjamin Tissoires        ]
30ffb85d5cSBenjamin Tissoires    ),
31ffb85d5cSBenjamin Tissoires    "Game Pad": EvdevMatch(  # in systemd, this is a lot more complex, but that will do
32ffb85d5cSBenjamin Tissoires        requires=[
33ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_X,
34ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_Y,
35ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_RX,
36ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_RY,
37ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_START,
38ffb85d5cSBenjamin Tissoires        ],
39ffb85d5cSBenjamin Tissoires        excl_properties=[
40ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
41ffb85d5cSBenjamin Tissoires        ],
42ffb85d5cSBenjamin Tissoires    ),
43ffb85d5cSBenjamin Tissoires    "Joystick": EvdevMatch(  # in systemd, this is a lot more complex, but that will do
44ffb85d5cSBenjamin Tissoires        requires=[
45ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_RX,
46ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_RY,
47ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_START,
48ffb85d5cSBenjamin Tissoires        ],
49ffb85d5cSBenjamin Tissoires        excl_properties=[
50ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
51ffb85d5cSBenjamin Tissoires        ],
52ffb85d5cSBenjamin Tissoires    ),
53ffb85d5cSBenjamin Tissoires    "Key": EvdevMatch(
54ffb85d5cSBenjamin Tissoires        requires=[
55ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.KEY_A,
56ffb85d5cSBenjamin Tissoires        ],
57ffb85d5cSBenjamin Tissoires        excl_properties=[
58ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
59ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_DIRECT,
60ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_POINTER,
61ffb85d5cSBenjamin Tissoires        ],
62ffb85d5cSBenjamin Tissoires    ),
63ffb85d5cSBenjamin Tissoires    "Mouse": EvdevMatch(
64ffb85d5cSBenjamin Tissoires        requires=[
65ffb85d5cSBenjamin Tissoires            libevdev.EV_REL.REL_X,
66ffb85d5cSBenjamin Tissoires            libevdev.EV_REL.REL_Y,
67ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_LEFT,
68ffb85d5cSBenjamin Tissoires        ],
69ffb85d5cSBenjamin Tissoires        excl_properties=[
70ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
71ffb85d5cSBenjamin Tissoires        ],
72ffb85d5cSBenjamin Tissoires    ),
73ffb85d5cSBenjamin Tissoires    "Pad": EvdevMatch(
74ffb85d5cSBenjamin Tissoires        requires=[
75ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_0,
76ffb85d5cSBenjamin Tissoires        ],
77ffb85d5cSBenjamin Tissoires        excludes=[
78ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_TOOL_PEN,
79ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_TOUCH,
80ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_DISTANCE,
81ffb85d5cSBenjamin Tissoires        ],
82ffb85d5cSBenjamin Tissoires        excl_properties=[
83ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
84ffb85d5cSBenjamin Tissoires        ],
85ffb85d5cSBenjamin Tissoires    ),
86ffb85d5cSBenjamin Tissoires    "Pen": EvdevMatch(
87ffb85d5cSBenjamin Tissoires        requires=[
88ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_STYLUS,
89ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_X,
90ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_Y,
91ffb85d5cSBenjamin Tissoires        ],
92ffb85d5cSBenjamin Tissoires        excl_properties=[
93ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
94ffb85d5cSBenjamin Tissoires        ],
95ffb85d5cSBenjamin Tissoires    ),
96ffb85d5cSBenjamin Tissoires    "Stylus": EvdevMatch(
97ffb85d5cSBenjamin Tissoires        requires=[
98ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_STYLUS,
99ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_X,
100ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_Y,
101ffb85d5cSBenjamin Tissoires        ],
102ffb85d5cSBenjamin Tissoires        excl_properties=[
103ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
104ffb85d5cSBenjamin Tissoires        ],
105ffb85d5cSBenjamin Tissoires    ),
106ffb85d5cSBenjamin Tissoires    "Touch Pad": EvdevMatch(
107ffb85d5cSBenjamin Tissoires        requires=[
108ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_LEFT,
109ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_X,
110ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_Y,
111ffb85d5cSBenjamin Tissoires        ],
112ffb85d5cSBenjamin Tissoires        excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
113ffb85d5cSBenjamin Tissoires        req_properties=[
114ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_POINTER,
115ffb85d5cSBenjamin Tissoires        ],
116ffb85d5cSBenjamin Tissoires        excl_properties=[
117ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
118ffb85d5cSBenjamin Tissoires        ],
119ffb85d5cSBenjamin Tissoires    ),
120ffb85d5cSBenjamin Tissoires    "Touch Screen": EvdevMatch(
121ffb85d5cSBenjamin Tissoires        requires=[
122ffb85d5cSBenjamin Tissoires            libevdev.EV_KEY.BTN_TOUCH,
123ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_X,
124ffb85d5cSBenjamin Tissoires            libevdev.EV_ABS.ABS_Y,
125ffb85d5cSBenjamin Tissoires        ],
126ffb85d5cSBenjamin Tissoires        excludes=[libevdev.EV_KEY.BTN_TOOL_PEN, libevdev.EV_KEY.BTN_STYLUS],
127ffb85d5cSBenjamin Tissoires        req_properties=[
128ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_DIRECT,
129ffb85d5cSBenjamin Tissoires        ],
130ffb85d5cSBenjamin Tissoires        excl_properties=[
131ffb85d5cSBenjamin Tissoires            libevdev.INPUT_PROP_ACCELEROMETER,
132ffb85d5cSBenjamin Tissoires        ],
133ffb85d5cSBenjamin Tissoires    ),
134ffb85d5cSBenjamin Tissoires}
135ffb85d5cSBenjamin Tissoires
136ffb85d5cSBenjamin Tissoires
137ffb85d5cSBenjamin Tissoiresclass UHIDTestDevice(BaseDevice):
138ffb85d5cSBenjamin Tissoires    def __init__(self, name, application, rdesc_str=None, rdesc=None, input_info=None):
139ffb85d5cSBenjamin Tissoires        super().__init__(name, application, rdesc_str, rdesc, input_info)
140ffb85d5cSBenjamin Tissoires        self.application_matches = application_matches
141ffb85d5cSBenjamin Tissoires        if name is None:
142ffb85d5cSBenjamin Tissoires            name = f"uhid test {self.__class__.__name__}"
143ffb85d5cSBenjamin Tissoires        if not name.startswith("uhid test "):
144ffb85d5cSBenjamin Tissoires            name = "uhid test " + self.name
145ffb85d5cSBenjamin Tissoires        self.name = name
146ffb85d5cSBenjamin Tissoires
147ffb85d5cSBenjamin Tissoires
148ffb85d5cSBenjamin Tissoiresclass BaseTestCase:
149ffb85d5cSBenjamin Tissoires    class TestUhid(object):
150ffb85d5cSBenjamin Tissoires        syn_event = libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT)  # type: ignore
151ffb85d5cSBenjamin Tissoires        key_event = libevdev.InputEvent(libevdev.EV_KEY)  # type: ignore
152ffb85d5cSBenjamin Tissoires        abs_event = libevdev.InputEvent(libevdev.EV_ABS)  # type: ignore
153ffb85d5cSBenjamin Tissoires        rel_event = libevdev.InputEvent(libevdev.EV_REL)  # type: ignore
154ffb85d5cSBenjamin Tissoires        msc_event = libevdev.InputEvent(libevdev.EV_MSC.MSC_SCAN)  # type: ignore
155ffb85d5cSBenjamin Tissoires
156ffb85d5cSBenjamin Tissoires        # List of kernel modules to load before starting the test
157ffb85d5cSBenjamin Tissoires        # if any module is not available (not compiled), the test will skip.
158ffb85d5cSBenjamin Tissoires        # Each element is a tuple '(kernel driver name, kernel module)',
159ffb85d5cSBenjamin Tissoires        # for example ("playstation", "hid-playstation")
160ed5bc56cSBenjamin Tissoires        kernel_modules: List[Tuple[str, str]] = []
161ffb85d5cSBenjamin Tissoires
162e9064630SBenjamin Tissoires        # List of in kernel HID-BPF object files to load
163e9064630SBenjamin Tissoires        # before starting the test
164e9064630SBenjamin Tissoires        # Any existing pre-loaded HID-BPF module will be removed
165e9064630SBenjamin Tissoires        # before the ones in this list will be manually loaded.
166e9064630SBenjamin Tissoires        # Each Element is a tuple '(hid_bpf_object, rdesc_fixup_present)',
167e9064630SBenjamin Tissoires        # for example '("xppen-ArtistPro16Gen2.bpf.o", True)'
168e9064630SBenjamin Tissoires        # If 'rdesc_fixup_present' is True, the test needs to wait
169e9064630SBenjamin Tissoires        # for one unbind and rebind before it can be sure the kernel is
170e9064630SBenjamin Tissoires        # ready
171e9064630SBenjamin Tissoires        hid_bpfs: List[Tuple[str, bool]] = []
172e9064630SBenjamin Tissoires
173ffb85d5cSBenjamin Tissoires        def assertInputEventsIn(self, expected_events, effective_events):
174ffb85d5cSBenjamin Tissoires            effective_events = effective_events.copy()
175ffb85d5cSBenjamin Tissoires            for ev in expected_events:
176ffb85d5cSBenjamin Tissoires                assert ev in effective_events
177ffb85d5cSBenjamin Tissoires                effective_events.remove(ev)
178ffb85d5cSBenjamin Tissoires            return effective_events
179ffb85d5cSBenjamin Tissoires
180ffb85d5cSBenjamin Tissoires        def assertInputEvents(self, expected_events, effective_events):
181ffb85d5cSBenjamin Tissoires            remaining = self.assertInputEventsIn(expected_events, effective_events)
182ffb85d5cSBenjamin Tissoires            assert remaining == []
183ffb85d5cSBenjamin Tissoires
184ffb85d5cSBenjamin Tissoires        @classmethod
185ffb85d5cSBenjamin Tissoires        def debug_reports(cls, reports, uhdev=None, events=None):
186ffb85d5cSBenjamin Tissoires            data = [" ".join([f"{v:02x}" for v in r]) for r in reports]
187ffb85d5cSBenjamin Tissoires
188ffb85d5cSBenjamin Tissoires            if uhdev is not None:
189ffb85d5cSBenjamin Tissoires                human_data = [
190ffb85d5cSBenjamin Tissoires                    uhdev.parsed_rdesc.format_report(r, split_lines=True)
191ffb85d5cSBenjamin Tissoires                    for r in reports
192ffb85d5cSBenjamin Tissoires                ]
193ffb85d5cSBenjamin Tissoires                try:
194ffb85d5cSBenjamin Tissoires                    human_data = [
195ffb85d5cSBenjamin Tissoires                        f'\n\t       {" " * h.index("/")}'.join(h.split("\n"))
196ffb85d5cSBenjamin Tissoires                        for h in human_data
197ffb85d5cSBenjamin Tissoires                    ]
198ffb85d5cSBenjamin Tissoires                except ValueError:
199ffb85d5cSBenjamin Tissoires                    # '/' not found: not a numbered report
200ffb85d5cSBenjamin Tissoires                    human_data = ["\n\t      ".join(h.split("\n")) for h in human_data]
201ffb85d5cSBenjamin Tissoires                data = [f"{d}\n\t ====> {h}" for d, h in zip(data, human_data)]
202ffb85d5cSBenjamin Tissoires
203ffb85d5cSBenjamin Tissoires            reports = data
204ffb85d5cSBenjamin Tissoires
205ffb85d5cSBenjamin Tissoires            if len(reports) == 1:
206ffb85d5cSBenjamin Tissoires                print("sending 1 report:")
207ffb85d5cSBenjamin Tissoires            else:
208ffb85d5cSBenjamin Tissoires                print(f"sending {len(reports)} reports:")
209ffb85d5cSBenjamin Tissoires            for report in reports:
210ffb85d5cSBenjamin Tissoires                print("\t", report)
211ffb85d5cSBenjamin Tissoires
212ffb85d5cSBenjamin Tissoires            if events is not None:
213ffb85d5cSBenjamin Tissoires                print("events received:", events)
214ffb85d5cSBenjamin Tissoires
215ffb85d5cSBenjamin Tissoires        def create_device(self):
216ffb85d5cSBenjamin Tissoires            raise Exception("please reimplement me in subclasses")
217ffb85d5cSBenjamin Tissoires
218ffb85d5cSBenjamin Tissoires        def _load_kernel_module(self, kernel_driver, kernel_module):
219ffb85d5cSBenjamin Tissoires            sysfs_path = Path("/sys/bus/hid/drivers")
220ffb85d5cSBenjamin Tissoires            if kernel_driver is not None:
221ffb85d5cSBenjamin Tissoires                sysfs_path /= kernel_driver
222ffb85d5cSBenjamin Tissoires            else:
223ffb85d5cSBenjamin Tissoires                # special case for when testing all available modules:
224ffb85d5cSBenjamin Tissoires                # we don't know beforehand the name of the module from modinfo
225ffb85d5cSBenjamin Tissoires                sysfs_path = Path("/sys/module") / kernel_module.replace("-", "_")
226ffb85d5cSBenjamin Tissoires            if not sysfs_path.exists():
227ffb85d5cSBenjamin Tissoires                ret = subprocess.run(["/usr/sbin/modprobe", kernel_module])
228ffb85d5cSBenjamin Tissoires                if ret.returncode != 0:
229ffb85d5cSBenjamin Tissoires                    pytest.skip(
230ffb85d5cSBenjamin Tissoires                        f"module {kernel_module} could not be loaded, skipping the test"
231ffb85d5cSBenjamin Tissoires                    )
232ffb85d5cSBenjamin Tissoires
233ffb85d5cSBenjamin Tissoires        @pytest.fixture()
234ffb85d5cSBenjamin Tissoires        def load_kernel_module(self):
235ffb85d5cSBenjamin Tissoires            for kernel_driver, kernel_module in self.kernel_modules:
236ffb85d5cSBenjamin Tissoires                self._load_kernel_module(kernel_driver, kernel_module)
237ffb85d5cSBenjamin Tissoires            yield
238ffb85d5cSBenjamin Tissoires
239e9064630SBenjamin Tissoires        def load_hid_bpfs(self):
240e9064630SBenjamin Tissoires            script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
241e9064630SBenjamin Tissoires            root_dir = (script_dir / "../../../../..").resolve()
242e9064630SBenjamin Tissoires            bpf_dir = root_dir / "drivers/hid/bpf/progs"
243e9064630SBenjamin Tissoires
244*89ea968aSBenjamin Tissoires            udev_hid_bpf = shutil.which("udev-hid-bpf")
245*89ea968aSBenjamin Tissoires            if not udev_hid_bpf:
246*89ea968aSBenjamin Tissoires                pytest.skip("udev-hid-bpf not found in $PATH, skipping")
247*89ea968aSBenjamin Tissoires
248e9064630SBenjamin Tissoires            wait = False
249e9064630SBenjamin Tissoires            for _, rdesc_fixup in self.hid_bpfs:
250e9064630SBenjamin Tissoires                if rdesc_fixup:
251e9064630SBenjamin Tissoires                    wait = True
252e9064630SBenjamin Tissoires
253e9064630SBenjamin Tissoires            for hid_bpf, _ in self.hid_bpfs:
254e9064630SBenjamin Tissoires                # We need to start `udev-hid-bpf` in the background
255e9064630SBenjamin Tissoires                # and dispatch uhid events in case the kernel needs
256e9064630SBenjamin Tissoires                # to fetch features on the device
257e9064630SBenjamin Tissoires                process = subprocess.Popen(
258e9064630SBenjamin Tissoires                    [
259e9064630SBenjamin Tissoires                        "udev-hid-bpf",
260e9064630SBenjamin Tissoires                        "--verbose",
261e9064630SBenjamin Tissoires                        "add",
262e9064630SBenjamin Tissoires                        str(self.uhdev.sys_path),
263e9064630SBenjamin Tissoires                        str(bpf_dir / hid_bpf),
264e9064630SBenjamin Tissoires                    ],
265e9064630SBenjamin Tissoires                )
266e9064630SBenjamin Tissoires                while process.poll() is None:
267e9064630SBenjamin Tissoires                    self.uhdev.dispatch(1)
268e9064630SBenjamin Tissoires
269e9064630SBenjamin Tissoires                if process.poll() != 0:
270e9064630SBenjamin Tissoires                    pytest.fail(
271e9064630SBenjamin Tissoires                        f"Couldn't insert hid-bpf program '{hid_bpf}', marking the test as failed"
272e9064630SBenjamin Tissoires                    )
273e9064630SBenjamin Tissoires
274e9064630SBenjamin Tissoires            if wait:
275e9064630SBenjamin Tissoires                # the HID-BPF program exports a rdesc fixup, so it needs to be
276e9064630SBenjamin Tissoires                # unbound by the kernel and then rebound.
277e9064630SBenjamin Tissoires                # Ensure we get the bound event exactly 2 times (one for the normal
278e9064630SBenjamin Tissoires                # uhid loading, and then the reload from HID-BPF)
279e9064630SBenjamin Tissoires                now = time.time()
280e9064630SBenjamin Tissoires                while self.uhdev.kernel_ready_count < 2 and time.time() - now < 2:
281e9064630SBenjamin Tissoires                    self.uhdev.dispatch(1)
282e9064630SBenjamin Tissoires
283e9064630SBenjamin Tissoires                if self.uhdev.kernel_ready_count < 2:
284e9064630SBenjamin Tissoires                    pytest.fail(
285e9064630SBenjamin Tissoires                        f"Couldn't insert hid-bpf programs, marking the test as failed"
286e9064630SBenjamin Tissoires                    )
287e9064630SBenjamin Tissoires
288e9064630SBenjamin Tissoires        def unload_hid_bpfs(self):
289e9064630SBenjamin Tissoires            ret = subprocess.run(
290e9064630SBenjamin Tissoires                ["udev-hid-bpf", "--verbose", "remove", str(self.uhdev.sys_path)],
291e9064630SBenjamin Tissoires            )
292e9064630SBenjamin Tissoires            if ret.returncode != 0:
293e9064630SBenjamin Tissoires                pytest.fail(
294e9064630SBenjamin Tissoires                    f"Couldn't unload hid-bpf programs, marking the test as failed"
295e9064630SBenjamin Tissoires                )
296e9064630SBenjamin Tissoires
297ffb85d5cSBenjamin Tissoires        @pytest.fixture()
298ffb85d5cSBenjamin Tissoires        def new_uhdev(self, load_kernel_module):
299ffb85d5cSBenjamin Tissoires            return self.create_device()
300ffb85d5cSBenjamin Tissoires
301ffb85d5cSBenjamin Tissoires        def assertName(self, uhdev):
302ffb85d5cSBenjamin Tissoires            evdev = uhdev.get_evdev()
303ffb85d5cSBenjamin Tissoires            assert uhdev.name in evdev.name
304ffb85d5cSBenjamin Tissoires
305ffb85d5cSBenjamin Tissoires        @pytest.fixture(autouse=True)
306ffb85d5cSBenjamin Tissoires        def context(self, new_uhdev, request):
307ffb85d5cSBenjamin Tissoires            try:
308ffb85d5cSBenjamin Tissoires                with HIDTestUdevRule.instance():
309ffb85d5cSBenjamin Tissoires                    with new_uhdev as self.uhdev:
310110292a7SBenjamin Tissoires                        for skip_cond in request.node.iter_markers("skip_if_uhdev"):
311ffb85d5cSBenjamin Tissoires                            test, message, *rest = skip_cond.args
312ffb85d5cSBenjamin Tissoires
313ffb85d5cSBenjamin Tissoires                            if test(self.uhdev):
314ffb85d5cSBenjamin Tissoires                                pytest.skip(message)
315ffb85d5cSBenjamin Tissoires
316ffb85d5cSBenjamin Tissoires                        self.uhdev.create_kernel_device()
317ffb85d5cSBenjamin Tissoires                        now = time.time()
318ffb85d5cSBenjamin Tissoires                        while not self.uhdev.is_ready() and time.time() - now < 5:
319ffb85d5cSBenjamin Tissoires                            self.uhdev.dispatch(1)
320e9064630SBenjamin Tissoires
321e9064630SBenjamin Tissoires                        if self.hid_bpfs:
322e9064630SBenjamin Tissoires                            self.load_hid_bpfs()
323e9064630SBenjamin Tissoires
324ffb85d5cSBenjamin Tissoires                        if self.uhdev.get_evdev() is None:
325ffb85d5cSBenjamin Tissoires                            logger.warning(
326ffb85d5cSBenjamin Tissoires                                f"available list of input nodes: (default application is '{self.uhdev.application}')"
327ffb85d5cSBenjamin Tissoires                            )
328ffb85d5cSBenjamin Tissoires                            logger.warning(self.uhdev.input_nodes)
329ffb85d5cSBenjamin Tissoires                        yield
330e9064630SBenjamin Tissoires                        if self.hid_bpfs:
331e9064630SBenjamin Tissoires                            self.unload_hid_bpfs()
332ffb85d5cSBenjamin Tissoires                        self.uhdev = None
333ffb85d5cSBenjamin Tissoires            except PermissionError:
334ffb85d5cSBenjamin Tissoires                pytest.skip("Insufficient permissions, run me as root")
335ffb85d5cSBenjamin Tissoires
336ffb85d5cSBenjamin Tissoires        @pytest.fixture(autouse=True)
337ffb85d5cSBenjamin Tissoires        def check_taint(self):
338ffb85d5cSBenjamin Tissoires            # we are abusing SysfsFile here, it's in /proc, but meh
339ffb85d5cSBenjamin Tissoires            taint_file = SysfsFile("/proc/sys/kernel/tainted")
340ffb85d5cSBenjamin Tissoires            taint = taint_file.int_value
341ffb85d5cSBenjamin Tissoires
342ffb85d5cSBenjamin Tissoires            yield
343ffb85d5cSBenjamin Tissoires
344ffb85d5cSBenjamin Tissoires            assert taint_file.int_value == taint
345ffb85d5cSBenjamin Tissoires
346ffb85d5cSBenjamin Tissoires        def test_creation(self):
347ffb85d5cSBenjamin Tissoires            """Make sure the device gets processed by the kernel and creates
348ffb85d5cSBenjamin Tissoires            the expected application input node.
349ffb85d5cSBenjamin Tissoires
350ffb85d5cSBenjamin Tissoires            If this fail, there is something wrong in the device report
351ffb85d5cSBenjamin Tissoires            descriptors."""
352ffb85d5cSBenjamin Tissoires            uhdev = self.uhdev
353ffb85d5cSBenjamin Tissoires            assert uhdev is not None
354ffb85d5cSBenjamin Tissoires            assert uhdev.get_evdev() is not None
355ffb85d5cSBenjamin Tissoires            self.assertName(uhdev)
356ffb85d5cSBenjamin Tissoires            assert len(uhdev.next_sync_events()) == 0
357ffb85d5cSBenjamin Tissoires            assert uhdev.get_evdev() is not None
358ffb85d5cSBenjamin Tissoires
359ffb85d5cSBenjamin Tissoires
360ffb85d5cSBenjamin Tissoiresclass HIDTestUdevRule(object):
361ffb85d5cSBenjamin Tissoires    _instance = None
362ffb85d5cSBenjamin Tissoires    """
363ffb85d5cSBenjamin Tissoires    A context-manager compatible class that sets up our udev rules file and
364ffb85d5cSBenjamin Tissoires    deletes it on context exit.
365ffb85d5cSBenjamin Tissoires
366ffb85d5cSBenjamin Tissoires    This class is tailored to our test setup: it only sets up the udev rule
367ffb85d5cSBenjamin Tissoires    on the **second** context and it cleans it up again on the last context
368ffb85d5cSBenjamin Tissoires    removed. This matches the expected pytest setup: we enter a context for
369ffb85d5cSBenjamin Tissoires    the session once, then once for each test (the first of which will
370ffb85d5cSBenjamin Tissoires    trigger the udev rule) and once the last test exited and the session
371ffb85d5cSBenjamin Tissoires    exited, we clean up after ourselves.
372ffb85d5cSBenjamin Tissoires    """
373ffb85d5cSBenjamin Tissoires
374ffb85d5cSBenjamin Tissoires    def __init__(self):
375ffb85d5cSBenjamin Tissoires        self.refs = 0
376ffb85d5cSBenjamin Tissoires        self.rulesfile = None
377ffb85d5cSBenjamin Tissoires
378ffb85d5cSBenjamin Tissoires    def __enter__(self):
379ffb85d5cSBenjamin Tissoires        self.refs += 1
380ffb85d5cSBenjamin Tissoires        if self.refs == 2 and self.rulesfile is None:
381ffb85d5cSBenjamin Tissoires            self.create_udev_rule()
382ffb85d5cSBenjamin Tissoires            self.reload_udev_rules()
383ffb85d5cSBenjamin Tissoires
384ffb85d5cSBenjamin Tissoires    def __exit__(self, exc_type, exc_value, traceback):
385ffb85d5cSBenjamin Tissoires        self.refs -= 1
386ffb85d5cSBenjamin Tissoires        if self.refs == 0 and self.rulesfile:
387ffb85d5cSBenjamin Tissoires            os.remove(self.rulesfile.name)
388ffb85d5cSBenjamin Tissoires            self.reload_udev_rules()
389ffb85d5cSBenjamin Tissoires
390ffb85d5cSBenjamin Tissoires    def reload_udev_rules(self):
391ffb85d5cSBenjamin Tissoires        subprocess.run("udevadm control --reload-rules".split())
392ffb85d5cSBenjamin Tissoires        subprocess.run("systemd-hwdb update".split())
393ffb85d5cSBenjamin Tissoires
394ffb85d5cSBenjamin Tissoires    def create_udev_rule(self):
395ffb85d5cSBenjamin Tissoires        import tempfile
396ffb85d5cSBenjamin Tissoires
397ffb85d5cSBenjamin Tissoires        os.makedirs("/run/udev/rules.d", exist_ok=True)
398ffb85d5cSBenjamin Tissoires        with tempfile.NamedTemporaryFile(
399ffb85d5cSBenjamin Tissoires            prefix="91-uhid-test-device-REMOVEME-",
400ffb85d5cSBenjamin Tissoires            suffix=".rules",
401ffb85d5cSBenjamin Tissoires            mode="w+",
402ffb85d5cSBenjamin Tissoires            dir="/run/udev/rules.d",
403ffb85d5cSBenjamin Tissoires            delete=False,
404ffb85d5cSBenjamin Tissoires        ) as f:
405ffb85d5cSBenjamin Tissoires            f.write(
406e9064630SBenjamin Tissoires                """
407e9064630SBenjamin TissoiresKERNELS=="*input*", ATTRS{name}=="*uhid test *", ENV{LIBINPUT_IGNORE_DEVICE}="1"
408e9064630SBenjamin TissoiresKERNELS=="*hid*", ENV{HID_NAME}=="*uhid test *", ENV{HID_BPF_IGNORE_DEVICE}="1"
409e9064630SBenjamin TissoiresKERNELS=="*input*", ATTRS{name}=="*uhid test * System Multi Axis", ENV{ID_INPUT_TOUCHSCREEN}="", ENV{ID_INPUT_SYSTEM_MULTIAXIS}="1"
410e9064630SBenjamin Tissoires"""
411ffb85d5cSBenjamin Tissoires            )
412ffb85d5cSBenjamin Tissoires            self.rulesfile = f
413ffb85d5cSBenjamin Tissoires
414ffb85d5cSBenjamin Tissoires    @classmethod
415ffb85d5cSBenjamin Tissoires    def instance(cls):
416ffb85d5cSBenjamin Tissoires        if not cls._instance:
417ffb85d5cSBenjamin Tissoires            cls._instance = HIDTestUdevRule()
418ffb85d5cSBenjamin Tissoires        return cls._instance
419