Files
InputRemapper/tests/unit/test_macros/test_mod_tap.py
Blomios 0ab77d2a64 chore(repo): baseline du fork input-remapper (upstream v2.2.1)
Le dépôt ne contenait que README.md ; ajout de l'intégralité du code
fonctionnel du fork comme socle stable de la branche de release.
L'état runtime d'orchestration (.ideai/) est exclu du suivi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:47:12 +02:00

386 lines
14 KiB
Python

import asyncio
import time
import unittest
from unittest.mock import patch
import evdev
from evdev.ecodes import KEY_A, EV_KEY, KEY_B, KEY_LEFTSHIFT, KEY_C
from inputremapper.configs.input_config import InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.preset import Preset
from inputremapper.injection.context import Context
from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.global_uinputs import GlobalUInputs, UInput
from inputremapper.injection.macros.parse import Parser
from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser
from inputremapper.input_event import InputEvent
from tests.lib.fixtures import fixtures
from tests.lib.patches import InputDevice
from tests.lib.pipes import uinput_write_history
from tests.lib.test_setup import test_setup
from tests.unit.test_macros.macro_test_base import MacroTestBase, DummyMapping
@test_setup
class TestModTapIntegration(unittest.IsolatedAsyncioTestCase):
# Testcases are from https://github.com/qmk/qmk_firmware/blob/78a0adfbb4d2c4e12f93f2a62ded0020d406243e/docs/tap_hold.md#nested-tap-abba-nested-tap
# This test-setup is a bit more involved, because I needed to modify the
# EventReader as well for this to work.
def setUp(self):
self.origin_hash = fixtures.bar_device.get_device_hash()
self.forward_uinput = evdev.UInput(name="test-forward-uinput")
self.source_device = InputDevice(fixtures.bar_device.path)
self.stop_event = asyncio.Event()
self.global_uinputs = GlobalUInputs(UInput)
self.global_uinputs.prepare_all()
self.target_uinput = self.global_uinputs.get_uinput("keyboard")
self.mapping_parser = MappingParser(self.global_uinputs)
self.mapping = Mapping.from_combination(
input_combination=[
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=self.origin_hash,
)
],
output_symbol="mod_tap(a, Shift_L)",
)
self.preset = Preset()
self.preset.add(self.mapping)
self.bootstrap_event_reader()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def bootstrap_event_reader(self):
self.context = Context(
self.preset,
source_devices={self.origin_hash: self.source_device},
forward_devices={self.origin_hash: self.forward_uinput},
mapping_parser=self.mapping_parser,
)
self.event_reader = EventReader(
self.context,
self.source_device,
self.stop_event,
)
def write(self, type_, code, value):
self.target_uinput.write_event(InputEvent.from_tuple((type_, code, value)))
async def input(self, type_, code, value):
asyncio.ensure_future(
self.event_reader.handle(
InputEvent.from_tuple(
(
type_,
code,
value,
),
origin_hash=self.origin_hash,
)
)
)
# Make the main_loop iterate a bit for the event_reader to do its thing.
await asyncio.sleep(0)
async def test_distinct_taps_1(self):
await self.input(EV_KEY, KEY_A, 1)
await asyncio.sleep(0.190)
await self.input(EV_KEY, KEY_A, 0)
await asyncio.sleep(0.020) # exceeds tapping_term here
await self.input(EV_KEY, KEY_B, 1)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_B, 0)
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_A, 1)),
InputEvent.from_tuple((EV_KEY, KEY_A, 0)),
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
],
)
self.assertEqual(
self.target_uinput.write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_A, 1)),
InputEvent.from_tuple((EV_KEY, KEY_A, 0)),
],
)
self.assertEqual(
self.forward_uinput.write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
],
)
async def test_distinct_taps_2(self):
await self.input(EV_KEY, KEY_A, 1)
await asyncio.sleep(0.220) # exceeds tapping_term here
await self.input(EV_KEY, KEY_A, 0)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_B, 1)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_B, 0)
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 1)),
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 0)),
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
],
)
self.assertEqual(
self.target_uinput.write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 1)),
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 0)),
],
)
self.assertEqual(
self.forward_uinput.write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
],
)
async def test_nested_tap_1(self):
await self.input(EV_KEY, KEY_A, 1)
await asyncio.sleep(0.100)
self.assertEqual(uinput_write_history, [])
await self.input(EV_KEY, KEY_B, 1)
await asyncio.sleep(0.020)
self.assertEqual(uinput_write_history, [])
await self.input(EV_KEY, KEY_B, 0)
await asyncio.sleep(0.050)
self.assertEqual(uinput_write_history, [])
await self.input(EV_KEY, KEY_A, 0)
# everything happened within the tapping_term, so the modifier is not activated.
# "ab" should be written, in the exact order of the input.
await asyncio.sleep(0.040)
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_A, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
InputEvent.from_tuple((EV_KEY, KEY_A, 0)),
],
)
async def test_nested_tap_2(self):
await self.input(EV_KEY, KEY_A, 1)
await asyncio.sleep(0.100)
await self.input(EV_KEY, KEY_B, 1)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_B, 0)
await asyncio.sleep(0.100) # exceeds tapping_term here
await self.input(EV_KEY, KEY_A, 0)
await asyncio.sleep(0.020)
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 0)),
],
)
async def test_nested_tap_3(self):
await self.input(EV_KEY, KEY_A, 1)
await asyncio.sleep(0.220) # exceeds tapping_term here
await self.input(EV_KEY, KEY_B, 1)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_B, 0)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_A, 0)
await asyncio.sleep(0.020)
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 0)),
],
)
async def test_rolling_keys_1(self):
await self.input(EV_KEY, KEY_A, 1)
await asyncio.sleep(0.100)
await self.input(EV_KEY, KEY_B, 1)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_A, 0)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_B, 0)
# everything happened within the tapping_term, so the modifier is not activated.
# "ab" should be written, in the exact order of the input.
await asyncio.sleep(0.100)
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_A, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_A, 0)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
],
)
async def test_rolling_keys_2(self):
await self.input(EV_KEY, KEY_A, 1)
await asyncio.sleep(0.100)
await self.input(EV_KEY, KEY_B, 1)
await asyncio.sleep(0.100) # exceeds tapping_term here
await self.input(EV_KEY, KEY_A, 0)
await asyncio.sleep(0.020)
await self.input(EV_KEY, KEY_B, 0)
await asyncio.sleep(0.020)
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 1)),
InputEvent.from_tuple((EV_KEY, KEY_B, 1)),
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 0)),
InputEvent.from_tuple((EV_KEY, KEY_B, 0)),
],
)
async def test_many_keys_correct_order_without_sleep(self):
self.mapping.macro_key_sleep_ms = 0
await self.many_keys_correct_order()
async def test_many_keys_correct_order_with_sleep(self):
self.mapping.macro_key_sleep_ms = 20
await self.many_keys_correct_order()
async def many_keys_correct_order(self):
await self.input(EV_KEY, KEY_A, 1)
# Send many events to the listener. It has to make all of them wait.
for i in range(30):
await self.input(EV_KEY, i, 1)
# exceed tapping_term. mod_tap will inject the modifier and replay all the
# previous events.
await asyncio.sleep(0.201)
# mod_tap is busy replaying events. While it does that, inject this
await self.input(EV_KEY, 100, 1)
start = time.time()
timeout = 2
while len(uinput_write_history) < 32 and (time.time() - start) < timeout:
# Wait for it to complete
await asyncio.sleep(0.1)
self.assertEqual(len(uinput_write_history), 32)
# Expect it to cleanly handle all events before injecting 100. Expect
# everything to be in the correct order.
self.assertEqual(
uinput_write_history,
[
InputEvent.from_tuple((EV_KEY, KEY_LEFTSHIFT, 1)),
*[InputEvent.from_tuple((EV_KEY, i, 1)) for i in range(30)],
InputEvent.from_tuple((EV_KEY, 100, 1)),
],
)
async def test_mapped_second_key(self):
# Map b to c.
# While mod_tap is waiting for the timeout to happen, press b.
# We expect c to be written, because b goes through the handlers and
# gets mapped.
# The event_reader has to wait for listeners to complete for mod_tap to work, so
# that it hands them over to the other handlers when the time comes.
# That means however, that the event_readers loop blocks. Therefore, it was turned
# into a fire-and-forget kind of thing. When an event arrives, it just schedules
# asyncio to do that stuff later, and continues reading.
self.preset.add(
Mapping.from_combination(
input_combination=[
InputConfig(
type=EV_KEY,
code=KEY_B,
origin_hash=self.origin_hash,
)
],
output_symbol="c",
),
)
self.bootstrap_event_reader()
async def async_generator():
events = [
InputEvent(0, 0, EV_KEY, KEY_A, 1),
InputEvent(0, 0, EV_KEY, KEY_B, 1),
InputEvent(0, 0, EV_KEY, KEY_A, 0),
InputEvent(0, 0, EV_KEY, KEY_B, 0),
]
for event in events:
yield event
# Wait a bit. During runtime, events don't come in that quickly
# and the mod_tap macro needs some loop iterations until it adds
# the listener to the context.
await asyncio.sleep(0.010)
with patch.object(self.event_reader, "read_loop", async_generator):
await self.event_reader.run()
await asyncio.sleep(0.020)
self.assertIn(InputEvent(0, 0, EV_KEY, KEY_C, 1), uinput_write_history)
self.assertIn(InputEvent(0, 0, EV_KEY, KEY_C, 0), uinput_write_history)
self.assertIn(InputEvent(0, 0, EV_KEY, KEY_A, 1), uinput_write_history)
self.assertIn(InputEvent(0, 0, EV_KEY, KEY_A, 0), uinput_write_history)
self.assertNotIn(InputEvent(0, 0, EV_KEY, KEY_B, 1), uinput_write_history)
self.assertNotIn(InputEvent(0, 0, EV_KEY, KEY_B, 0), uinput_write_history)
@test_setup
class TestModTapUnit(MacroTestBase):
async def wait_for_timeout(self, macro):
macro = Parser.parse(macro, self.context, DummyMapping, True)
start = time.time()
# Awaiting macro.run will cause it to wait for the tapping_term.
# When it injects the modifier, release the trigger.
macro.press_trigger()
await macro.run(lambda *_, **__: macro.release_trigger())
return time.time() - start
async def test_tapping_term_configuration_default(self):
time_ = await self.wait_for_timeout("mod_tap(a, b)")
# + 2 times 10ms of keycode_pause
self.assertAlmostEqual(time_, 0.22, delta=0.02)
async def test_tapping_term_configuration_100(self):
time_ = await self.wait_for_timeout("mod_tap(a, b, 100)")
self.assertAlmostEqual(time_, 0.12, delta=0.02)
async def test_tapping_term_configuration_100_kwarg(self):
time_ = await self.wait_for_timeout("mod_tap(a, b, tapping_term=100)")
self.assertAlmostEqual(time_, 0.12, delta=0.02)