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
3 years ago
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 (
3 years ago
Injector,
is_in_capabilities,
STARTING,
RUNNING,
STOPPED,
NO_GRAB,
UNKNOWN,
get_udev_name,
3 years ago
)
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
3 years ago
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):
3 years ago
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
4 years ago
self.make_it_fail = 2
def grab_fail_twice(_):
4 years ago
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
3 years ago
path = "/dev/input/event10"
custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a")
3 years ago
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)
4 years ago
gamepad = classify(device) == GAMEPAD
self.assertFalse(gamepad)
self.assertEqual(self.failed, 2)
# success on the third try
3 years ago
self.assertEqual(device.name, fixtures[path]["name"])
4 years ago
def test_fail_grab(self):
self.make_it_fail = 999
custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a")
4 years ago
3 years ago
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)
4 years ago
self.assertIsNone(device)
self.assertGreaterEqual(self.failed, 1)
4 years ago
self.assertEqual(self.injector.get_state(), UNKNOWN)
self.injector.start()
self.assertEqual(self.injector.get_state(), STARTING)
4 years ago
# 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)
4 years ago
def test_grab_device_1(self):
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), "keyboard", "a")
3 years ago
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
4 years ago
_grab_device = self.injector._grab_device
# doesn't have the required capability
3 years ago
self.assertIsNone(_grab_device("/dev/input/event10"))
# according to the fixtures, /dev/input/event30 can do ABS_HAT0X
3 years ago
self.assertIsNotNone(_grab_device("/dev/input/event30"))
# this doesn't exist
3 years ago
self.assertIsNone(_grab_device("/dev/input/event1234"))
4 years ago
def test_gamepad_purpose_none(self):
# forward abs joystick events
3 years ago
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
config.set("gamepad.joystick.right_purpose", NONE)
3 years ago
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
3 years ago
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)
4 years ago
gamepad = classify(device) == GAMEPAD
self.assertTrue(gamepad)
def test_gamepad_purpose_none_2(self):
# forward abs joystick events for the left joystick only
3 years ago
custom_mapping.set("gamepad.joystick.left_purpose", NONE)
config.set("gamepad.joystick.right_purpose", MOUSE)
3 years ago
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
self.injector.context = Context(custom_mapping)
3 years ago
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)
4 years ago
gamepad = classify(device) == GAMEPAD
self.assertTrue(gamepad)
custom_mapping.change(Key(EV_KEY, BTN_A, 1), "keyboard", "a")
device = self.injector._grab_device(path)
4 years ago
gamepad = classify(device) == GAMEPAD
self.assertIsNotNone(device)
self.assertTrue(gamepad)
4 years ago
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")
3 years ago
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.context = Context(custom_mapping)
3 years ago
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")
4 years ago
# skips a device because its capabilities are not used in the mapping
3 years ago
self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping)
self.injector.context = Context(custom_mapping)
3 years ago
path = "/dev/input/event11"
device = self.injector._grab_device(path)
4 years ago
# 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
4 years ago
def wrapped_1():
set_numlock(not is_numlock_on())
4 years ago
@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
3 years ago
config.set("gamepad.joystick.non_linearity", 1)
4 years ago
pointer_speed = 80
3 years ago
config.set("gamepad.joystick.pointer_speed", pointer_speed)
config.set("gamepad.joystick.left_purpose", MOUSE)
4 years ago
# they need to sum up before something is written
divisor = 10
x = MAX_ABS / pointer_speed / divisor
y = MAX_ABS / pointer_speed / divisor
3 years ago
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()
4 years ago
# 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()
4 years ago
if history[0][0] == EV_ABS:
raise AssertionError(
3 years ago
"The injector probably just forwarded them unchanged"
# possibly in addition to writing mouse events
)
4 years ago
# 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):
3 years ago
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")
3 years ago
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
3 years ago
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")
3 years ago
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)
3 years ago
@mock.patch("evdev.InputDevice.ungrab")
def test_gamepad_to_mouse_joystick_to_mouse(self, ungrab_patch):
3 years ago
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):
3 years ago
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):
3 years ago
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)
3 years ago
self.injector.device = "abcd"
self.assertEqual(
get_udev_name("abcd", "forwarded"),
"input-remapper abcd forwarded",
)
3 years ago
@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)")
3 years ago
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()),
3 years ago
sorted(
[
# reading and preventing original events from reaching the
# display server
"input-remapper Foo Device foo forwarded",
"input-remapper Foo Device forwarded",
3 years ago
]
),
)
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())
3 years ago
self.assertEqual(sorted(forwarded.capabilities()[EV_KEY]), keyboard_keys)
self.assertEqual(ungrab_patch.call_count, 2)
def test_injector(self):
4 years ago
# 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
4 years ago
input_b = 10
custom_mapping.change(Key(EV_KEY, input_b, 1), "keyboard", "b")
4 years ago
# stuff the custom_mapping outputs (except for the unknown b)
system_mapping.clear()
code_a = 100
code_q = 101
code_w = 102
3 years ago
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)
4 years ago
# 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()
4 years ago
# 1 event before the combination was triggered (+1 for release)
4 years ago
# 4 events for the macro
4 years ago
# 2 for mapped keys
# 3 for forwarded events
4 years ago
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.
4 years ago
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]
4 years ago
index_q_0 = history.index((EV_KEY, code_q, 0))
del history[index_q_0]
4 years ago
index_w_1 = history.index((EV_KEY, code_w, 1))
del history[index_w_1]
4 years ago
index_w_0 = history.index((EV_KEY, code_w, 0))
del history[index_w_0]
# the rest should be in order.
4 years ago
# 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))
4 years ago
time.sleep(0.1)
self.assertTrue(self.injector.is_alive())
4 years ago
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
3 years ago
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()
3 years ago
push_events(
"gamepad",
[
new_event(*w_down),
new_event(*d_down),
new_event(*w_up),
new_event(*d_up),
],
)
3 years ago
self.injector = Injector(groups.find(name="gamepad"), custom_mapping)
# the injector will otherwise skip the device because
# the capabilities don't contain EV_TYPE
3 years ago
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
3 years ago
system_mapping._set("b", code_b)
system_mapping._set("c", code_c)
group_key = "Foo Device 2"
3 years ago
push_events(
group_key,
[new_event(*w_up)] * 10
+ [new_event(*hw_right), new_event(*w_up)] * 5
3 years ago
+ [new_event(*hw_left)],
)
group = groups.find(key=group_key)
self.injector = Injector(group, custom_mapping)
3 years ago
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)
4 years ago
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)")
3 years ago
self.injector = Injector(groups.find(key="Foo Device 2"), mapping)
4 years ago
history = []
class Stop(Exception):
pass
def _copy_capabilities(*args):
4 years ago
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)")
4 years ago
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")
4 years ago
# 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"))
4 years ago
system_mapping.clear()
3 years ago
system_mapping._set("a", 51)
system_mapping._set("b", 52)
4 years ago
3 years ago
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"))
4 years ago
# 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)
4 years ago
def test_is_in_capabilities(self):
key = Key(1, 2, 1)
3 years ago
capabilities = {1: [9, 2, 5]}
4 years ago
self.assertTrue(is_in_capabilities(key, capabilities))
key = Key((1, 2, 1), (1, 3, 1))
3 years ago
capabilities = {1: [9, 2, 5]}
4 years ago
# 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))
3 years ago
capabilities = {1: [9, 2, 5]}
4 years ago
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: [
3 years ago
(
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)
3 years ago
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")
3 years ago
self.a = system_mapping.get("a")
self.shift_l = system_mapping.get("ShIfT_L")
self.one = system_mapping.get(1)
3 years ago
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)
3 years ago
# 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()