diff mbox series

[05/11] selftests: hid: import hid-tools hid-mouse tests

Message ID 20230217-import-hid-tools-tests-v1-5-d1c48590d0ee@redhat.com (mailing list archive)
State Mainlined
Commit 356888cb08890a61f4d4565c5950c070feafada1
Headers show
Series selftests: hid: import the tests from hid-tools | expand

Commit Message

Benjamin Tissoires Feb. 17, 2023, 4:17 p.m. UTC
These tests have been developed in the hid-tools[0] tree for a while.
Now that we have  a proper selftests/hid kernel entry and that the tests
are more reliable, it is time to directly include those in the kernel
tree.

[0] https://gitlab.freedesktop.org/libevdev/hid-tools

Cc: Peter Hutterer <peter.hutterer@who-t.net>
Cc: Roderick Colenbrander <roderick.colenbrander@sony.com>
Signed-off-by: Benjamin Tissoires <benjamin.tissoires@redhat.com>
---
 tools/testing/selftests/hid/Makefile            |   1 +
 tools/testing/selftests/hid/hid-mouse.sh        |   7 +
 tools/testing/selftests/hid/tests/test_mouse.py | 977 ++++++++++++++++++++++++
 3 files changed, 985 insertions(+)
diff mbox series

Patch

diff --git a/tools/testing/selftests/hid/Makefile b/tools/testing/selftests/hid/Makefile
index 181a594ffe92..00b2cc25e24c 100644
--- a/tools/testing/selftests/hid/Makefile
+++ b/tools/testing/selftests/hid/Makefile
@@ -8,6 +8,7 @@  include ../../../scripts/Makefile.include
 TEST_PROGS := hid-core.sh
 TEST_PROGS += hid-gamepad.sh
 TEST_PROGS += hid-keyboard.sh
+TEST_PROGS += hid-mouse.sh
 
 CXX ?= $(CROSS_COMPILE)g++
 
diff --git a/tools/testing/selftests/hid/hid-mouse.sh b/tools/testing/selftests/hid/hid-mouse.sh
new file mode 100755
index 000000000000..7b4ad4f646f7
--- /dev/null
+++ b/tools/testing/selftests/hid/hid-mouse.sh
@@ -0,0 +1,7 @@ 
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# Runs tests for the HID subsystem
+
+export TARGET=test_mouse.py
+
+bash ./run-hid-tools-tests.sh
diff --git a/tools/testing/selftests/hid/tests/test_mouse.py b/tools/testing/selftests/hid/tests/test_mouse.py
new file mode 100644
index 000000000000..fd2ba62e783a
--- /dev/null
+++ b/tools/testing/selftests/hid/tests/test_mouse.py
@@ -0,0 +1,977 @@ 
+#!/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Benjamin Tissoires <benjamin.tissoires@gmail.com>
+# Copyright (c) 2017 Red Hat, Inc.
+#
+
+from . import base
+import hidtools.hid
+from hidtools.util import BusType
+import libevdev
+import logging
+import pytest
+
+logger = logging.getLogger("hidtools.test.mouse")
+
+# workaround https://gitlab.freedesktop.org/libevdev/python-libevdev/issues/6
+try:
+    libevdev.EV_REL.REL_WHEEL_HI_RES
+except AttributeError:
+    libevdev.EV_REL.REL_WHEEL_HI_RES = libevdev.EV_REL.REL_0B
+    libevdev.EV_REL.REL_HWHEEL_HI_RES = libevdev.EV_REL.REL_0C
+
+
+class InvalidHIDCommunication(Exception):
+    pass
+
+
+class MouseData(object):
+    pass
+
+
+class BaseMouse(base.UHIDTestDevice):
+    def __init__(self, rdesc, name=None, input_info=None):
+        assert rdesc is not None
+        super().__init__(name, "Mouse", input_info=input_info, rdesc=rdesc)
+        self.left = False
+        self.right = False
+        self.middle = False
+
+    def create_report(self, x, y, buttons=None, wheels=None, reportID=None):
+        """
+        Return an input report for this device.
+
+        :param x: relative x
+        :param y: relative y
+        :param buttons: a (l, r, m) tuple of bools for the button states,
+            where ``None`` is "leave unchanged"
+        :param wheels: a single value for the vertical wheel or a (vertical, horizontal) tuple for
+            the two wheels
+        :param reportID: the numeric report ID for this report, if needed
+        """
+        if buttons is not None:
+            l, r, m = buttons
+            if l is not None:
+                self.left = l
+            if r is not None:
+                self.right = r
+            if m is not None:
+                self.middle = m
+        left = self.left
+        right = self.right
+        middle = self.middle
+        # Note: the BaseMouse doesn't actually have a wheel but the
+        # create_report magic only fills in those fields exist, so let's
+        # make this generic here.
+        wheel, acpan = 0, 0
+        if wheels is not None:
+            if isinstance(wheels, tuple):
+                wheel = wheels[0]
+                acpan = wheels[1]
+            else:
+                wheel = wheels
+
+        reportID = reportID or self.default_reportID
+
+        mouse = MouseData()
+        mouse.b1 = int(left)
+        mouse.b2 = int(right)
+        mouse.b3 = int(middle)
+        mouse.x = x
+        mouse.y = y
+        mouse.wheel = wheel
+        mouse.acpan = acpan
+        return super().create_report(mouse, reportID=reportID)
+
+    def event(self, x, y, buttons=None, wheels=None):
+        """
+        Send an input event on the default report ID.
+
+        :param x: relative x
+        :param y: relative y
+        :param buttons: a (l, r, m) tuple of bools for the button states,
+            where ``None`` is "leave unchanged"
+        :param wheels: a single value for the vertical wheel or a (vertical, horizontal) tuple for
+            the two wheels
+        """
+        r = self.create_report(x, y, buttons, wheels)
+        self.call_input_event(r)
+        return [r]
+
+
+class ButtonMouse(BaseMouse):
+    # fmt: off
+    report_descriptor = [
+        0x05, 0x01,  # .Usage Page (Generic Desktop)        0
+        0x09, 0x02,  # .Usage (Mouse)                       2
+        0xa1, 0x01,  # .Collection (Application)            4
+        0x09, 0x02,  # ..Usage (Mouse)                      6
+        0xa1, 0x02,  # ..Collection (Logical)               8
+        0x09, 0x01,  # ...Usage (Pointer)                   10
+        0xa1, 0x00,  # ...Collection (Physical)             12
+        0x05, 0x09,  # ....Usage Page (Button)              14
+        0x19, 0x01,  # ....Usage Minimum (1)                16
+        0x29, 0x03,  # ....Usage Maximum (3)                18
+        0x15, 0x00,  # ....Logical Minimum (0)              20
+        0x25, 0x01,  # ....Logical Maximum (1)              22
+        0x75, 0x01,  # ....Report Size (1)                  24
+        0x95, 0x03,  # ....Report Count (3)                 26
+        0x81, 0x02,  # ....Input (Data,Var,Abs)             28
+        0x75, 0x05,  # ....Report Size (5)                  30
+        0x95, 0x01,  # ....Report Count (1)                 32
+        0x81, 0x03,  # ....Input (Cnst,Var,Abs)             34
+        0x05, 0x01,  # ....Usage Page (Generic Desktop)     36
+        0x09, 0x30,  # ....Usage (X)                        38
+        0x09, 0x31,  # ....Usage (Y)                        40
+        0x15, 0x81,  # ....Logical Minimum (-127)           42
+        0x25, 0x7f,  # ....Logical Maximum (127)            44
+        0x75, 0x08,  # ....Report Size (8)                  46
+        0x95, 0x02,  # ....Report Count (2)                 48
+        0x81, 0x06,  # ....Input (Data,Var,Rel)             50
+        0xc0,        # ...End Collection                    52
+        0xc0,        # ..End Collection                     53
+        0xc0,        # .End Collection                      54
+    ]
+    # fmt: on
+
+    def __init__(self, rdesc=report_descriptor, name=None, input_info=None):
+        super().__init__(rdesc, name, input_info)
+
+    def fake_report(self, x, y, buttons):
+        if buttons is not None:
+            left, right, middle = buttons
+            if left is None:
+                left = self.left
+            if right is None:
+                right = self.right
+            if middle is None:
+                middle = self.middle
+        else:
+            left = self.left
+            right = self.right
+            middle = self.middle
+
+        button_mask = sum(1 << i for i, b in enumerate([left, right, middle]) if b)
+        x = max(-127, min(127, x))
+        y = max(-127, min(127, y))
+        x = hidtools.util.to_twos_comp(x, 8)
+        y = hidtools.util.to_twos_comp(y, 8)
+        return [button_mask, x, y]
+
+
+class WheelMouse(ButtonMouse):
+    # fmt: off
+    report_descriptor = [
+        0x05, 0x01,  # Usage Page (Generic Desktop)        0
+        0x09, 0x02,  # Usage (Mouse)                       2
+        0xa1, 0x01,  # Collection (Application)            4
+        0x05, 0x09,  # .Usage Page (Button)                6
+        0x19, 0x01,  # .Usage Minimum (1)                  8
+        0x29, 0x03,  # .Usage Maximum (3)                  10
+        0x15, 0x00,  # .Logical Minimum (0)                12
+        0x25, 0x01,  # .Logical Maximum (1)                14
+        0x95, 0x03,  # .Report Count (3)                   16
+        0x75, 0x01,  # .Report Size (1)                    18
+        0x81, 0x02,  # .Input (Data,Var,Abs)               20
+        0x95, 0x01,  # .Report Count (1)                   22
+        0x75, 0x05,  # .Report Size (5)                    24
+        0x81, 0x03,  # .Input (Cnst,Var,Abs)               26
+        0x05, 0x01,  # .Usage Page (Generic Desktop)       28
+        0x09, 0x01,  # .Usage (Pointer)                    30
+        0xa1, 0x00,  # .Collection (Physical)              32
+        0x09, 0x30,  # ..Usage (X)                         34
+        0x09, 0x31,  # ..Usage (Y)                         36
+        0x15, 0x81,  # ..Logical Minimum (-127)            38
+        0x25, 0x7f,  # ..Logical Maximum (127)             40
+        0x75, 0x08,  # ..Report Size (8)                   42
+        0x95, 0x02,  # ..Report Count (2)                  44
+        0x81, 0x06,  # ..Input (Data,Var,Rel)              46
+        0xc0,        # .End Collection                     48
+        0x09, 0x38,  # .Usage (Wheel)                      49
+        0x15, 0x81,  # .Logical Minimum (-127)             51
+        0x25, 0x7f,  # .Logical Maximum (127)              53
+        0x75, 0x08,  # .Report Size (8)                    55
+        0x95, 0x01,  # .Report Count (1)                   57
+        0x81, 0x06,  # .Input (Data,Var,Rel)               59
+        0xc0,        # End Collection                      61
+    ]
+    # fmt: on
+
+    def __init__(self, rdesc=report_descriptor, name=None, input_info=None):
+        super().__init__(rdesc, name, input_info)
+        self.wheel_multiplier = 1
+
+
+class TwoWheelMouse(WheelMouse):
+    # fmt: off
+    report_descriptor = [
+        0x05, 0x01,        # Usage Page (Generic Desktop)        0
+        0x09, 0x02,        # Usage (Mouse)                       2
+        0xa1, 0x01,        # Collection (Application)            4
+        0x09, 0x01,        # .Usage (Pointer)                    6
+        0xa1, 0x00,        # .Collection (Physical)              8
+        0x05, 0x09,        # ..Usage Page (Button)               10
+        0x19, 0x01,        # ..Usage Minimum (1)                 12
+        0x29, 0x10,        # ..Usage Maximum (16)                14
+        0x15, 0x00,        # ..Logical Minimum (0)               16
+        0x25, 0x01,        # ..Logical Maximum (1)               18
+        0x95, 0x10,        # ..Report Count (16)                 20
+        0x75, 0x01,        # ..Report Size (1)                   22
+        0x81, 0x02,        # ..Input (Data,Var,Abs)              24
+        0x05, 0x01,        # ..Usage Page (Generic Desktop)      26
+        0x16, 0x01, 0x80,  # ..Logical Minimum (-32767)          28
+        0x26, 0xff, 0x7f,  # ..Logical Maximum (32767)           31
+        0x75, 0x10,        # ..Report Size (16)                  34
+        0x95, 0x02,        # ..Report Count (2)                  36
+        0x09, 0x30,        # ..Usage (X)                         38
+        0x09, 0x31,        # ..Usage (Y)                         40
+        0x81, 0x06,        # ..Input (Data,Var,Rel)              42
+        0x15, 0x81,        # ..Logical Minimum (-127)            44
+        0x25, 0x7f,        # ..Logical Maximum (127)             46
+        0x75, 0x08,        # ..Report Size (8)                   48
+        0x95, 0x01,        # ..Report Count (1)                  50
+        0x09, 0x38,        # ..Usage (Wheel)                     52
+        0x81, 0x06,        # ..Input (Data,Var,Rel)              54
+        0x05, 0x0c,        # ..Usage Page (Consumer Devices)     56
+        0x0a, 0x38, 0x02,  # ..Usage (AC Pan)                    58
+        0x95, 0x01,        # ..Report Count (1)                  61
+        0x81, 0x06,        # ..Input (Data,Var,Rel)              63
+        0xc0,              # .End Collection                     65
+        0xc0,              # End Collection                      66
+    ]
+    # fmt: on
+
+    def __init__(self, rdesc=report_descriptor, name=None, input_info=None):
+        super().__init__(rdesc, name, input_info)
+        self.hwheel_multiplier = 1
+
+
+class MIDongleMIWirelessMouse(TwoWheelMouse):
+    # fmt: off
+    report_descriptor = [
+        0x05, 0x01,         # Usage Page (Generic Desktop)
+        0x09, 0x02,         # Usage (Mouse)
+        0xa1, 0x01,         # Collection (Application)
+        0x85, 0x01,         # .Report ID (1)
+        0x09, 0x01,         # .Usage (Pointer)
+        0xa1, 0x00,         # .Collection (Physical)
+        0x95, 0x05,         # ..Report Count (5)
+        0x75, 0x01,         # ..Report Size (1)
+        0x05, 0x09,         # ..Usage Page (Button)
+        0x19, 0x01,         # ..Usage Minimum (1)
+        0x29, 0x05,         # ..Usage Maximum (5)
+        0x15, 0x00,         # ..Logical Minimum (0)
+        0x25, 0x01,         # ..Logical Maximum (1)
+        0x81, 0x02,         # ..Input (Data,Var,Abs)
+        0x95, 0x01,         # ..Report Count (1)
+        0x75, 0x03,         # ..Report Size (3)
+        0x81, 0x01,         # ..Input (Cnst,Arr,Abs)
+        0x75, 0x08,         # ..Report Size (8)
+        0x95, 0x01,         # ..Report Count (1)
+        0x05, 0x01,         # ..Usage Page (Generic Desktop)
+        0x09, 0x38,         # ..Usage (Wheel)
+        0x15, 0x81,         # ..Logical Minimum (-127)
+        0x25, 0x7f,         # ..Logical Maximum (127)
+        0x81, 0x06,         # ..Input (Data,Var,Rel)
+        0x05, 0x0c,         # ..Usage Page (Consumer Devices)
+        0x0a, 0x38, 0x02,   # ..Usage (AC Pan)
+        0x95, 0x01,         # ..Report Count (1)
+        0x81, 0x06,         # ..Input (Data,Var,Rel)
+        0xc0,               # .End Collection
+        0x85, 0x02,         # .Report ID (2)
+        0x09, 0x01,         # .Usage (Consumer Control)
+        0xa1, 0x00,         # .Collection (Physical)
+        0x75, 0x0c,         # ..Report Size (12)
+        0x95, 0x02,         # ..Report Count (2)
+        0x05, 0x01,         # ..Usage Page (Generic Desktop)
+        0x09, 0x30,         # ..Usage (X)
+        0x09, 0x31,         # ..Usage (Y)
+        0x16, 0x01, 0xf8,   # ..Logical Minimum (-2047)
+        0x26, 0xff, 0x07,   # ..Logical Maximum (2047)
+        0x81, 0x06,         # ..Input (Data,Var,Rel)
+        0xc0,               # .End Collection
+        0xc0,               # End Collection
+        0x05, 0x0c,         # Usage Page (Consumer Devices)
+        0x09, 0x01,         # Usage (Consumer Control)
+        0xa1, 0x01,         # Collection (Application)
+        0x85, 0x03,         # .Report ID (3)
+        0x15, 0x00,         # .Logical Minimum (0)
+        0x25, 0x01,         # .Logical Maximum (1)
+        0x75, 0x01,         # .Report Size (1)
+        0x95, 0x01,         # .Report Count (1)
+        0x09, 0xcd,         # .Usage (Play/Pause)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0x0a, 0x83, 0x01,   # .Usage (AL Consumer Control Config)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0x09, 0xb5,         # .Usage (Scan Next Track)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0x09, 0xb6,         # .Usage (Scan Previous Track)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0x09, 0xea,         # .Usage (Volume Down)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0x09, 0xe9,         # .Usage (Volume Up)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0x0a, 0x25, 0x02,   # .Usage (AC Forward)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0x0a, 0x24, 0x02,   # .Usage (AC Back)
+        0x81, 0x06,         # .Input (Data,Var,Rel)
+        0xc0,               # End Collection
+    ]
+    # fmt: on
+    device_input_info = (BusType.USB, 0x2717, 0x003B)
+    device_name = "uhid test MI Dongle MI Wireless Mouse"
+
+    def __init__(
+        self, rdesc=report_descriptor, name=device_name, input_info=device_input_info
+    ):
+        super().__init__(rdesc, name, input_info)
+
+    def event(self, x, y, buttons=None, wheels=None):
+        # this mouse spreads the relative pointer and the mouse buttons
+        # onto 2 distinct reports
+        rs = []
+        r = self.create_report(x, y, buttons, wheels, reportID=1)
+        self.call_input_event(r)
+        rs.append(r)
+        r = self.create_report(x, y, buttons, reportID=2)
+        self.call_input_event(r)
+        rs.append(r)
+        return rs
+
+
+class ResolutionMultiplierMouse(TwoWheelMouse):
+    # fmt: off
+    report_descriptor = [
+        0x05, 0x01,        # Usage Page (Generic Desktop)        83
+        0x09, 0x02,        # Usage (Mouse)                       85
+        0xa1, 0x01,        # Collection (Application)            87
+        0x05, 0x01,        # .Usage Page (Generic Desktop)       89
+        0x09, 0x02,        # .Usage (Mouse)                      91
+        0xa1, 0x02,        # .Collection (Logical)               93
+        0x85, 0x11,        # ..Report ID (17)                    95
+        0x09, 0x01,        # ..Usage (Pointer)                   97
+        0xa1, 0x00,        # ..Collection (Physical)             99
+        0x05, 0x09,        # ...Usage Page (Button)              101
+        0x19, 0x01,        # ...Usage Minimum (1)                103
+        0x29, 0x03,        # ...Usage Maximum (3)                105
+        0x95, 0x03,        # ...Report Count (3)                 107
+        0x75, 0x01,        # ...Report Size (1)                  109
+        0x25, 0x01,        # ...Logical Maximum (1)              111
+        0x81, 0x02,        # ...Input (Data,Var,Abs)             113
+        0x95, 0x01,        # ...Report Count (1)                 115
+        0x81, 0x01,        # ...Input (Cnst,Arr,Abs)             117
+        0x09, 0x05,        # ...Usage (Vendor Usage 0x05)        119
+        0x81, 0x02,        # ...Input (Data,Var,Abs)             121
+        0x95, 0x03,        # ...Report Count (3)                 123
+        0x81, 0x01,        # ...Input (Cnst,Arr,Abs)             125
+        0x05, 0x01,        # ...Usage Page (Generic Desktop)     127
+        0x09, 0x30,        # ...Usage (X)                        129
+        0x09, 0x31,        # ...Usage (Y)                        131
+        0x95, 0x02,        # ...Report Count (2)                 133
+        0x75, 0x08,        # ...Report Size (8)                  135
+        0x15, 0x81,        # ...Logical Minimum (-127)           137
+        0x25, 0x7f,        # ...Logical Maximum (127)            139
+        0x81, 0x06,        # ...Input (Data,Var,Rel)             141
+        0xa1, 0x02,        # ...Collection (Logical)             143
+        0x85, 0x12,        # ....Report ID (18)                  145
+        0x09, 0x48,        # ....Usage (Resolution Multiplier)   147
+        0x95, 0x01,        # ....Report Count (1)                149
+        0x75, 0x02,        # ....Report Size (2)                 151
+        0x15, 0x00,        # ....Logical Minimum (0)             153
+        0x25, 0x01,        # ....Logical Maximum (1)             155
+        0x35, 0x01,        # ....Physical Minimum (1)            157
+        0x45, 0x04,        # ....Physical Maximum (4)            159
+        0xb1, 0x02,        # ....Feature (Data,Var,Abs)          161
+        0x35, 0x00,        # ....Physical Minimum (0)            163
+        0x45, 0x00,        # ....Physical Maximum (0)            165
+        0x75, 0x06,        # ....Report Size (6)                 167
+        0xb1, 0x01,        # ....Feature (Cnst,Arr,Abs)          169
+        0x85, 0x11,        # ....Report ID (17)                  171
+        0x09, 0x38,        # ....Usage (Wheel)                   173
+        0x15, 0x81,        # ....Logical Minimum (-127)          175
+        0x25, 0x7f,        # ....Logical Maximum (127)           177
+        0x75, 0x08,        # ....Report Size (8)                 179
+        0x81, 0x06,        # ....Input (Data,Var,Rel)            181
+        0xc0,              # ...End Collection                   183
+        0x05, 0x0c,        # ...Usage Page (Consumer Devices)    184
+        0x75, 0x08,        # ...Report Size (8)                  186
+        0x0a, 0x38, 0x02,  # ...Usage (AC Pan)                   188
+        0x81, 0x06,        # ...Input (Data,Var,Rel)             191
+        0xc0,              # ..End Collection                    193
+        0xc0,              # .End Collection                     194
+        0xc0,              # End Collection                      195
+    ]
+    # fmt: on
+
+    def __init__(self, rdesc=report_descriptor, name=None, input_info=None):
+        super().__init__(rdesc, name, input_info)
+        self.default_reportID = 0x11
+
+        # Feature Report 12, multiplier Feature value must be set to 0b01,
+        # i.e. 1. We should extract that from the descriptor instead
+        # of hardcoding it here, but meanwhile this will do.
+        self.set_feature_report = [0x12, 0x1]
+
+    def set_report(self, req, rnum, rtype, data):
+        if rtype != self.UHID_FEATURE_REPORT:
+            raise InvalidHIDCommunication(f"Unexpected report type: {rtype}")
+        if rnum != 0x12:
+            raise InvalidHIDCommunication(f"Unexpected report number: {rnum}")
+
+        if data != self.set_feature_report:
+            raise InvalidHIDCommunication(
+                f"Unexpected data: {data}, expected {self.set_feature_report}"
+            )
+
+        self.wheel_multiplier = 4
+
+        return 0
+
+
+class BadResolutionMultiplierMouse(ResolutionMultiplierMouse):
+    def set_report(self, req, rnum, rtype, data):
+        super().set_report(req, rnum, rtype, data)
+
+        self.wheel_multiplier = 1
+        self.hwheel_multiplier = 1
+        return 32  # EPIPE
+
+
+class ResolutionMultiplierHWheelMouse(TwoWheelMouse):
+    # fmt: off
+    report_descriptor = [
+        0x05, 0x01,         # Usage Page (Generic Desktop)        0
+        0x09, 0x02,         # Usage (Mouse)                       2
+        0xa1, 0x01,         # Collection (Application)            4
+        0x05, 0x01,         # .Usage Page (Generic Desktop)       6
+        0x09, 0x02,         # .Usage (Mouse)                      8
+        0xa1, 0x02,         # .Collection (Logical)               10
+        0x85, 0x1a,         # ..Report ID (26)                    12
+        0x09, 0x01,         # ..Usage (Pointer)                   14
+        0xa1, 0x00,         # ..Collection (Physical)             16
+        0x05, 0x09,         # ...Usage Page (Button)              18
+        0x19, 0x01,         # ...Usage Minimum (1)                20
+        0x29, 0x05,         # ...Usage Maximum (5)                22
+        0x95, 0x05,         # ...Report Count (5)                 24
+        0x75, 0x01,         # ...Report Size (1)                  26
+        0x15, 0x00,         # ...Logical Minimum (0)              28
+        0x25, 0x01,         # ...Logical Maximum (1)              30
+        0x81, 0x02,         # ...Input (Data,Var,Abs)             32
+        0x75, 0x03,         # ...Report Size (3)                  34
+        0x95, 0x01,         # ...Report Count (1)                 36
+        0x81, 0x01,         # ...Input (Cnst,Arr,Abs)             38
+        0x05, 0x01,         # ...Usage Page (Generic Desktop)     40
+        0x09, 0x30,         # ...Usage (X)                        42
+        0x09, 0x31,         # ...Usage (Y)                        44
+        0x95, 0x02,         # ...Report Count (2)                 46
+        0x75, 0x10,         # ...Report Size (16)                 48
+        0x16, 0x01, 0x80,   # ...Logical Minimum (-32767)         50
+        0x26, 0xff, 0x7f,   # ...Logical Maximum (32767)          53
+        0x81, 0x06,         # ...Input (Data,Var,Rel)             56
+        0xa1, 0x02,         # ...Collection (Logical)             58
+        0x85, 0x12,         # ....Report ID (18)                  60
+        0x09, 0x48,         # ....Usage (Resolution Multiplier)   62
+        0x95, 0x01,         # ....Report Count (1)                64
+        0x75, 0x02,         # ....Report Size (2)                 66
+        0x15, 0x00,         # ....Logical Minimum (0)             68
+        0x25, 0x01,         # ....Logical Maximum (1)             70
+        0x35, 0x01,         # ....Physical Minimum (1)            72
+        0x45, 0x0c,         # ....Physical Maximum (12)           74
+        0xb1, 0x02,         # ....Feature (Data,Var,Abs)          76
+        0x85, 0x1a,         # ....Report ID (26)                  78
+        0x09, 0x38,         # ....Usage (Wheel)                   80
+        0x35, 0x00,         # ....Physical Minimum (0)            82
+        0x45, 0x00,         # ....Physical Maximum (0)            84
+        0x95, 0x01,         # ....Report Count (1)                86
+        0x75, 0x10,         # ....Report Size (16)                88
+        0x16, 0x01, 0x80,   # ....Logical Minimum (-32767)        90
+        0x26, 0xff, 0x7f,   # ....Logical Maximum (32767)         93
+        0x81, 0x06,         # ....Input (Data,Var,Rel)            96
+        0xc0,               # ...End Collection                   98
+        0xa1, 0x02,         # ...Collection (Logical)             99
+        0x85, 0x12,         # ....Report ID (18)                  101
+        0x09, 0x48,         # ....Usage (Resolution Multiplier)   103
+        0x75, 0x02,         # ....Report Size (2)                 105
+        0x15, 0x00,         # ....Logical Minimum (0)             107
+        0x25, 0x01,         # ....Logical Maximum (1)             109
+        0x35, 0x01,         # ....Physical Minimum (1)            111
+        0x45, 0x0c,         # ....Physical Maximum (12)           113
+        0xb1, 0x02,         # ....Feature (Data,Var,Abs)          115
+        0x35, 0x00,         # ....Physical Minimum (0)            117
+        0x45, 0x00,         # ....Physical Maximum (0)            119
+        0x75, 0x04,         # ....Report Size (4)                 121
+        0xb1, 0x01,         # ....Feature (Cnst,Arr,Abs)          123
+        0x85, 0x1a,         # ....Report ID (26)                  125
+        0x05, 0x0c,         # ....Usage Page (Consumer Devices)   127
+        0x95, 0x01,         # ....Report Count (1)                129
+        0x75, 0x10,         # ....Report Size (16)                131
+        0x16, 0x01, 0x80,   # ....Logical Minimum (-32767)        133
+        0x26, 0xff, 0x7f,   # ....Logical Maximum (32767)         136
+        0x0a, 0x38, 0x02,   # ....Usage (AC Pan)                  139
+        0x81, 0x06,         # ....Input (Data,Var,Rel)            142
+        0xc0,               # ...End Collection                   144
+        0xc0,               # ..End Collection                    145
+        0xc0,               # .End Collection                     146
+        0xc0,               # End Collection                      147
+    ]
+    # fmt: on
+
+    def __init__(self, rdesc=report_descriptor, name=None, input_info=None):
+        super().__init__(rdesc, name, input_info)
+        self.default_reportID = 0x1A
+
+        # Feature Report 12, multiplier Feature value must be set to 0b0101,
+        # i.e. 5. We should extract that from the descriptor instead
+        # of hardcoding it here, but meanwhile this will do.
+        self.set_feature_report = [0x12, 0x5]
+
+    def set_report(self, req, rnum, rtype, data):
+        super().set_report(req, rnum, rtype, data)
+
+        self.wheel_multiplier = 12
+        self.hwheel_multiplier = 12
+
+        return 0
+
+
+class BaseTest:
+    class TestMouse(base.BaseTestCase.TestUhid):
+        def test_buttons(self):
+            """check for button reliability."""
+            uhdev = self.uhdev
+            evdev = uhdev.get_evdev()
+            syn_event = self.syn_event
+
+            r = uhdev.event(0, 0, (None, True, None))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_RIGHT, 1)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_RIGHT] == 1
+
+            r = uhdev.event(0, 0, (None, False, None))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_RIGHT, 0)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_RIGHT] == 0
+
+            r = uhdev.event(0, 0, (None, None, True))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_MIDDLE, 1)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_MIDDLE] == 1
+
+            r = uhdev.event(0, 0, (None, None, False))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_MIDDLE, 0)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_MIDDLE] == 0
+
+            r = uhdev.event(0, 0, (True, None, None))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_LEFT, 1)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_LEFT] == 1
+
+            r = uhdev.event(0, 0, (False, None, None))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_LEFT, 0)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_LEFT] == 0
+
+            r = uhdev.event(0, 0, (True, True, None))
+            expected_event0 = libevdev.InputEvent(libevdev.EV_KEY.BTN_LEFT, 1)
+            expected_event1 = libevdev.InputEvent(libevdev.EV_KEY.BTN_RIGHT, 1)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn(
+                (syn_event, expected_event0, expected_event1), events
+            )
+            assert evdev.value[libevdev.EV_KEY.BTN_RIGHT] == 1
+            assert evdev.value[libevdev.EV_KEY.BTN_LEFT] == 1
+
+            r = uhdev.event(0, 0, (False, None, None))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_LEFT, 0)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_RIGHT] == 1
+            assert evdev.value[libevdev.EV_KEY.BTN_LEFT] == 0
+
+            r = uhdev.event(0, 0, (None, False, None))
+            expected_event = libevdev.InputEvent(libevdev.EV_KEY.BTN_RIGHT, 0)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEventsIn((syn_event, expected_event), events)
+            assert evdev.value[libevdev.EV_KEY.BTN_RIGHT] == 0
+            assert evdev.value[libevdev.EV_KEY.BTN_LEFT] == 0
+
+        def test_relative(self):
+            """Check for relative events."""
+            uhdev = self.uhdev
+
+            syn_event = self.syn_event
+
+            r = uhdev.event(0, -1)
+            expected_event = libevdev.InputEvent(libevdev.EV_REL.REL_Y, -1)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEvents((syn_event, expected_event), events)
+
+            r = uhdev.event(1, 0)
+            expected_event = libevdev.InputEvent(libevdev.EV_REL.REL_X, 1)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEvents((syn_event, expected_event), events)
+
+            r = uhdev.event(-1, 2)
+            expected_event0 = libevdev.InputEvent(libevdev.EV_REL.REL_X, -1)
+            expected_event1 = libevdev.InputEvent(libevdev.EV_REL.REL_Y, 2)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEvents(
+                (syn_event, expected_event0, expected_event1), events
+            )
+
+
+class TestSimpleMouse(BaseTest.TestMouse):
+    def create_device(self):
+        return ButtonMouse()
+
+    def test_rdesc(self):
+        """Check that the testsuite actually manages to format the
+        reports according to the report descriptors.
+        No kernel device is used here"""
+        uhdev = self.uhdev
+
+        event = (0, 0, (None, None, None))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (0, 0, (None, True, None))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (0, 0, (True, True, None))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (0, 0, (False, False, False))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (1, 0, (True, False, True))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (-1, 0, (True, False, True))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (-5, 5, (True, False, True))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (-127, 127, (True, False, True))
+        assert uhdev.fake_report(*event) == uhdev.create_report(*event)
+
+        event = (0, -128, (True, False, True))
+        with pytest.raises(hidtools.hid.RangeError):
+            uhdev.create_report(*event)
+
+
+class TestWheelMouse(BaseTest.TestMouse):
+    def create_device(self):
+        return WheelMouse()
+
+    def is_wheel_highres(self, uhdev):
+        evdev = uhdev.get_evdev()
+        assert evdev.has(libevdev.EV_REL.REL_WHEEL)
+        return evdev.has(libevdev.EV_REL.REL_WHEEL_HI_RES)
+
+    def test_wheel(self):
+        uhdev = self.uhdev
+
+        # check if the kernel is high res wheel compatible
+        high_res_wheel = self.is_wheel_highres(uhdev)
+
+        syn_event = self.syn_event
+        # The Resolution Multiplier is applied to the HID reports, so we
+        # need to pre-multiply too.
+        mult = uhdev.wheel_multiplier
+
+        r = uhdev.event(0, 0, wheels=1 * mult)
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL, 1))
+        if high_res_wheel:
+            expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL_HI_RES, 120))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        r = uhdev.event(0, 0, wheels=-1 * mult)
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL, -1))
+        if high_res_wheel:
+            expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL_HI_RES, -120))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        r = uhdev.event(-1, 2, wheels=3 * mult)
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_X, -1))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_Y, 2))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL, 3))
+        if high_res_wheel:
+            expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL_HI_RES, 360))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+
+class TestTwoWheelMouse(TestWheelMouse):
+    def create_device(self):
+        return TwoWheelMouse()
+
+    def is_hwheel_highres(self, uhdev):
+        evdev = uhdev.get_evdev()
+        assert evdev.has(libevdev.EV_REL.REL_HWHEEL)
+        return evdev.has(libevdev.EV_REL.REL_HWHEEL_HI_RES)
+
+    def test_ac_pan(self):
+        uhdev = self.uhdev
+
+        # check if the kernel is high res wheel compatible
+        high_res_wheel = self.is_wheel_highres(uhdev)
+        high_res_hwheel = self.is_hwheel_highres(uhdev)
+        assert high_res_wheel == high_res_hwheel
+
+        syn_event = self.syn_event
+        # The Resolution Multiplier is applied to the HID reports, so we
+        # need to pre-multiply too.
+        hmult = uhdev.hwheel_multiplier
+        vmult = uhdev.wheel_multiplier
+
+        r = uhdev.event(0, 0, wheels=(0, 1 * hmult))
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL, 1))
+        if high_res_hwheel:
+            expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL_HI_RES, 120))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        r = uhdev.event(0, 0, wheels=(0, -1 * hmult))
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL, -1))
+        if high_res_hwheel:
+            expected.append(
+                libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL_HI_RES, -120)
+            )
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        r = uhdev.event(-1, 2, wheels=(0, 3 * hmult))
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_X, -1))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_Y, 2))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL, 3))
+        if high_res_hwheel:
+            expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL_HI_RES, 360))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        r = uhdev.event(-1, 2, wheels=(-3 * vmult, 4 * hmult))
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_X, -1))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_Y, 2))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL, -3))
+        if high_res_wheel:
+            expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL_HI_RES, -360))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL, 4))
+        if high_res_wheel:
+            expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL_HI_RES, 480))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+
+class TestResolutionMultiplierMouse(TestTwoWheelMouse):
+    def create_device(self):
+        return ResolutionMultiplierMouse()
+
+    def is_wheel_highres(self, uhdev):
+        high_res = super().is_wheel_highres(uhdev)
+
+        if not high_res:
+            # the kernel doesn't seem to support the high res wheel mice,
+            # make sure we haven't triggered the feature
+            assert uhdev.wheel_multiplier == 1
+
+        return high_res
+
+    def test_resolution_multiplier_wheel(self):
+        uhdev = self.uhdev
+
+        if not self.is_wheel_highres(uhdev):
+            pytest.skip("Kernel not compatible, we can not trigger the conditions")
+
+        assert uhdev.wheel_multiplier > 1
+        assert 120 % uhdev.wheel_multiplier == 0
+
+    def test_wheel_with_multiplier(self):
+        uhdev = self.uhdev
+
+        if not self.is_wheel_highres(uhdev):
+            pytest.skip("Kernel not compatible, we can not trigger the conditions")
+
+        assert uhdev.wheel_multiplier > 1
+
+        syn_event = self.syn_event
+        mult = uhdev.wheel_multiplier
+
+        r = uhdev.event(0, 0, wheels=1)
+        expected = [syn_event]
+        expected.append(
+            libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL_HI_RES, 120 / mult)
+        )
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        r = uhdev.event(0, 0, wheels=-1)
+        expected = [syn_event]
+        expected.append(
+            libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL_HI_RES, -120 / mult)
+        )
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_X, 1))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_Y, -2))
+        expected.append(
+            libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL_HI_RES, 120 / mult)
+        )
+
+        for _ in range(mult - 1):
+            r = uhdev.event(1, -2, wheels=1)
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEvents(expected, events)
+
+        r = uhdev.event(1, -2, wheels=1)
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_WHEEL, 1))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+
+class TestBadResolutionMultiplierMouse(TestTwoWheelMouse):
+    def create_device(self):
+        return BadResolutionMultiplierMouse()
+
+    def is_wheel_highres(self, uhdev):
+        high_res = super().is_wheel_highres(uhdev)
+
+        assert uhdev.wheel_multiplier == 1
+
+        return high_res
+
+    def test_resolution_multiplier_wheel(self):
+        uhdev = self.uhdev
+
+        assert uhdev.wheel_multiplier == 1
+
+
+class TestResolutionMultiplierHWheelMouse(TestResolutionMultiplierMouse):
+    def create_device(self):
+        return ResolutionMultiplierHWheelMouse()
+
+    def is_hwheel_highres(self, uhdev):
+        high_res = super().is_hwheel_highres(uhdev)
+
+        if not high_res:
+            # the kernel doesn't seem to support the high res wheel mice,
+            # make sure we haven't triggered the feature
+            assert uhdev.hwheel_multiplier == 1
+
+        return high_res
+
+    def test_resolution_multiplier_ac_pan(self):
+        uhdev = self.uhdev
+
+        if not self.is_hwheel_highres(uhdev):
+            pytest.skip("Kernel not compatible, we can not trigger the conditions")
+
+        assert uhdev.hwheel_multiplier > 1
+        assert 120 % uhdev.hwheel_multiplier == 0
+
+    def test_ac_pan_with_multiplier(self):
+        uhdev = self.uhdev
+
+        if not self.is_hwheel_highres(uhdev):
+            pytest.skip("Kernel not compatible, we can not trigger the conditions")
+
+        assert uhdev.hwheel_multiplier > 1
+
+        syn_event = self.syn_event
+        hmult = uhdev.hwheel_multiplier
+
+        r = uhdev.event(0, 0, wheels=(0, 1))
+        expected = [syn_event]
+        expected.append(
+            libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL_HI_RES, 120 / hmult)
+        )
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        r = uhdev.event(0, 0, wheels=(0, -1))
+        expected = [syn_event]
+        expected.append(
+            libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL_HI_RES, -120 / hmult)
+        )
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+        expected = [syn_event]
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_X, 1))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_Y, -2))
+        expected.append(
+            libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL_HI_RES, 120 / hmult)
+        )
+
+        for _ in range(hmult - 1):
+            r = uhdev.event(1, -2, wheels=(0, 1))
+            events = uhdev.next_sync_events()
+            self.debug_reports(r, uhdev, events)
+            self.assertInputEvents(expected, events)
+
+        r = uhdev.event(1, -2, wheels=(0, 1))
+        expected.append(libevdev.InputEvent(libevdev.EV_REL.REL_HWHEEL, 1))
+        events = uhdev.next_sync_events()
+        self.debug_reports(r, uhdev, events)
+        self.assertInputEvents(expected, events)
+
+
+class TestMiMouse(TestWheelMouse):
+    def create_device(self):
+        return MIDongleMIWirelessMouse()
+
+    def assertInputEvents(self, expected_events, effective_events):
+        # Buttons and x/y are spread over two HID reports, so we can get two
+        # event frames for this device.
+        remaining = self.assertInputEventsIn(expected_events, effective_events)
+        try:
+            remaining.remove(libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0))
+        except ValueError:
+            # If there's no SYN_REPORT in the list, continue and let the
+            # assert below print out the real error
+            pass
+        assert remaining == []