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>
This commit is contained in:
385
tests/unit/test_macros/test_mod_tap.py
Normal file
385
tests/unit/test_macros/test_mod_tap.py
Normal file
@ -0,0 +1,385 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user