Commit d52f5206 authored by Benjamin Tissoires's avatar Benjamin Tissoires
Browse files

selftests/hid: tablets: move the transitions to PenState



Those transitions have nothing to do with `Pen`, so migrate them to
`PenState`.

The hidden agenda is to remove `Pen` and integrate it into `PenDigitizer`
so that we can tweak the events in each state to emulate firmware bugs.

Reviewed-by: default avatarPeter Hutterer <peter.hutterer@who-t.net>
Acked-by: default avatarJiri Kosina <jkosina@suse.com>
Link: https://lore.kernel.org/r/20231206-wip-selftests-v2-5-c0350c2f5986@kernel.org


Signed-off-by: default avatarBenjamin Tissoires <bentiss@kernel.org>
parent b5edacf7
Loading
Loading
Loading
Loading
+109 −106
Original line number Diff line number Diff line
@@ -132,104 +132,8 @@ class PenState(Enum):

        return tuple()


class Pen(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.tipswitch = False
        self.tippressure = 15
        self.azimuth = 0
        self.inrange = False
        self.width = 10
        self.height = 10
        self.barrelswitch = False
        self.invert = False
        self.eraser = False
        self.x_tilt = 0
        self.y_tilt = 0
        self.twist = 0
        self._old_values = None
        self.current_state = None

    def _restore(self):
        if self._old_values is not None:
            for i in [
                "x",
                "y",
                "tippressure",
                "azimuth",
                "width",
                "height",
                "twist",
                "x_tilt",
                "y_tilt",
            ]:
                setattr(self, i, getattr(self._old_values, i))

    def move_to(self, state):
        # fill in the previous values
        if self.current_state == PenState.PEN_IS_OUT_OF_RANGE:
            self._restore()

        print(f"\n  *** pen is moving to {state} ***")

        if state == PenState.PEN_IS_OUT_OF_RANGE:
            self._old_values = copy.copy(self)
            self.x = 0
            self.y = 0
            self.tipswitch = False
            self.tippressure = 0
            self.azimuth = 0
            self.inrange = False
            self.width = 0
            self.height = 0
            self.invert = False
            self.eraser = False
            self.x_tilt = 0
            self.y_tilt = 0
            self.twist = 0
        elif state == PenState.PEN_IS_IN_RANGE:
            self.tipswitch = False
            self.inrange = True
            self.invert = False
            self.eraser = False
        elif state == PenState.PEN_IS_IN_CONTACT:
            self.tipswitch = True
            self.inrange = True
            self.invert = False
            self.eraser = False
        elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:
            self.tipswitch = False
            self.inrange = True
            self.invert = True
            self.eraser = False
        elif state == PenState.PEN_IS_ERASING:
            self.tipswitch = False
            self.inrange = True
            self.invert = True
            self.eraser = True

        self.current_state = state

    def __assert_axis(self, evdev, axis, value):
        if (
            axis == libevdev.EV_KEY.BTN_TOOL_RUBBER
            and evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER] is None
        ):
            return

        assert (
            evdev.value[axis] == value
        ), f"assert evdev.value[{axis}] ({evdev.value[axis]}) != {value}"

    def assert_expected_input_events(self, evdev):
        assert evdev.value[libevdev.EV_ABS.ABS_X] == self.x
        assert evdev.value[libevdev.EV_ABS.ABS_Y] == self.y
        assert self.current_state == PenState.from_evdev(evdev)

    @staticmethod
    def legal_transitions() -> Dict[str, Tuple[PenState, ...]]:
    def legal_transitions() -> Dict[str, Tuple["PenState", ...]]:
        """This is the first half of the Windows Pen Implementation state machine:
        we don't have Invert nor Erase bits, so just move in/out-of-range or proximity.
        https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states
@@ -255,7 +159,7 @@ class Pen(object):
        }

    @staticmethod
    def legal_transitions_with_invert() -> Dict[str, Tuple[PenState, ...]]:
    def legal_transitions_with_invert() -> Dict[str, Tuple["PenState", ...]]:
        """This is the second half of the Windows Pen Implementation state machine:
        we now have Invert and Erase bits, so move in/out or proximity with the intend
        to erase.
@@ -293,7 +197,7 @@ class Pen(object):
        }

    @staticmethod
    def tolerated_transitions() -> Dict[str, Tuple[PenState, ...]]:
    def tolerated_transitions() -> Dict[str, Tuple["PenState", ...]]:
        """This is not adhering to the Windows Pen Implementation state machine
        but we should expect the kernel to behave properly, mostly for historical
        reasons."""
@@ -306,7 +210,7 @@ class Pen(object):
        }

    @staticmethod
    def tolerated_transitions_with_invert() -> Dict[str, Tuple[PenState, ...]]:
    def tolerated_transitions_with_invert() -> Dict[str, Tuple["PenState", ...]]:
        """This is the second half of the Windows Pen Implementation state machine:
        we now have Invert and Erase bits, so move in/out or proximity with the intend
        to erase.
@@ -321,7 +225,7 @@ class Pen(object):
        }

    @staticmethod
    def broken_transitions() -> Dict[str, Tuple[PenState, ...]]:
    def broken_transitions() -> Dict[str, Tuple["PenState", ...]]:
        """Those tests are definitely not part of the Windows specification.
        However, a half broken device might export those transitions.
        For example, a pen that has the eraser button might wobble between
@@ -359,6 +263,102 @@ class Pen(object):
        }


class Pen(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.tipswitch = False
        self.tippressure = 15
        self.azimuth = 0
        self.inrange = False
        self.width = 10
        self.height = 10
        self.barrelswitch = False
        self.invert = False
        self.eraser = False
        self.x_tilt = 0
        self.y_tilt = 0
        self.twist = 0
        self._old_values = None
        self.current_state = None

    def _restore(self):
        if self._old_values is not None:
            for i in [
                "x",
                "y",
                "tippressure",
                "azimuth",
                "width",
                "height",
                "twist",
                "x_tilt",
                "y_tilt",
            ]:
                setattr(self, i, getattr(self._old_values, i))

    def move_to(self, state):
        # fill in the previous values
        if self.current_state == PenState.PEN_IS_OUT_OF_RANGE:
            self._restore()

        print(f"\n  *** pen is moving to {state} ***")

        if state == PenState.PEN_IS_OUT_OF_RANGE:
            self._old_values = copy.copy(self)
            self.x = 0
            self.y = 0
            self.tipswitch = False
            self.tippressure = 0
            self.azimuth = 0
            self.inrange = False
            self.width = 0
            self.height = 0
            self.invert = False
            self.eraser = False
            self.x_tilt = 0
            self.y_tilt = 0
            self.twist = 0
        elif state == PenState.PEN_IS_IN_RANGE:
            self.tipswitch = False
            self.inrange = True
            self.invert = False
            self.eraser = False
        elif state == PenState.PEN_IS_IN_CONTACT:
            self.tipswitch = True
            self.inrange = True
            self.invert = False
            self.eraser = False
        elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:
            self.tipswitch = False
            self.inrange = True
            self.invert = True
            self.eraser = False
        elif state == PenState.PEN_IS_ERASING:
            self.tipswitch = False
            self.inrange = True
            self.invert = True
            self.eraser = True

        self.current_state = state

    def __assert_axis(self, evdev, axis, value):
        if (
            axis == libevdev.EV_KEY.BTN_TOOL_RUBBER
            and evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER] is None
        ):
            return

        assert (
            evdev.value[axis] == value
        ), f"assert evdev.value[{axis}] ({evdev.value[axis]}) != {value}"

    def assert_expected_input_events(self, evdev):
        assert evdev.value[libevdev.EV_ABS.ABS_X] == self.x
        assert evdev.value[libevdev.EV_ABS.ABS_Y] == self.y
        assert self.current_state == PenState.from_evdev(evdev)


class PenDigitizer(base.UHIDTestDevice):
    def __init__(
        self,
@@ -486,7 +486,7 @@ class BaseTest:
        @pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
        @pytest.mark.parametrize(
            "state_list",
            [pytest.param(v, id=k) for k, v in Pen.legal_transitions().items()],
            [pytest.param(v, id=k) for k, v in PenState.legal_transitions().items()],
        )
        def test_valid_pen_states(self, state_list, scribble):
            """This is the first half of the Windows Pen Implementation state machine:
@@ -498,7 +498,10 @@ class BaseTest:
        @pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
        @pytest.mark.parametrize(
            "state_list",
            [pytest.param(v, id=k) for k, v in Pen.tolerated_transitions().items()],
            [
                pytest.param(v, id=k)
                for k, v in PenState.tolerated_transitions().items()
            ],
        )
        def test_tolerated_pen_states(self, state_list, scribble):
            """This is not adhering to the Windows Pen Implementation state machine
@@ -515,7 +518,7 @@ class BaseTest:
            "state_list",
            [
                pytest.param(v, id=k)
                for k, v in Pen.legal_transitions_with_invert().items()
                for k, v in PenState.legal_transitions_with_invert().items()
            ],
        )
        def test_valid_invert_pen_states(self, state_list, scribble):
@@ -535,7 +538,7 @@ class BaseTest:
            "state_list",
            [
                pytest.param(v, id=k)
                for k, v in Pen.tolerated_transitions_with_invert().items()
                for k, v in PenState.tolerated_transitions_with_invert().items()
            ],
        )
        def test_tolerated_invert_pen_states(self, state_list, scribble):
@@ -553,7 +556,7 @@ class BaseTest:
        @pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
        @pytest.mark.parametrize(
            "state_list",
            [pytest.param(v, id=k) for k, v in Pen.broken_transitions().items()],
            [pytest.param(v, id=k) for k, v in PenState.broken_transitions().items()],
        )
        def test_tolerated_broken_pen_states(self, state_list, scribble):
            """Those tests are definitely not part of the Windows specification.