Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/hid: tablets: be stricter for some transitions

To accommodate for legacy devices, we rely on the last state of a
transition to be valid:
for example when we test PEN_IS_OUT_OF_RANGE to PEN_IS_IN_CONTACT,
any "normal" device that reports an InRange bit would insert a
PEN_IS_IN_RANGE state between the 2.

This is of course valid, but this solution prevents to detect false
releases emitted by some firmware:
when pressing an "eraser mode" button, they might send an extra
PEN_IS_OUT_OF_RANGE that we may want to filter.

So define 2 sets of transitions: one that is the ideal behavior, and
one that is OK, it won't break user space, but we have serious doubts
if we are doing the right thing. And depending on the test, either
ask only for valid transitions, or tolerate weird ones.

Reviewed-by: Peter Hutterer <peter.hutterer@who-t.net>
Acked-by: Jiri Kosina <jkosina@suse.com>
Link: https://lore.kernel.org/r/20231206-wip-selftests-v2-13-c0350c2f5986@kernel.org
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>

+113 -19
+113 -19
tools/testing/selftests/hid/tests/test_tablet.py
··· 13 13 import libevdev 14 14 import logging 15 15 import pytest 16 - from typing import Dict, Optional, Tuple 16 + from typing import Dict, List, Optional, Tuple 17 17 18 18 logger = logging.getLogger("hidtools.test.tablet") 19 19 ··· 124 124 125 125 return cls((touch, tool, button)) 126 126 127 - def apply(self, events) -> "PenState": 127 + def apply(self, events: List[libevdev.InputEvent], strict: bool) -> "PenState": 128 128 if libevdev.EV_SYN.SYN_REPORT in events: 129 129 raise ValueError("EV_SYN is in the event sequence") 130 130 touch = self.touch ··· 163 163 button = None 164 164 165 165 new_state = PenState((touch, tool, button)) 166 - assert ( 167 - new_state in self.valid_transitions() 168 - ), f"moving from {self} to {new_state} is forbidden" 166 + if strict: 167 + assert ( 168 + new_state in self.valid_transitions() 169 + ), f"moving from {self} to {new_state} is forbidden" 170 + else: 171 + assert ( 172 + new_state in self.historically_tolerated_transitions() 173 + ), f"moving from {self} to {new_state} is forbidden" 169 174 170 175 return new_state 171 176 172 177 def valid_transitions(self) -> Tuple["PenState", ...]: 178 + """Following the state machine in the URL above. 179 + 180 + Note that those transitions are from the evdev point of view, not HID""" 181 + if self == PenState.PEN_IS_OUT_OF_RANGE: 182 + return ( 183 + PenState.PEN_IS_OUT_OF_RANGE, 184 + PenState.PEN_IS_IN_RANGE, 185 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 186 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 187 + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, 188 + PenState.PEN_IS_IN_CONTACT, 189 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 190 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 191 + PenState.PEN_IS_ERASING, 192 + ) 193 + 194 + if self == PenState.PEN_IS_IN_RANGE: 195 + return ( 196 + PenState.PEN_IS_IN_RANGE, 197 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 198 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 199 + PenState.PEN_IS_OUT_OF_RANGE, 200 + PenState.PEN_IS_IN_CONTACT, 201 + ) 202 + 203 + if self == PenState.PEN_IS_IN_CONTACT: 204 + return ( 205 + PenState.PEN_IS_IN_CONTACT, 206 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 207 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 208 + PenState.PEN_IS_IN_RANGE, 209 + ) 210 + 211 + if self == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT: 212 + return ( 213 + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, 214 + PenState.PEN_IS_OUT_OF_RANGE, 215 + PenState.PEN_IS_ERASING, 216 + ) 217 + 218 + if self == PenState.PEN_IS_ERASING: 219 + return ( 220 + PenState.PEN_IS_ERASING, 221 + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, 222 + ) 223 + 224 + if self == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: 225 + return ( 226 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 227 + PenState.PEN_IS_IN_RANGE, 228 + PenState.PEN_IS_OUT_OF_RANGE, 229 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 230 + ) 231 + 232 + if self == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: 233 + return ( 234 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 235 + PenState.PEN_IS_IN_CONTACT, 236 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 237 + ) 238 + 239 + if self == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON: 240 + return ( 241 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 242 + PenState.PEN_IS_IN_RANGE, 243 + PenState.PEN_IS_OUT_OF_RANGE, 244 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 245 + ) 246 + 247 + if self == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON: 248 + return ( 249 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 250 + PenState.PEN_IS_IN_CONTACT, 251 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 252 + ) 253 + 254 + return tuple() 255 + 256 + def historically_tolerated_transitions(self) -> Tuple["PenState", ...]: 173 257 """Following the state machine in the URL above, with a couple of addition 174 258 for skipping the in-range state, due to historical reasons. 175 259 ··· 777 693 self.debug_reports(r, uhdev, events) 778 694 return events 779 695 780 - def validate_transitions(self, from_state, pen, evdev, events): 696 + def validate_transitions( 697 + self, from_state, pen, evdev, events, allow_intermediate_states 698 + ): 781 699 # check that the final state is correct 782 700 pen.assert_expected_input_events(evdev) 701 + 702 + state = from_state 783 703 784 704 # check that the transitions are valid 785 705 sync_events = [] ··· 794 706 events = events[idx + 1 :] 795 707 796 708 # now check for a valid transition 797 - from_state = from_state.apply(sync_events) 709 + state = state.apply(sync_events, not allow_intermediate_states) 798 710 799 711 if events: 800 - from_state = from_state.apply(sync_events) 712 + state = state.apply(sync_events, not allow_intermediate_states) 801 713 802 - def _test_states(self, state_list, scribble): 714 + def _test_states(self, state_list, scribble, allow_intermediate_states): 803 715 """Internal method to test against a list of 804 716 transition between states. 805 717 state_list is a list of PenState objects ··· 814 726 p = Pen(50, 60) 815 727 uhdev.move_to(p, PenState.PEN_IS_OUT_OF_RANGE) 816 728 events = self.post(uhdev, p) 817 - self.validate_transitions(cur_state, p, evdev, events) 729 + self.validate_transitions( 730 + cur_state, p, evdev, events, allow_intermediate_states 731 + ) 818 732 819 733 cur_state = p.current_state 820 734 ··· 825 735 p.x += 1 826 736 p.y -= 1 827 737 events = self.post(uhdev, p) 828 - self.validate_transitions(cur_state, p, evdev, events) 738 + self.validate_transitions( 739 + cur_state, p, evdev, events, allow_intermediate_states 740 + ) 829 741 assert len(events) >= 3 # X, Y, SYN 830 742 uhdev.move_to(p, state) 831 743 if scribble and state != PenState.PEN_IS_OUT_OF_RANGE: 832 744 p.x += 1 833 745 p.y -= 1 834 746 events = self.post(uhdev, p) 835 - self.validate_transitions(cur_state, p, evdev, events) 747 + self.validate_transitions( 748 + cur_state, p, evdev, events, allow_intermediate_states 749 + ) 836 750 cur_state = p.current_state 837 751 838 752 @pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"]) ··· 849 755 we don't have Invert nor Erase bits, so just move in/out-of-range or proximity. 850 756 https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states 851 757 """ 852 - self._test_states(state_list, scribble) 758 + self._test_states(state_list, scribble, allow_intermediate_states=False) 853 759 854 760 @pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"]) 855 761 @pytest.mark.parametrize( ··· 863 769 """This is not adhering to the Windows Pen Implementation state machine 864 770 but we should expect the kernel to behave properly, mostly for historical 865 771 reasons.""" 866 - self._test_states(state_list, scribble) 772 + self._test_states(state_list, scribble, allow_intermediate_states=True) 867 773 868 774 @pytest.mark.skip_if_uhdev( 869 775 lambda uhdev: "Barrel Switch" not in uhdev.fields, ··· 879 785 ) 880 786 def test_valid_primary_button_pen_states(self, state_list, scribble): 881 787 """Rework the transition state machine by adding the primary button.""" 882 - self._test_states(state_list, scribble) 788 + self._test_states(state_list, scribble, allow_intermediate_states=False) 883 789 884 790 @pytest.mark.skip_if_uhdev( 885 791 lambda uhdev: "Secondary Barrel Switch" not in uhdev.fields, ··· 895 801 ) 896 802 def test_valid_secondary_button_pen_states(self, state_list, scribble): 897 803 """Rework the transition state machine by adding the secondary button.""" 898 - self._test_states(state_list, scribble) 804 + self._test_states(state_list, scribble, allow_intermediate_states=False) 899 805 900 806 @pytest.mark.skip_if_uhdev( 901 807 lambda uhdev: "Invert" not in uhdev.fields, ··· 915 821 to erase. 916 822 https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states 917 823 """ 918 - self._test_states(state_list, scribble) 824 + self._test_states(state_list, scribble, allow_intermediate_states=False) 919 825 920 826 @pytest.mark.skip_if_uhdev( 921 827 lambda uhdev: "Invert" not in uhdev.fields, ··· 935 841 to erase. 936 842 https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states 937 843 """ 938 - self._test_states(state_list, scribble) 844 + self._test_states(state_list, scribble, allow_intermediate_states=True) 939 845 940 846 @pytest.mark.skip_if_uhdev( 941 847 lambda uhdev: "Invert" not in uhdev.fields, ··· 952 858 For example, a pen that has the eraser button might wobble between 953 859 touching and erasing if the tablet doesn't enforce the Windows 954 860 state machine.""" 955 - self._test_states(state_list, scribble) 861 + self._test_states(state_list, scribble, allow_intermediate_states=True) 956 862 957 863 958 864 class GXTP_pen(PenDigitizer):