Merge branch 'for-6.8/selftests' into for-linus

- greatly improved coverage of Tablets in hid-selftests (Benjamin Tissoires)
This commit is contained in:
Jiri Kosina 2024-01-08 21:11:10 +01:00
commit 1cb09b552b
5 changed files with 575 additions and 278 deletions

View File

@ -14,7 +14,7 @@ import logging
from hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile
from pathlib import Path
from typing import Final
from typing import Final, List, Tuple
logger = logging.getLogger("hidtools.test.base")
@ -155,7 +155,7 @@ class BaseTestCase:
# if any module is not available (not compiled), the test will skip.
# Each element is a tuple '(kernel driver name, kernel module)',
# for example ("playstation", "hid-playstation")
kernel_modules = []
kernel_modules: List[Tuple[str, str]] = []
def assertInputEventsIn(self, expected_events, effective_events):
effective_events = effective_events.copy()
@ -238,8 +238,7 @@ class BaseTestCase:
try:
with HIDTestUdevRule.instance():
with new_uhdev as self.uhdev:
skip_cond = request.node.get_closest_marker("skip_if_uhdev")
if skip_cond:
for skip_cond in request.node.iter_markers("skip_if_uhdev"):
test, message, *rest = skip_cond.args
if test(self.uhdev):

View File

@ -52,13 +52,13 @@ class BaseMouse(base.UHIDTestDevice):
: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, right, middle = buttons
if left is not None:
self.left = left
if right is not None:
self.right = right
if middle is not None:
self.middle = middle
left = self.left
right = self.right
middle = self.middle

View File

@ -13,62 +13,133 @@ from hidtools.util import BusType
import libevdev
import logging
import pytest
from typing import Dict, Tuple
from typing import Dict, List, Optional, Tuple
logger = logging.getLogger("hidtools.test.tablet")
class BtnTouch(Enum):
"""Represents whether the BTN_TOUCH event is set to True or False"""
DOWN = True
UP = False
class ToolType(Enum):
PEN = libevdev.EV_KEY.BTN_TOOL_PEN
RUBBER = libevdev.EV_KEY.BTN_TOOL_RUBBER
class BtnPressed(Enum):
"""Represents whether a button is pressed on the stylus"""
PRIMARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS
SECONDARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS2
class PenState(Enum):
"""Pen states according to Microsoft reference:
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states
We extend it with the various buttons when we need to check them.
"""
PEN_IS_OUT_OF_RANGE = (False, None)
PEN_IS_IN_RANGE = (False, libevdev.EV_KEY.BTN_TOOL_PEN)
PEN_IS_IN_CONTACT = (True, libevdev.EV_KEY.BTN_TOOL_PEN)
PEN_IS_IN_RANGE_WITH_ERASING_INTENT = (False, libevdev.EV_KEY.BTN_TOOL_RUBBER)
PEN_IS_ERASING = (True, libevdev.EV_KEY.BTN_TOOL_RUBBER)
PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None, None
PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN, None
PEN_IS_IN_RANGE_WITH_BUTTON = BtnTouch.UP, ToolType.PEN, BtnPressed.PRIMARY_PRESSED
PEN_IS_IN_RANGE_WITH_SECOND_BUTTON = (
BtnTouch.UP,
ToolType.PEN,
BtnPressed.SECONDARY_PRESSED,
)
PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN, None
PEN_IS_IN_CONTACT_WITH_BUTTON = (
BtnTouch.DOWN,
ToolType.PEN,
BtnPressed.PRIMARY_PRESSED,
)
PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON = (
BtnTouch.DOWN,
ToolType.PEN,
BtnPressed.SECONDARY_PRESSED,
)
PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER, None
PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON = (
BtnTouch.UP,
ToolType.RUBBER,
BtnPressed.PRIMARY_PRESSED,
)
PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_SECOND_BUTTON = (
BtnTouch.UP,
ToolType.RUBBER,
BtnPressed.SECONDARY_PRESSED,
)
PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER, None
PEN_IS_ERASING_WITH_BUTTON = (
BtnTouch.DOWN,
ToolType.RUBBER,
BtnPressed.PRIMARY_PRESSED,
)
PEN_IS_ERASING_WITH_SECOND_BUTTON = (
BtnTouch.DOWN,
ToolType.RUBBER,
BtnPressed.SECONDARY_PRESSED,
)
def __init__(self, touch, tool):
self.touch = touch
self.tool = tool
def __init__(self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[BtnPressed]):
self.touch = touch # type: ignore
self.tool = tool # type: ignore
self.button = button # type: ignore
@classmethod
def from_evdev(cls, evdev) -> "PenState":
touch = bool(evdev.value[libevdev.EV_KEY.BTN_TOUCH])
touch = BtnTouch(evdev.value[libevdev.EV_KEY.BTN_TOUCH])
tool = None
button = None
if (
evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]
and not evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]
):
tool = libevdev.EV_KEY.BTN_TOOL_RUBBER
tool = ToolType(libevdev.EV_KEY.BTN_TOOL_RUBBER)
elif (
evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]
and not evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]
):
tool = libevdev.EV_KEY.BTN_TOOL_PEN
tool = ToolType(libevdev.EV_KEY.BTN_TOOL_PEN)
elif (
evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]
or evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]
):
raise ValueError("2 tools are not allowed")
return cls((touch, tool))
# we take only the highest button in account
for b in [libevdev.EV_KEY.BTN_STYLUS, libevdev.EV_KEY.BTN_STYLUS2]:
if bool(evdev.value[b]):
button = BtnPressed(b)
def apply(self, events) -> "PenState":
# the kernel tends to insert an EV_SYN once removing the tool, so
# the button will be released after
if tool is None:
button = None
return cls((touch, tool, button)) # type: ignore
def apply(self, events: List[libevdev.InputEvent], strict: bool) -> "PenState":
if libevdev.EV_SYN.SYN_REPORT in events:
raise ValueError("EV_SYN is in the event sequence")
touch = self.touch
touch_found = False
tool = self.tool
tool_found = False
button = self.button
button_found = False
for ev in events:
if ev == libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH):
if touch_found:
raise ValueError(f"duplicated BTN_TOUCH in {events}")
touch_found = True
touch = bool(ev.value)
touch = BtnTouch(ev.value)
elif ev in (
libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN),
libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_RUBBER),
@ -76,19 +147,113 @@ class PenState(Enum):
if tool_found:
raise ValueError(f"duplicated BTN_TOOL_* in {events}")
tool_found = True
if ev.value:
tool = ev.code
else:
tool = None
tool = ToolType(ev.code) if ev.value else None
elif ev in (
libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS),
libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2),
):
if button_found:
raise ValueError(f"duplicated BTN_STYLUS* in {events}")
button_found = True
button = BtnPressed(ev.code) if ev.value else None
new_state = PenState((touch, tool))
assert (
new_state in self.valid_transitions()
), f"moving from {self} to {new_state} is forbidden"
# the kernel tends to insert an EV_SYN once removing the tool, so
# the button will be released after
if tool is None:
button = None
new_state = PenState((touch, tool, button)) # type: ignore
if strict:
assert (
new_state in self.valid_transitions()
), f"moving from {self} to {new_state} is forbidden"
else:
assert (
new_state in self.historically_tolerated_transitions()
), f"moving from {self} to {new_state} is forbidden"
return new_state
def valid_transitions(self) -> Tuple["PenState", ...]:
"""Following the state machine in the URL above.
Note that those transitions are from the evdev point of view, not HID"""
if self == PenState.PEN_IS_OUT_OF_RANGE:
return (
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_ERASING,
)
if self == PenState.PEN_IS_IN_RANGE:
return (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT,
)
if self == PenState.PEN_IS_IN_CONTACT:
return (
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
)
if self == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:
return (
PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_ERASING,
)
if self == PenState.PEN_IS_ERASING:
return (
PenState.PEN_IS_ERASING,
PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,
)
if self == PenState.PEN_IS_IN_RANGE_WITH_BUTTON:
return (
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
)
if self == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON:
return (
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
)
if self == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON:
return (
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
)
if self == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON:
return (
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
)
return tuple()
def historically_tolerated_transitions(self) -> Tuple["PenState", ...]:
"""Following the state machine in the URL above, with a couple of addition
for skipping the in-range state, due to historical reasons.
@ -97,14 +262,20 @@ class PenState(Enum):
return (
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_ERASING,
)
if self == PenState.PEN_IS_IN_RANGE:
return (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT,
)
@ -112,6 +283,8 @@ class PenState(Enum):
if self == PenState.PEN_IS_IN_CONTACT:
return (
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE,
)
@ -130,110 +303,42 @@ class PenState(Enum):
PenState.PEN_IS_OUT_OF_RANGE,
)
if self == PenState.PEN_IS_IN_RANGE_WITH_BUTTON:
return (
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
)
if self == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON:
return (
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_OUT_OF_RANGE,
)
if self == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON:
return (
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
)
if self == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON:
return (
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_OUT_OF_RANGE,
)
return tuple()
class Data(object):
pass
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
@ -259,7 +364,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.
@ -297,7 +402,106 @@ class Pen(object):
}
@staticmethod
def tolerated_transitions() -> Dict[str, Tuple[PenState, ...]]:
def legal_transitions_with_primary_button() -> Dict[str, Tuple["PenState", ...]]:
"""We revisit the Windows Pen Implementation state machine:
we now have a primary button.
"""
return {
"hover-button": (PenState.PEN_IS_IN_RANGE_WITH_BUTTON,),
"hover-button -> out-of-range": (
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_OUT_OF_RANGE,
),
"in-range -> button-press": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
),
"in-range -> button-press -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE,
),
"in-range -> touch -> button-press -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT,
),
"in-range -> touch -> button-press -> release -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE,
),
"in-range -> button-press -> touch -> release -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_RANGE,
),
"in-range -> button-press -> touch -> button-release -> release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_RANGE,
),
}
@staticmethod
def legal_transitions_with_secondary_button() -> Dict[str, Tuple["PenState", ...]]:
"""We revisit the Windows Pen Implementation state machine:
we now have a secondary button.
Note: we don't looks for 2 buttons interactions.
"""
return {
"hover-button": (PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,),
"hover-button -> out-of-range": (
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_OUT_OF_RANGE,
),
"in-range -> button-press": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
),
"in-range -> button-press -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
),
"in-range -> touch -> button-press -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_CONTACT,
),
"in-range -> touch -> button-press -> release -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
),
"in-range -> button-press -> touch -> release -> button-release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
),
"in-range -> button-press -> touch -> button-release -> release": (
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_RANGE,
),
}
@staticmethod
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."""
@ -310,7 +514,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.
@ -325,7 +529,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
@ -363,6 +567,61 @@ 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.secondarybarrelswitch = False
self.invert = False
self.eraser = False
self.xtilt = 1
self.ytilt = 1
self.twist = 1
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",
"xtilt",
"ytilt",
]:
setattr(self, i, getattr(self._old_values, i))
def backup(self):
self._old_values = copy.copy(self)
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,
@ -388,6 +647,89 @@ class PenDigitizer(base.UHIDTestDevice):
continue
self.fields = [f.usage_name for f in r]
def move_to(self, pen, state):
# fill in the previous values
if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE:
pen.restore()
print(f"\n *** pen is moving to {state} ***")
if state == PenState.PEN_IS_OUT_OF_RANGE:
pen.backup()
pen.x = 0
pen.y = 0
pen.tipswitch = False
pen.tippressure = 0
pen.azimuth = 0
pen.inrange = False
pen.width = 0
pen.height = 0
pen.invert = False
pen.eraser = False
pen.xtilt = 0
pen.ytilt = 0
pen.twist = 0
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_RANGE:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_CONTACT:
pen.tipswitch = True
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = True
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON:
pen.tipswitch = True
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = True
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = True
elif state == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON:
pen.tipswitch = True
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = True
elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:
pen.tipswitch = False
pen.inrange = True
pen.invert = True
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_ERASING:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = True
pen.barrelswitch = False
pen.secondarybarrelswitch = False
pen.current_state = state
def event(self, pen):
rs = []
r = self.create_report(application=self.cur_application, data=pen)
@ -435,10 +777,14 @@ class BaseTest:
self.debug_reports(r, uhdev, events)
return events
def validate_transitions(self, from_state, pen, evdev, events):
def validate_transitions(
self, from_state, pen, evdev, events, allow_intermediate_states
):
# check that the final state is correct
pen.assert_expected_input_events(evdev)
state = from_state
# check that the transitions are valid
sync_events = []
while libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT) in events:
@ -448,12 +794,12 @@ class BaseTest:
events = events[idx + 1 :]
# now check for a valid transition
from_state = from_state.apply(sync_events)
state = state.apply(sync_events, not allow_intermediate_states)
if events:
from_state = from_state.apply(sync_events)
state = state.apply(sync_events, not allow_intermediate_states)
def _test_states(self, state_list, scribble):
def _test_states(self, state_list, scribble, allow_intermediate_states):
"""Internal method to test against a list of
transition between states.
state_list is a list of PenState objects
@ -466,9 +812,11 @@ class BaseTest:
cur_state = PenState.PEN_IS_OUT_OF_RANGE
p = Pen(50, 60)
p.move_to(PenState.PEN_IS_OUT_OF_RANGE)
uhdev.move_to(p, PenState.PEN_IS_OUT_OF_RANGE)
events = self.post(uhdev, p)
self.validate_transitions(cur_state, p, evdev, events)
self.validate_transitions(
cur_state, p, evdev, events, allow_intermediate_states
)
cur_state = p.current_state
@ -477,38 +825,77 @@ class BaseTest:
p.x += 1
p.y -= 1
events = self.post(uhdev, p)
self.validate_transitions(cur_state, p, evdev, events)
self.validate_transitions(
cur_state, p, evdev, events, allow_intermediate_states
)
assert len(events) >= 3 # X, Y, SYN
p.move_to(state)
uhdev.move_to(p, state)
if scribble and state != PenState.PEN_IS_OUT_OF_RANGE:
p.x += 1
p.y -= 1
events = self.post(uhdev, p)
self.validate_transitions(cur_state, p, evdev, events)
self.validate_transitions(
cur_state, p, evdev, events, allow_intermediate_states
)
cur_state = p.current_state
@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:
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
"""
self._test_states(state_list, scribble)
self._test_states(state_list, scribble, allow_intermediate_states=False)
@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
but we should expect the kernel to behave properly, mostly for historical
reasons."""
self._test_states(state_list, scribble)
self._test_states(state_list, scribble, allow_intermediate_states=True)
@pytest.mark.skip_if_uhdev(
lambda uhdev: "Barrel Switch" not in uhdev.fields,
"Device not compatible, missing Barrel Switch usage",
)
@pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
@pytest.mark.parametrize(
"state_list",
[
pytest.param(v, id=k)
for k, v in PenState.legal_transitions_with_primary_button().items()
],
)
def test_valid_primary_button_pen_states(self, state_list, scribble):
"""Rework the transition state machine by adding the primary button."""
self._test_states(state_list, scribble, allow_intermediate_states=False)
@pytest.mark.skip_if_uhdev(
lambda uhdev: "Secondary Barrel Switch" not in uhdev.fields,
"Device not compatible, missing Secondary Barrel Switch usage",
)
@pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
@pytest.mark.parametrize(
"state_list",
[
pytest.param(v, id=k)
for k, v in PenState.legal_transitions_with_secondary_button().items()
],
)
def test_valid_secondary_button_pen_states(self, state_list, scribble):
"""Rework the transition state machine by adding the secondary button."""
self._test_states(state_list, scribble, allow_intermediate_states=False)
@pytest.mark.skip_if_uhdev(
lambda uhdev: "Invert" not in uhdev.fields,
@ -519,7 +906,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):
@ -528,7 +915,7 @@ class BaseTest:
to erase.
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states
"""
self._test_states(state_list, scribble)
self._test_states(state_list, scribble, allow_intermediate_states=False)
@pytest.mark.skip_if_uhdev(
lambda uhdev: "Invert" not in uhdev.fields,
@ -539,7 +926,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):
@ -548,7 +935,7 @@ class BaseTest:
to erase.
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states
"""
self._test_states(state_list, scribble)
self._test_states(state_list, scribble, allow_intermediate_states=True)
@pytest.mark.skip_if_uhdev(
lambda uhdev: "Invert" not in uhdev.fields,
@ -557,7 +944,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.
@ -565,102 +952,7 @@ class BaseTest:
For example, a pen that has the eraser button might wobble between
touching and erasing if the tablet doesn't enforce the Windows
state machine."""
self._test_states(state_list, scribble)
@pytest.mark.skip_if_uhdev(
lambda uhdev: "Barrel Switch" not in uhdev.fields,
"Device not compatible, missing Barrel Switch usage",
)
def test_primary_button(self):
"""Primary button (stylus) pressed, reports as pressed even while hovering.
Actual reporting from the device: hid=TIPSWITCH,BARRELSWITCH,INRANGE (code=TOUCH,STYLUS,PEN):
{ 0, 0, 1 } <- hover
{ 0, 1, 1 } <- primary button pressed
{ 0, 1, 1 } <- liftoff
{ 0, 0, 0 } <- leaves
"""
uhdev = self.uhdev
evdev = uhdev.get_evdev()
p = Pen(50, 60)
p.inrange = True
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1) in events
assert evdev.value[libevdev.EV_ABS.ABS_X] == 50
assert evdev.value[libevdev.EV_ABS.ABS_Y] == 60
assert not evdev.value[libevdev.EV_KEY.BTN_STYLUS]
p.barrelswitch = True
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 1) in events
p.x += 1
p.y -= 1
events = self.post(uhdev, p)
assert len(events) == 3 # X, Y, SYN
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 51) in events
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 59) in events
p.barrelswitch = False
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 0) in events
p.inrange = False
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 0) in events
@pytest.mark.skip_if_uhdev(
lambda uhdev: "Barrel Switch" not in uhdev.fields,
"Device not compatible, missing Barrel Switch usage",
)
def test_contact_primary_button(self):
"""Primary button (stylus) pressed, reports as pressed even while hovering.
Actual reporting from the device: hid=TIPSWITCH,BARRELSWITCH,INRANGE (code=TOUCH,STYLUS,PEN):
{ 0, 0, 1 } <- hover
{ 0, 1, 1 } <- primary button pressed
{ 1, 1, 1 } <- touch-down
{ 1, 1, 1 } <- still touch, scribble on the screen
{ 0, 1, 1 } <- liftoff
{ 0, 0, 0 } <- leaves
"""
uhdev = self.uhdev
evdev = uhdev.get_evdev()
p = Pen(50, 60)
p.inrange = True
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1) in events
assert evdev.value[libevdev.EV_ABS.ABS_X] == 50
assert evdev.value[libevdev.EV_ABS.ABS_Y] == 60
assert not evdev.value[libevdev.EV_KEY.BTN_STYLUS]
p.barrelswitch = True
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 1) in events
p.tipswitch = True
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events
assert evdev.value[libevdev.EV_KEY.BTN_STYLUS]
p.x += 1
p.y -= 1
events = self.post(uhdev, p)
assert len(events) == 3 # X, Y, SYN
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 51) in events
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 59) in events
p.tipswitch = False
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0) in events
p.barrelswitch = False
p.inrange = False
events = self.post(uhdev, p)
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 0) in events
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 0) in events
self._test_states(state_list, scribble, allow_intermediate_states=True)
class GXTP_pen(PenDigitizer):

View File

@ -909,7 +909,7 @@ class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest
Ensure that the confidence bit being set to false should not result in a touch event.
"""
uhdev = self.uhdev
evdev = uhdev.get_evdev()
_evdev = uhdev.get_evdev()
t0 = test_multitouch.Touch(1, 50, 100)
t0.confidence = False
@ -917,6 +917,6 @@ class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest
events = uhdev.next_sync_events()
self.debug_reports(r, uhdev, events)
slot = self.get_slot(uhdev, t0, 0)
_slot = self.get_slot(uhdev, t0, 0)
assert not events
assert not events

View File

@ -19,12 +19,12 @@ esac
SCRIPT_DIR="$(dirname $(realpath $0))"
OUTPUT_DIR="$SCRIPT_DIR/results"
KCONFIG_REL_PATHS=("${SCRIPT_DIR}/config" "${SCRIPT_DIR}/config.common" "${SCRIPT_DIR}/config.${ARCH}")
B2C_URL="https://gitlab.freedesktop.org/mupuf/boot2container/-/raw/master/vm2c.py"
B2C_URL="https://gitlab.freedesktop.org/gfx-ci/boot2container/-/raw/main/vm2c.py"
NUM_COMPILE_JOBS="$(nproc)"
LOG_FILE_BASE="$(date +"hid_selftests.%Y-%m-%d_%H-%M-%S")"
LOG_FILE="${LOG_FILE_BASE}.log"
EXIT_STATUS_FILE="${LOG_FILE_BASE}.exit_status"
CONTAINER_IMAGE="registry.freedesktop.org/libevdev/hid-tools/fedora/37:2023-02-17.1"
CONTAINER_IMAGE="registry.freedesktop.org/bentiss/hid/fedora/39:2023-11-22.1"
TARGETS="${TARGETS:=$(basename ${SCRIPT_DIR})}"
DEFAULT_COMMAND="pip3 install hid-tools; make -C tools/testing/selftests TARGETS=${TARGETS} run_tests"
@ -32,7 +32,7 @@ DEFAULT_COMMAND="pip3 install hid-tools; make -C tools/testing/selftests TARGETS
usage()
{
cat <<EOF
Usage: $0 [-i] [-s] [-d <output_dir>] -- [<command>]
Usage: $0 [-j N] [-s] [-b] [-d <output_dir>] -- [<command>]
<command> is the command you would normally run when you are in
the source kernel direcory. e.g:
@ -55,6 +55,7 @@ Options:
-u) Update the boot2container script to a newer version.
-d) Update the output directory (default: ${OUTPUT_DIR})
-b) Run only the build steps for the kernel and the selftests
-j) Number of jobs for compilation, similar to -j in make
(default: ${NUM_COMPILE_JOBS})
-s) Instead of powering off the VM, start an interactive
@ -191,8 +192,9 @@ main()
local command="${DEFAULT_COMMAND}"
local update_b2c="no"
local debug_shell="no"
local build_only="no"
while getopts ':hsud:j:' opt; do
while getopts ':hsud:j:b' opt; do
case ${opt} in
u)
update_b2c="yes"
@ -207,6 +209,9 @@ main()
command="/bin/sh"
debug_shell="yes"
;;
b)
build_only="yes"
;;
h)
usage
exit 0
@ -226,8 +231,7 @@ main()
shift $((OPTIND -1))
# trap 'catch "$?"' EXIT
if [[ "${debug_shell}" == "no" ]]; then
if [[ "${build_only}" == "no" && "${debug_shell}" == "no" ]]; then
if [[ $# -eq 0 ]]; then
echo "No command specified, will run ${DEFAULT_COMMAND} in the vm"
else
@ -267,24 +271,26 @@ main()
update_kconfig "${kernel_checkout}" "${kconfig_file}"
recompile_kernel "${kernel_checkout}" "${make_command}"
if [[ "${update_b2c}" == "no" && ! -f "${b2c}" ]]; then
echo "vm2c script not found in ${b2c}"
update_b2c="yes"
fi
if [[ "${update_b2c}" == "yes" ]]; then
download $B2C_URL $b2c
chmod +x $b2c
fi
update_selftests "${kernel_checkout}" "${make_command}"
run_vm "${kernel_checkout}" $b2c "${kernel_bzimage}" "${command}"
if [[ "${debug_shell}" != "yes" ]]; then
echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
fi
exit $(cat ${OUTPUT_DIR}/${EXIT_STATUS_FILE})
if [[ "${build_only}" == "no" ]]; then
if [[ "${update_b2c}" == "no" && ! -f "${b2c}" ]]; then
echo "vm2c script not found in ${b2c}"
update_b2c="yes"
fi
if [[ "${update_b2c}" == "yes" ]]; then
download $B2C_URL $b2c
chmod +x $b2c
fi
run_vm "${kernel_checkout}" $b2c "${kernel_bzimage}" "${command}"
if [[ "${debug_shell}" != "yes" ]]; then
echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
fi
exit $(cat ${OUTPUT_DIR}/${EXIT_STATUS_FILE})
fi
}
main "$@"