You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
input-remapper/tests/unit/test_injector.py

919 lines
33 KiB
Python

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# This file is part of input-remapper.
#
# input-remapper is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# input-remapper is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
import unittest
from unittest import mock
import time
import copy
import evdev
from evdev.ecodes import (
EV_REL,
EV_KEY,
EV_ABS,
ABS_HAT0X,
BTN_LEFT,
KEY_A,
REL_X,
REL_Y,
REL_WHEEL,
REL_HWHEEL,
BTN_A,
ABS_X,
ABS_Y,
ABS_Z,
ABS_RZ,
ABS_VOLUME,
KEY_B,
KEY_C,
)
from inputremapper.injection.consumers.joystick_to_mouse import JoystickToMouse
from inputremapper.injection.injector import (
Injector,
is_in_capabilities,
STARTING,
RUNNING,
STOPPED,
NO_GRAB,
UNKNOWN,
get_udev_name,
)
from inputremapper.injection.numlock import is_numlock_on, set_numlock, ensure_numlock
from inputremapper.system_mapping import system_mapping, DISABLE_CODE, DISABLE_NAME
from inputremapper.gui.custom_mapping import custom_mapping
from inputremapper.mapping import Mapping
from inputremapper.config import config, NONE, MOUSE, WHEEL, BUTTONS
from inputremapper.key import Key
from inputremapper.injection.macros.parse import parse
from inputremapper.injection.context import Context
from inputremapper.groups import groups, classify, GAMEPAD
from tests.test import (
new_event,
push_events,
fixtures,
EVENT_READ_TIMEOUT,
uinput_write_history_pipe,
MAX_ABS,
quick_cleanup,
read_write_history_pipe,
InputDevice,
uinputs,
keyboard_keys,
MIN_ABS,
)
def wait_for_uinput_write():
start = time.time()
if not uinput_write_history_pipe[0].poll(timeout=10):
raise AssertionError("No event written within 10 seconds")
return float(time.time() - start)
class TestInjector(unittest.IsolatedAsyncioTestCase):
new_gamepad_path = "/dev/input/event100"
@classmethod
def setUpClass(cls):
cls.injector = None
cls.grab = evdev.InputDevice.grab
quick_cleanup()
def setUp(self):
self.failed = 0
self.make_it_fail = 2
def grab_fail_twice(_):
if self.failed < self.make_it_fail:
self.failed += 1
raise OSError()
evdev.InputDevice.grab = grab_fail_twice
def tearDown(self):
if self.injector is not None:
self.injector.stop_injecting()
self.assertEqual(self.injector.get_state(), STOPPED)
self.injector = None
evdev.InputDevice.grab = self.grab
quick_cleanup()
def find_joystick_to_mouse(self):
# this object became somewhat a pain to retreive
return [
consumer
for consumer in self.injector._consumer_controls[0]._consumers
if isinstance(consumer, JoystickToMouse)
][0]
def test_grab(self):
# path is from the fixtures
path = "/dev/input/event10"
custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a")
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
# this test needs to pass around all other constraints of
# _grab_device
self.injector.context = Context(custom_mapping)
device = self.injector._grab_device(path)
gamepad = classify(device) == GAMEPAD
self.assertFalse(gamepad)
self.assertEqual(self.failed, 2)
# success on the third try
self.assertEqual(device.name, fixtures[path]["name"])
def test_fail_grab(self):
self.make_it_fail = 999
custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a")
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
path = "/dev/input/event10"
self.injector.context = Context(custom_mapping)
device = self.injector._grab_device(path)
self.assertIsNone(device)
self.assertGreaterEqual(self.failed, 1)
self.assertEqual(self.injector.get_state(), UNKNOWN)
self.injector.start()
self.assertEqual(self.injector.get_state(), STARTING)
# since none can be grabbed, the process will terminate. But that
# actually takes quite some time.
time.sleep(self.injector.regrab_timeout * 12)
self.assertFalse(self.injector.is_alive())
self.assertEqual(self.injector.get_state(), NO_GRAB)
def test_grab_device_1(self):
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), "keyboard", "a")
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
_grab_device = self.injector._grab_device
# doesn't have the required capability
self.assertIsNone(_grab_device("/dev/input/event10"))
# according to the fixtures, /dev/input/event30 can do ABS_HAT0X
self.assertIsNotNone(_grab_device("/dev/input/event30"))
# this doesn't exist
self.assertIsNone(_grab_device("/dev/input/event1234"))
def test_gamepad_purpose_none(self):
# forward abs joystick events
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
config.set("gamepad.joystick.right_purpose", NONE)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = "/dev/input/event30"
device = self.injector._grab_device(path)
self.assertIsNone(device) # no capability is used, so it won't grab
custom_mapping.change(Key(EV_KEY, BTN_A, 1), "keyboard", "a")
device = self.injector._grab_device(path)
self.assertIsNotNone(device)
gamepad = classify(device) == GAMEPAD
self.assertTrue(gamepad)
def test_gamepad_purpose_none_2(self):
# forward abs joystick events for the left joystick only
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
config.set("gamepad.joystick.right_purpose", MOUSE)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = "/dev/input/event30"
device = self.injector._grab_device(path)
# the right joystick maps as mouse, so it is grabbed
# even with an empty mapping
self.assertIsNotNone(device)
gamepad = classify(device) == GAMEPAD
self.assertTrue(gamepad)
custom_mapping.change(Key(EV_KEY, BTN_A, 1), "keyboard", "a")
device = self.injector._grab_device(path)
gamepad = classify(device) == GAMEPAD
self.assertIsNotNone(device)
self.assertTrue(gamepad)
def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping
custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a")
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = "/dev/input/event11"
device = self.injector._grab_device(path)
self.assertIsNone(device)
self.assertEqual(self.failed, 0)
def test_skip_unknown_device(self):
custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a")
# skips a device because its capabilities are not used in the mapping
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.context = Context(custom_mapping)
path = "/dev/input/event11"
device = self.injector._grab_device(path)
# skips the device alltogether, so no grab attempts fail
self.assertEqual(self.failed, 0)
self.assertIsNone(device)
def test_numlock(self):
before = is_numlock_on()
set_numlock(not before) # should change
self.assertEqual(not before, is_numlock_on())
@ensure_numlock
def wrapped_1():
set_numlock(not is_numlock_on())
@ensure_numlock
def wrapped_2():
pass
# should not change
wrapped_1()
self.assertEqual(not before, is_numlock_on())
wrapped_2()
self.assertEqual(not before, is_numlock_on())
# toggle one more time to restore the previous configuration
set_numlock(before)
self.assertEqual(before, is_numlock_on())
def test_gamepad_to_mouse(self):
# maps gamepad joystick events to mouse events
config.set("gamepad.joystick.non_linearity", 1)
pointer_speed = 80
config.set("gamepad.joystick.pointer_speed", pointer_speed)
config.set("gamepad.joystick.left_purpose", MOUSE)
# they need to sum up before something is written
divisor = 10
x = MAX_ABS / pointer_speed / divisor
y = MAX_ABS / pointer_speed / divisor
push_events(
"gamepad",
[
new_event(EV_ABS, ABS_X, x),
new_event(EV_ABS, ABS_Y, y),
new_event(EV_ABS, ABS_X, -x),
new_event(EV_ABS, ABS_Y, -y),
],
)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.start()
# wait for the injector to start sending, at most 1s
uinput_write_history_pipe[0].poll(1)
# wait a bit more for it to sum up
sleep = 0.5
time.sleep(sleep)
# convert the write history to some easier to manage list
history = read_write_history_pipe()
if history[0][0] == EV_ABS:
raise AssertionError(
"The injector probably just forwarded them unchanged"
# possibly in addition to writing mouse events
)
# movement is written at 60hz and it takes `divisor` steps to
# move 1px. take it times 2 for both x and y events.
self.assertGreater(len(history), 60 * sleep * 0.9 * 2 / divisor)
self.assertLess(len(history), 60 * sleep * 1.1 * 2 / divisor)
# those may be in arbitrary order
count_x = history.count((EV_REL, REL_X, -1))
count_y = history.count((EV_REL, REL_Y, -1))
self.assertGreater(count_x, 1)
self.assertGreater(count_y, 1)
# only those two types of events were written
self.assertEqual(len(history), count_x + count_y)
def test_gamepad_forward_joysticks(self):
push_events(
"gamepad",
[
# should forward them unmodified
new_event(EV_ABS, ABS_X, 10),
new_event(EV_ABS, ABS_Y, 20),
new_event(EV_ABS, ABS_X, -30),
new_event(EV_ABS, ABS_Y, -40),
new_event(EV_KEY, BTN_A, 1),
new_event(EV_KEY, BTN_A, 0),
]
* 2,
)
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
custom_mapping.set("gamepad.joystick.right_purpose", NONE)
# BTN_A -> 77
custom_mapping.change(Key((1, BTN_A, 1)), "keyboard", "b")
system_mapping._set("b", 77)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.start()
# wait for the injector to start sending, at most 1s
uinput_write_history_pipe[0].poll(1)
time.sleep(0.2)
# convert the write history to some easier to manage list
history = read_write_history_pipe()
self.assertEqual(history.count((EV_ABS, ABS_X, 10)), 2)
self.assertEqual(history.count((EV_ABS, ABS_Y, 20)), 2)
self.assertEqual(history.count((EV_ABS, ABS_X, -30)), 2)
self.assertEqual(history.count((EV_ABS, ABS_Y, -40)), 2)
self.assertEqual(history.count((EV_KEY, 77, 1)), 2)
self.assertEqual(history.count((EV_KEY, 77, 0)), 2)
def test_gamepad_trigger(self):
# map one of the triggers to BTN_NORTH, while the other one
# should be forwarded unchanged
value = MAX_ABS // 2
push_events(
"gamepad",
[
new_event(EV_ABS, ABS_Z, value),
new_event(EV_ABS, ABS_RZ, value),
],
)
# ABS_Z -> 77
# ABS_RZ is not mapped
custom_mapping.change(Key((EV_ABS, ABS_Z, 1)), "keyboard", "b")
system_mapping._set("b", 77)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.start()
# wait for the injector to start sending, at most 1s
uinput_write_history_pipe[0].poll(1)
time.sleep(0.2)
# convert the write history to some easier to manage list
history = read_write_history_pipe()
self.assertEqual(history.count((EV_KEY, 77, 1)), 1)
self.assertEqual(history.count((EV_ABS, ABS_RZ, value)), 1)
@mock.patch("evdev.InputDevice.ungrab")
def test_gamepad_to_mouse_joystick_to_mouse(self, ungrab_patch):
custom_mapping.set("gamepad.joystick.left_purpose", MOUSE)
custom_mapping.set("gamepad.joystick.right_purpose", NONE)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
# the stop message will be available in the pipe right away,
# so run won't block and just stop. all the stuff
# will be initialized though, so that stuff can be tested
self.injector.stop_injecting()
# the context serves no purpose in the main process (which runs the
# tests). The context is only accessible in the newly created process.
self.assertIsNone(self.injector.context)
# not in a process because this doesn't call start, so the
# joystick_to_mouse state can be checked
self.injector.run()
joystick_to_mouse = self.find_joystick_to_mouse()
self.assertEqual(joystick_to_mouse._abs_range[0], MIN_ABS)
self.assertEqual(joystick_to_mouse._abs_range[1], MAX_ABS)
self.assertEqual(
self.injector.context.mapping.get("gamepad.joystick.left_purpose"), MOUSE
)
self.assertEqual(ungrab_patch.call_count, 1)
def test_device1_not_a_gamepad(self):
custom_mapping.set("gamepad.joystick.left_purpose", MOUSE)
custom_mapping.set("gamepad.joystick.right_purpose", WHEEL)
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.stop_injecting()
self.injector.run()
# not a gamepad, so nothing should happen
self.assertEqual(len(self.injector._consumer_controls), 0)
def test_get_udev_name(self):
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
suffix = "mapped"
prefix = "input-remapper"
expected = f'{prefix} {"a" * (80 - len(suffix) - len(prefix) - 2)} {suffix}'
self.assertEqual(len(expected), 80)
self.assertEqual(get_udev_name("a" * 100, suffix), expected)
self.injector.device = "abcd"
self.assertEqual(
get_udev_name("abcd", "forwarded"),
"input-remapper abcd forwarded",
)
@mock.patch("evdev.InputDevice.ungrab")
def test_capabilities_and_uinput_presence(self, ungrab_patch):
custom_mapping.change(Key(EV_KEY, KEY_A, 1), "keyboard", "c")
custom_mapping.change(Key(EV_REL, REL_HWHEEL, 1), "keyboard", "k(b)")
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.stop_injecting()
self.injector.run()
self.assertEqual(
self.injector.context.mapping.get_mapping(Key(EV_KEY, KEY_A, 1)),
("c", "keyboard"),
)
self.assertEqual(
self.injector.context.key_to_code[((EV_KEY, KEY_A, 1),)],
(KEY_C, "keyboard"),
)
self.assertEqual(
self.injector.context.mapping.get_mapping(Key(EV_REL, REL_HWHEEL, 1)),
("k(b)", "keyboard"),
)
self.assertEqual(
self.injector.context.macros[((EV_REL, REL_HWHEEL, 1),)][0].code, "k(b)"
)
self.assertListEqual(
sorted(uinputs.keys()),
sorted(
[
# reading and preventing original events from reaching the
# display server
"input-remapper Foo Device foo forwarded",
"input-remapper Foo Device forwarded",
]
),
)
forwarded_foo = uinputs.get("input-remapper Foo Device foo forwarded")
forwarded = uinputs.get("input-remapper Foo Device forwarded")
self.assertIsNotNone(forwarded_foo)
self.assertIsNotNone(forwarded)
# copies capabilities for all other forwarded devices
self.assertIn(EV_REL, forwarded_foo.capabilities())
self.assertIn(EV_KEY, forwarded.capabilities())
self.assertEqual(sorted(forwarded.capabilities()[EV_KEY]), keyboard_keys)
self.assertEqual(ungrab_patch.call_count, 2)
def test_injector(self):
# the tests in test_keycode_mapper.py test this stuff in detail
numlock_before = is_numlock_on()
combination = Key((EV_KEY, 8, 1), (EV_KEY, 9, 1))
custom_mapping.change(combination, "keyboard", "k(KEY_Q).k(w)")
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, -1), "keyboard", "a")
# one mapping that is unknown in the system_mapping on purpose
input_b = 10
custom_mapping.change(Key(EV_KEY, input_b, 1), "keyboard", "b")
# stuff the custom_mapping outputs (except for the unknown b)
system_mapping.clear()
code_a = 100
code_q = 101
code_w = 102
system_mapping._set("a", code_a)
system_mapping._set("key_q", code_q)
system_mapping._set("w", code_w)
push_events(
"Bar Device",
[
# should execute a macro...
new_event(EV_KEY, 8, 1),
new_event(EV_KEY, 9, 1), # ...now
new_event(EV_KEY, 8, 0),
new_event(EV_KEY, 9, 0),
# gamepad stuff. trigger a combination
new_event(EV_ABS, ABS_HAT0X, -1),
new_event(EV_ABS, ABS_HAT0X, 0),
# just pass those over without modifying
new_event(EV_KEY, 10, 1),
new_event(EV_KEY, 10, 0),
new_event(3124, 3564, 6542),
],
)
self.injector = Injector(groups.find(name="Bar Device"), custom_mapping)
self.assertEqual(self.injector.get_state(), UNKNOWN)
self.injector.start()
self.assertEqual(self.injector.get_state(), STARTING)
uinput_write_history_pipe[0].poll(timeout=1)
self.assertEqual(self.injector.get_state(), RUNNING)
time.sleep(EVENT_READ_TIMEOUT * 10)
# sending anything arbitrary does not stop the process
# (is_alive checked later after some time)
self.injector._msg_pipe[1].send(1234)
# convert the write history to some easier to manage list
history = read_write_history_pipe()
# 1 event before the combination was triggered (+1 for release)
# 4 events for the macro
# 2 for mapped keys
# 3 for forwarded events
self.assertEqual(len(history), 11)
# since the macro takes a little bit of time to execute, its
# keystrokes are all over the place.
# just check if they are there and if so, remove them from the list.
self.assertIn((EV_KEY, 8, 1), history)
self.assertIn((EV_KEY, code_q, 1), history)
self.assertIn((EV_KEY, code_q, 1), history)
self.assertIn((EV_KEY, code_q, 0), history)
self.assertIn((EV_KEY, code_w, 1), history)
self.assertIn((EV_KEY, code_w, 0), history)
index_q_1 = history.index((EV_KEY, code_q, 1))
index_q_0 = history.index((EV_KEY, code_q, 0))
index_w_1 = history.index((EV_KEY, code_w, 1))
index_w_0 = history.index((EV_KEY, code_w, 0))
self.assertGreater(index_q_0, index_q_1)
self.assertGreater(index_w_1, index_q_0)
self.assertGreater(index_w_0, index_w_1)
del history[index_q_1]
index_q_0 = history.index((EV_KEY, code_q, 0))
del history[index_q_0]
index_w_1 = history.index((EV_KEY, code_w, 1))
del history[index_w_1]
index_w_0 = history.index((EV_KEY, code_w, 0))
del history[index_w_0]
# the rest should be in order.
# first the incomplete combination key that wasn't mapped to anything
# and just forwarded. The input event that triggered the macro
# won't appear here.
self.assertEqual(history[0], (EV_KEY, 8, 1))
self.assertEqual(history[1], (EV_KEY, 8, 0))
# value should be 1, even if the input event was -1.
# Injected keycodes should always be either 0 or 1
self.assertEqual(history[2], (EV_KEY, code_a, 1))
self.assertEqual(history[3], (EV_KEY, code_a, 0))
self.assertEqual(history[4], (EV_KEY, input_b, 1))
self.assertEqual(history[5], (EV_KEY, input_b, 0))
self.assertEqual(history[6], (3124, 3564, 6542))
time.sleep(0.1)
self.assertTrue(self.injector.is_alive())
numlock_after = is_numlock_on()
self.assertEqual(numlock_before, numlock_after)
self.assertEqual(self.injector.get_state(), RUNNING)
def test_any_funky_event_as_button(self):
# as long as should_map_as_btn says it should be a button,
# it will be.
EV_TYPE = 4531
CODE_1 = 754
CODE_2 = 4139
w_down = (EV_TYPE, CODE_1, -1)
w_up = (EV_TYPE, CODE_1, 0)
d_down = (EV_TYPE, CODE_2, 1)
d_up = (EV_TYPE, CODE_2, 0)
custom_mapping.change(Key(*w_down[:2], -1), "keyboard", "w")
custom_mapping.change(Key(*d_down[:2], 1), "keyboard", "k(d)")
system_mapping.clear()
code_w = 71
code_d = 74
system_mapping._set("w", code_w)
system_mapping._set("d", code_d)
def do_stuff():
if self.injector is not None:
# discard the previous injector
self.injector.stop_injecting()
time.sleep(0.1)
while uinput_write_history_pipe[0].poll():
uinput_write_history_pipe[0].recv()
push_events(
"gamepad",
[
new_event(*w_down),
new_event(*d_down),
new_event(*w_up),
new_event(*d_up),
],
)
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
# the injector will otherwise skip the device because
# the capabilities don't contain EV_TYPE
input = InputDevice("/dev/input/event30")
self.injector._grab_device = lambda *args: input
self.injector.start()
uinput_write_history_pipe[0].poll(timeout=1)
time.sleep(EVENT_READ_TIMEOUT * 10)
return read_write_history_pipe()
"""no"""
history = do_stuff()
self.assertEqual(history.count((EV_KEY, code_w, 1)), 0)
self.assertEqual(history.count((EV_KEY, code_d, 1)), 0)
self.assertEqual(history.count((EV_KEY, code_w, 0)), 0)
self.assertEqual(history.count((EV_KEY, code_d, 0)), 0)
"""yes"""
with mock.patch("inputremapper.utils.should_map_as_btn", lambda *_: True):
history = do_stuff()
self.assertEqual(history.count((EV_KEY, code_w, 1)), 1)
self.assertEqual(history.count((EV_KEY, code_d, 1)), 1)
self.assertEqual(history.count((EV_KEY, code_w, 0)), 1)
self.assertEqual(history.count((EV_KEY, code_d, 0)), 1)
def test_wheel(self):
# wheel release events are made up with a debouncer
# map those two to stuff
w_up = (EV_REL, REL_WHEEL, -1)
hw_right = (EV_REL, REL_HWHEEL, 1)
# should be forwarded and present in the capabilities
hw_left = (EV_REL, REL_HWHEEL, -1)
custom_mapping.change(Key(*hw_right), "keyboard", "k(b)")
custom_mapping.change(Key(*w_up), "keyboard", "c")
system_mapping.clear()
code_b = 91
code_c = 92
system_mapping._set("b", code_b)
system_mapping._set("c", code_c)
group_key = "Foo Device 2"
push_events(
group_key,
[new_event(*w_up)] * 10
+ [new_event(*hw_right), new_event(*w_up)] * 5
+ [new_event(*hw_left)],
)
group = groups.find(key=group_key)
self.injector = Injector(group, custom_mapping)
device = InputDevice("/dev/input/event11")
# make sure this test uses a device that has the needed capabilities
# for the injector to grab it
self.assertIn(EV_REL, device.capabilities())
self.assertIn(REL_WHEEL, device.capabilities()[EV_REL])
self.assertIn(REL_HWHEEL, device.capabilities()[EV_REL])
self.assertIn(device.path, group.paths)
self.injector.start()
# wait for the first injected key down event
uinput_write_history_pipe[0].poll(timeout=1)
self.assertTrue(uinput_write_history_pipe[0].poll())
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.t, (EV_KEY, code_c, 1))
# in 5 more read-loop ticks, nothing new should have happened.
# add a bit of a head-start of one EVENT_READ_TIMEOUT to avoid race-conditions
# in tests
self.assertFalse(
uinput_write_history_pipe[0].poll(timeout=EVENT_READ_TIMEOUT * 6)
)
# 5 more and it should be within the second phase in which
# the horizontal wheel is used. add some tolerance
self.assertAlmostEqual(
wait_for_uinput_write(), EVENT_READ_TIMEOUT * 5, delta=EVENT_READ_TIMEOUT
)
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.t, (EV_KEY, code_b, 1))
time.sleep(EVENT_READ_TIMEOUT * 10 + 5 / 60)
# after 21 read-loop ticks all events should be consumed, wait for
# at least 3 (lets use 5 so that the test passes even if it lags)
# ticks so that the debouncers are triggered.
# Key-up events for both wheel events should be written now that no
# new key-down event arrived.
events = read_write_history_pipe()
self.assertEqual(events.count((EV_KEY, code_b, 0)), 1)
self.assertEqual(events.count((EV_KEY, code_c, 0)), 1)
self.assertEqual(events.count(hw_left), 1) # the unmapped wheel
# the unmapped wheel won't get a debounced release command, it's
# forwarded as is
self.assertNotIn((EV_REL, REL_HWHEEL, 0), events)
self.assertEqual(len(events), 3)
def test_store_permutations_for_macros(self):
mapping = Mapping()
ev_1 = (EV_KEY, 41, 1)
ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1)
# a combination
mapping.change(Key(ev_1, ev_2, ev_3), "keyboard", "k(a)")
self.injector = Injector(groups.find(key="Foo Device 2"), mapping)
history = []
class Stop(Exception):
pass
def _copy_capabilities(*args):
history.append(args)
# avoid going into any mainloop
raise Stop()
with mock.patch.object(self.injector, "_copy_capabilities", _copy_capabilities):
try:
self.injector.run()
except Stop:
pass
# one call
self.assertEqual(len(history), 1)
# first argument of the first call
macros = self.injector.context.macros
self.assertEqual(len(macros), 2)
self.assertEqual(macros[(ev_1, ev_2, ev_3)][0].code, "k(a)")
self.assertEqual(macros[(ev_2, ev_1, ev_3)][0].code, "k(a)")
def test_key_to_code(self):
mapping = Mapping()
ev_1 = (EV_KEY, 41, 1)
ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1)
ev_4 = (EV_KEY, 44, 1)
mapping.change(Key(ev_1), "keyboard", "a")
# a combination
mapping.change(Key(ev_2, ev_3, ev_4), "keyboard", "b")
self.assertEqual(mapping.get_mapping(Key(ev_2, ev_3, ev_4)), ("b", "keyboard"))
system_mapping.clear()
system_mapping._set("a", 51)
system_mapping._set("b", 52)
injector = Injector(groups.find(key="Foo Device 2"), mapping)
injector.context = Context(mapping)
self.assertEqual(injector.context.key_to_code.get((ev_1,)), (51, "keyboard"))
# permutations to make matching combinations easier
self.assertEqual(
injector.context.key_to_code.get((ev_2, ev_3, ev_4)), (52, "keyboard")
)
self.assertEqual(
injector.context.key_to_code.get((ev_3, ev_2, ev_4)), (52, "keyboard")
)
self.assertEqual(len(injector.context.key_to_code), 3)
def test_is_in_capabilities(self):
key = Key(1, 2, 1)
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
key = Key((1, 2, 1), (1, 3, 1))
capabilities = {1: [9, 2, 5]}
# only one of the codes of the combination is required.
# The goal is to make combinations across those sub-devices possible,
# that make up one hardware device
self.assertTrue(is_in_capabilities(key, capabilities))
key = Key((1, 2, 1), (1, 5, 1))
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
class TestModifyCapabilities(unittest.TestCase):
@classmethod
def setUpClass(cls):
quick_cleanup()
def setUp(self):
class FakeDevice:
def __init__(self):
self._capabilities = {
evdev.ecodes.EV_SYN: [1, 2, 3],
evdev.ecodes.EV_FF: [1, 2, 3],
EV_ABS: [
(
1,
evdev.AbsInfo(
value=None,
min=None,
max=1234,
fuzz=None,
flat=None,
resolution=None,
),
),
(
2,
evdev.AbsInfo(
value=None,
min=50,
max=2345,
fuzz=None,
flat=None,
resolution=None,
),
),
3,
],
}
def capabilities(self, absinfo=False):
assert absinfo is True
return self._capabilities
mapping = Mapping()
mapping.change(Key(EV_KEY, 80, 1), "keyboard", "a")
mapping.change(Key(EV_KEY, 81, 1), "keyboard", DISABLE_NAME)
macro_code = "r(2, m(sHiFt_l, r(2, k(1).k(2))))"
macro = parse(macro_code, mapping)
mapping.change(Key(EV_KEY, 60, 111), "keyboard", macro_code)
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
mapping.change(Key(EV_REL, 1234, 3), "keyboard", "b")
self.a = system_mapping.get("a")
self.shift_l = system_mapping.get("ShIfT_L")
self.one = system_mapping.get(1)
self.two = system_mapping.get("2")
self.left = system_mapping.get("BtN_lEfT")
self.fake_device = FakeDevice()
self.mapping = mapping
self.macro = macro
def check_keys(self, capabilities):
"""No matter the configuration, EV_KEY will be mapped to EV_KEY."""
self.assertIn(EV_KEY, capabilities)
keys = capabilities[EV_KEY]
self.assertIn(self.a, keys)
self.assertIn(self.one, keys)
self.assertIn(self.two, keys)
self.assertIn(self.shift_l, keys)
self.assertNotIn(DISABLE_CODE, keys)
def tearDown(self):
quick_cleanup()
def test_copy_capabilities(self):
self.mapping.change(Key(EV_KEY, 60, 1), "keyboard", self.macro.code)
# I don't know what ABS_VOLUME is, for now I would like to just always
# remove it until somebody complains, since its presence broke stuff
self.injector = Injector(None, self.mapping)
self.fake_device._capabilities = {
EV_ABS: [ABS_VOLUME, (ABS_X, evdev.AbsInfo(0, 0, 500, 0, 0, 0))],
EV_KEY: [1, 2, 3],
EV_REL: [11, 12, 13],
evdev.ecodes.EV_SYN: [1],
evdev.ecodes.EV_FF: [2],
}
capabilities = self.injector._copy_capabilities(self.fake_device)
self.assertNotIn(ABS_VOLUME, capabilities[EV_ABS])
self.assertNotIn(evdev.ecodes.EV_SYN, capabilities)
self.assertNotIn(evdev.ecodes.EV_FF, capabilities)
self.assertListEqual(capabilities[EV_KEY], [1, 2, 3])
self.assertListEqual(capabilities[EV_REL], [11, 12, 13])
self.assertEqual(capabilities[EV_ABS][0][1].max, 500)
if __name__ == "__main__":
unittest.main()