def test_release_joystick_button(self):
        # with the left joystick mapped as button, it will release the mapped
        # key when it goes back to close to its resting position
        ev_1 = (3, 0, MAX_ABS // 10)  # release
        ev_3 = (3, 0, MIN_ABS)  # press

        uinput = UInput()

        _key_to_code = {
            ((3, 0, -1),): 73
        }

        self.mapping.set('gamepad.joystick.left_purpose', BUTTONS)

        # something with gamepad capabilities
        source = InputDevice('/dev/input/event30')

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, source, uinput)

        keycode_mapper.handle_keycode(new_event(*ev_3))
        keycode_mapper.handle_keycode(new_event(*ev_1))

        # array of 3-tuples
        history = [a.t for a in uinput_write_history]

        self.assertIn((EV_KEY, 73, 1), history)
        self.assertEqual(history.count((EV_KEY, 73, 1)), 1)

        self.assertIn((EV_KEY, 73, 0), history)
        self.assertEqual(history.count((EV_KEY, 73, 0)), 1)
Beispiel #2
0
    async def _event_consumer(self, source, forward_to):
        """Reads input events to inject keycodes or talk to the event_producer.

        Can be stopped by stopping the asyncio loop. This loop
        reads events from a single device only. Other devnodes may be
        present for the hardware device, in which case this needs to be
        started multiple times.

        Parameters
        ----------
        source : evdev.InputDevice
            where to read keycodes from
        forward_to : evdev.UInput
            where to write keycodes to that were not mapped to anything.
            Should be an UInput with capabilities that work for all forwarded
            events, so ideally they should be copied from source.
        """
        logger.debug(
            'Started consumer to inject to %s, fd %s',
            source.path, source.fd
        )

        gamepad = classify(source) == GAMEPAD

        keycode_handler = KeycodeMapper(self.context, source, forward_to)

        async for event in source.async_read_loop():
            if self._event_producer.is_handled(event):
                # the event_producer will take care of it
                self._event_producer.notify(event)
                continue

            # for mapped stuff
            if utils.should_map_as_btn(event, self.context.mapping, gamepad):
                will_report_key_up = utils.will_report_key_up(event)

                keycode_handler.handle_keycode(event)

                if not will_report_key_up:
                    # simulate a key-up event if no down event arrives anymore.
                    # this may release macros, combinations or keycodes.
                    release = evdev.InputEvent(0, 0, event.type, event.code, 0)
                    self._event_producer.debounce(
                        debounce_id=(event.type, event.code, event.value),
                        func=keycode_handler.handle_keycode,
                        args=(release, False),
                        ticks=3,
                    )

                continue

            # forward the rest
            forward_to.write(event.type, event.code, event.value)
            # this already includes SYN events, so need to syn here again

        # This happens all the time in tests because the async_read_loop
        # stops when there is nothing to read anymore. Otherwise tests
        # would block.
        logger.error('The consumer for "%s" stopped early', source.path)
    def test_hold_3(self):
        # test irregular input patterns
        code_a = 100
        code_b = 101
        code_c = 102
        system_mapping.clear()
        system_mapping._set('a', code_a)
        system_mapping._set('b', code_b)
        system_mapping._set('c', code_c)

        macro_mapping = {
            ((EV_KEY, 1, 1),): parse('k(a).h(k(b)).k(c)', self.mapping),
        }

        history = []

        def handler(*args):
            history.append(args)

        context = Context(self.mapping)
        context.macros = macro_mapping
        keycode_mapper = KeycodeMapper(context, self.source, None)

        keycode_mapper.macro_write = handler

        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1))
        loop = asyncio.get_event_loop()

        loop.run_until_complete(asyncio.sleep(0.1))
        for _ in range(5):
            self.assertTrue(active_macros[(EV_KEY, 1)].is_holding())
            self.assertTrue(active_macros[(EV_KEY, 1)].running)
            keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1))
            loop.run_until_complete(asyncio.sleep(0.05))

        # duplicate key down events don't do anything
        self.assertEqual(history.count((EV_KEY, code_a, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_a, 0)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 1)), 0)
        self.assertEqual(history.count((EV_KEY, code_c, 0)), 0)

        # stop
        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 0))
        loop.run_until_complete(asyncio.sleep(0.1))
        self.assertEqual(history.count((EV_KEY, code_a, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_a, 0)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 0)), 1)
        self.assertFalse(active_macros[(EV_KEY, 1)].is_holding())
        self.assertFalse(active_macros[(EV_KEY, 1)].running)

        # it's stopped and won't write stuff anymore
        count_before = len(history)
        loop.run_until_complete(asyncio.sleep(0.1))
        count_after = len(history)
        self.assertEqual(count_before, count_after)
    def test_ignore_hold(self):
        # hold as in event-value 2, not in macro-hold.
        # linux will generate events with value 2 after key-mapper injected
        # the key-press, so key-mapper doesn't need to forward them. That
        # would cause duplicate events of those values otherwise.
        key = (EV_KEY, KEY_A)
        ev_1 = (*key, 1)
        ev_2 = (*key, 2)
        ev_3 = (*key, 0)

        _key_to_code = {
            ((*key, 1),): 21,
        }

        uinput = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        keycode_mapper.handle_keycode(new_event(*ev_1))

        for _ in range(10):
            keycode_mapper.handle_keycode(new_event(*ev_2))

        self.assertIn(key, unreleased)
        keycode_mapper.handle_keycode(new_event(*ev_3))
        self.assertNotIn(key, unreleased)

        self.assertEqual(len(uinput_write_history), 2)
        self.assertEqual(uinput_write_history[0].t, (EV_KEY, 21, 1))
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, 21, 0))
    def test_two_d_pad_macros(self):
        # executing two macros that stop automatically at the same time

        code_1 = 61
        code_2 = 62
        system_mapping.clear()
        system_mapping._set('1', code_1)
        system_mapping._set('2', code_2)

        # try two concurrent macros with D-Pad events because they are
        # more difficult to manage, since their only difference is their
        # value, and one of them is negative.
        right = (EV_ABS, ABS_HAT0X, 1)
        release = (EV_ABS, ABS_HAT0X, 0)
        left = (EV_ABS, ABS_HAT0X, -1)

        repeats = 10

        macro_mapping = {
            (right,): parse(f'r({repeats}, k(1))', self.mapping),
            (left,): parse(f'r({repeats}, k(2))', self.mapping)
        }

        history = []

        def handler(*args):
            history.append(args)

        context = Context(self.mapping)
        context.macros = macro_mapping
        keycode_mapper = KeycodeMapper(context, self.source, None)

        keycode_mapper.macro_write = handler
        keycode_mapper.macro_write = handler

        keycode_mapper.handle_keycode(new_event(*right))
        self.assertIn((EV_ABS, ABS_HAT0X), unreleased)
        keycode_mapper.handle_keycode(new_event(*release))
        self.assertNotIn((EV_ABS, ABS_HAT0X), unreleased)
        keycode_mapper.handle_keycode(new_event(*left))
        self.assertIn((EV_ABS, ABS_HAT0X), unreleased)

        loop = asyncio.get_event_loop()
        sleeptime = config.get('macros.keystroke_sleep_ms') / 1000
        loop.run_until_complete(asyncio.sleep(1.1 * repeats * 2 * sleeptime))

        self.assertEqual(history.count((EV_KEY, code_1, 1)), 10)
        self.assertEqual(history.count((EV_KEY, code_1, 0)), 10)
        self.assertEqual(history.count((EV_KEY, code_2, 1)), 10)
        self.assertEqual(history.count((EV_KEY, code_2, 0)), 10)
        self.assertEqual(len(history), repeats * 4)
    def test_filter_trigger_spam(self):
        # test_filter_duplicates
        trigger = (EV_KEY, BTN_TL)

        _key_to_code = {
            ((*trigger, 1),): 51,
            ((*trigger, -1),): 52
        }

        uinput = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        """positive"""

        for _ in range(1, 20):
            keycode_mapper.handle_keycode(new_event(*trigger, 1))
            self.assertIn(trigger, unreleased)

        keycode_mapper.handle_keycode(new_event(*trigger, 0))
        self.assertNotIn(trigger, unreleased)

        self.assertEqual(len(uinput_write_history), 2)

        """negative"""

        for _ in range(1, 20):
            keycode_mapper.handle_keycode(new_event(*trigger, -1))
            self.assertIn(trigger, unreleased)

        keycode_mapper.handle_keycode(new_event(*trigger, 0))
        self.assertNotIn(trigger, unreleased)

        self.assertEqual(len(uinput_write_history), 4)
        self.assertEqual(uinput_write_history[0].t, (EV_KEY, 51, 1))
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, 51, 0))
        self.assertEqual(uinput_write_history[2].t, (EV_KEY, 52, 1))
        self.assertEqual(uinput_write_history[3].t, (EV_KEY, 52, 0))
    def test_handle_keycode(self):
        code_2 = 2
        # this also makes sure that the keycode_mapper doesn't get confused
        # when input and output codes are the same (because it at some point
        # screwed it up because of that)
        _key_to_code = {
            ((EV_KEY, 1, 1),): 101,
            ((EV_KEY, code_2, 1),): code_2
        }

        uinput_mapped = UInput()
        uinput_forwarded = UInput()

        context = Context(self.mapping)
        context.uinput = uinput_mapped
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, self.source, uinput_forwarded)

        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1))
        keycode_mapper.handle_keycode(new_event(EV_KEY, 3, 1))
        keycode_mapper.handle_keycode(new_event(EV_KEY, code_2, 1))
        keycode_mapper.handle_keycode(new_event(EV_KEY, code_2, 0))

        self.assertEqual(len(uinput_write_history), 4)
        self.assertEqual(uinput_mapped.write_history[0].t, (EV_KEY, 101, 1))
        self.assertEqual(uinput_mapped.write_history[1].t, (EV_KEY, code_2, 1))
        self.assertEqual(uinput_mapped.write_history[2].t, (EV_KEY, code_2, 0))

        self.assertEqual(uinput_forwarded.write_history[0].t, (EV_KEY, 3, 1))
    def test_dont_filter_unmapped(self):
        # if an event is not used at all, it should be written but not
        # furthermore modified. For example wheel events
        # keep reporting events of the same value without a release inbetween,
        # they should be forwarded.

        down = (EV_KEY, 91, 1)
        up = (EV_KEY, 91, 0)
        uinput = UInput()
        forward_to = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        keycode_mapper = KeycodeMapper(context, self.source, forward_to)

        for _ in range(10):
            # don't filter duplicate events if not mapped
            keycode_mapper.handle_keycode(new_event(*down))

        self.assertEqual(unreleased[(EV_KEY, 91)].input_event_tuple, down)
        self.assertEqual(unreleased[(EV_KEY, 91)].target_type_code, down[:2])
        self.assertEqual(len(unreleased), 1)
        self.assertEqual(forward_to.write_count, 10)
        self.assertEqual(uinput.write_count, 0)

        keycode_mapper.handle_keycode(new_event(*up))
        self.assertEqual(len(unreleased), 0)
        self.assertEqual(forward_to.write_count, 11)
        self.assertEqual(uinput.write_count, 0)
    def test_handle_keycode_macro(self):
        history = []

        code_a = 100
        code_b = 101
        system_mapping.clear()
        system_mapping._set('a', code_a)
        system_mapping._set('b', code_b)

        macro_mapping = {
            ((EV_KEY, 1, 1),): parse('k(a)', self.mapping),
            ((EV_KEY, 2, 1),): parse('r(5, k(b))', self.mapping)
        }

        context = Context(self.mapping)
        context.macros = macro_mapping
        keycode_mapper = KeycodeMapper(context, self.source, None)

        keycode_mapper.macro_write = lambda *args: history.append(args)
        keycode_mapper.macro_write = lambda *args: history.append(args)

        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1))
        keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 1))

        loop = asyncio.get_event_loop()

        sleeptime = config.get('macros.keystroke_sleep_ms', 10) * 12

        # let the mainloop run for some time so that the macro does its stuff
        loop.run_until_complete(asyncio.sleep(sleeptime / 1000 + 0.1))

        # 6 keycodes written, with down and up events
        self.assertEqual(len(history), 12)
        self.assertIn((EV_KEY, code_a, 1), history)
        self.assertIn((EV_KEY, code_a, 0), history)
        self.assertIn((EV_KEY, code_b, 1), history)
        self.assertIn((EV_KEY, code_b, 0), history)

        # releasing stuff
        self.assertIn((EV_KEY, 1), unreleased)
        self.assertIn((EV_KEY, 2), unreleased)
        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 0))
        keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 0))
        self.assertNotIn((EV_KEY, 1), unreleased)
        self.assertNotIn((EV_KEY, 2), unreleased)
        loop.run_until_complete(asyncio.sleep(0.1))
        self.assertEqual(len(history), 12)
    def test_combination_keycode(self):
        combination = ((EV_KEY, 1, 1), (EV_KEY, 2, 1))
        _key_to_code = {
            combination: 101
        }

        uinput = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        keycode_mapper.handle_keycode(new_event(*combination[0]))
        keycode_mapper.handle_keycode(new_event(*combination[1]))

        self.assertEqual(len(uinput_write_history), 2)
        # the first event is written and then the triggered combination
        self.assertEqual(uinput_write_history[0].t, (EV_KEY, 1, 1))
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, 101, 1))

        # release them
        keycode_mapper.handle_keycode(new_event(*combination[0][:2], 0))
        keycode_mapper.handle_keycode(new_event(*combination[1][:2], 0))
        # the first key writes its release event. The second key is hidden
        # behind the executed combination. The result of the combination is
        # also released, because it acts like a key.
        self.assertEqual(len(uinput_write_history), 4)
        self.assertEqual(uinput_write_history[2].t, (EV_KEY, 1, 0))
        self.assertEqual(uinput_write_history[3].t, (EV_KEY, 101, 0))

        # press them in the wrong order (the wrong key at the end, the order
        # of all other keys won't matter). no combination should be triggered
        keycode_mapper.handle_keycode(new_event(*combination[1]))
        keycode_mapper.handle_keycode(new_event(*combination[0]))
        self.assertEqual(len(uinput_write_history), 6)
        self.assertEqual(uinput_write_history[4].t, (EV_KEY, 2, 1))
        self.assertEqual(uinput_write_history[5].t, (EV_KEY, 1, 1))
    def test_filter_combi_mapped_duplicate_down(self):
        # the opposite of the other test, but don't map the key directly
        # but rather as the trigger for a combination
        down_1 = (EV_KEY, 91, 1)
        down_2 = (EV_KEY, 92, 1)
        up_1 = (EV_KEY, 91, 0)
        up_2 = (EV_KEY, 92, 0)
        uinput = UInput()

        output = 71

        key_to_code = {
            (down_1, down_2): 71
        }

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = key_to_code
        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        keycode_mapper.handle_keycode(new_event(*down_1))
        for _ in range(10):
            keycode_mapper.handle_keycode(new_event(*down_2))

        # all duplicate down events should have been ignored
        self.assertEqual(len(unreleased), 2)
        self.assertEqual(uinput.write_count, 2)
        self.assertEqual(uinput_write_history[0].t, down_1)
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, output, 1))

        keycode_mapper.handle_keycode(new_event(*up_1))
        keycode_mapper.handle_keycode(new_event(*up_2))
        self.assertEqual(len(unreleased), 0)
        self.assertEqual(uinput.write_count, 4)
        self.assertEqual(uinput_write_history[2].t, up_1)
        self.assertEqual(uinput_write_history[3].t, (EV_KEY, output, 0))
    def test_not_forward(self):
        down = (EV_KEY, 91, 1)
        up = (EV_KEY, 91, 0)
        uinput = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        keycode_mapper.handle_keycode(new_event(*down), forward=False)
        self.assertEqual(unreleased[(EV_KEY, 91)].input_event_tuple, down)
        self.assertEqual(unreleased[(EV_KEY, 91)].target_type_code, down[:2])
        self.assertEqual(len(unreleased), 1)
        self.assertEqual(uinput.write_count, 0)

        keycode_mapper.handle_keycode(new_event(*up), forward=False)
        self.assertEqual(len(unreleased), 0)
        self.assertEqual(uinput.write_count, 0)
    def test_d_pad_combination(self):
        ev_1 = (EV_ABS, ABS_HAT0X, 1)
        ev_2 = (EV_ABS, ABS_HAT0Y, -1)

        ev_3 = (EV_ABS, ABS_HAT0X, 0)
        ev_4 = (EV_ABS, ABS_HAT0Y, 0)

        _key_to_code = {
            (ev_1, ev_2): 51,
            (ev_2,): 52,
        }

        uinput = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        # a bunch of d-pad key down events at once
        keycode_mapper.handle_keycode(new_event(*ev_1))
        keycode_mapper.handle_keycode(new_event(*ev_2))
        # (what_will_be_released, what_caused_the_key_down)
        self.assertEqual(unreleased.get(ev_1[:2]).target_type_code, (EV_ABS, ABS_HAT0X))
        self.assertEqual(unreleased.get(ev_1[:2]).input_event_tuple, ev_1)
        self.assertEqual(unreleased.get(ev_2[:2]).target_type_code, (EV_KEY, 51))
        self.assertEqual(unreleased.get(ev_2[:2]).input_event_tuple, ev_2)
        self.assertEqual(len(unreleased), 2)

        # ev_1 is unmapped and the other is the triggered combination
        self.assertEqual(len(uinput_write_history), 2)
        self.assertEqual(uinput_write_history[0].t, ev_1)
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, 51, 1))

        # release all of them
        keycode_mapper.handle_keycode(new_event(*ev_3))
        keycode_mapper.handle_keycode(new_event(*ev_4))
        self.assertEqual(len(unreleased), 0)

        self.assertEqual(len(uinput_write_history), 4)
        self.assertEqual(uinput_write_history[2].t, ev_3)
        self.assertEqual(uinput_write_history[3].t, (EV_KEY, 51, 0))
    def test_macro_writes_to_context_uinput(self):
        macro_mapping = {
            ((EV_KEY, 1, 1),): parse('k(a)', self.mapping)
        }

        context = Context(self.mapping)
        context.macros = macro_mapping
        context.uinput = UInput()
        forward_to = UInput()
        keycode_mapper = KeycodeMapper(context, self.source, forward_to)

        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1))

        loop = asyncio.get_event_loop()
        sleeptime = config.get('macros.keystroke_sleep_ms', 10) * 12
        loop.run_until_complete(asyncio.sleep(sleeptime / 1000 + 0.1))

        self.assertEqual(context.uinput.write_count, 2)  # down and up
        self.assertEqual(forward_to.write_count, 0)

        keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 1))
        self.assertEqual(forward_to.write_count, 1)
    def test_ignore_disabled(self):
        ev_1 = (EV_ABS, ABS_HAT0Y, 1)
        ev_2 = (EV_ABS, ABS_HAT0Y, 0)

        ev_3 = (EV_ABS, ABS_HAT0X, 1)  # disabled
        ev_4 = (EV_ABS, ABS_HAT0X, 0)

        ev_5 = (EV_KEY, KEY_A, 1)
        ev_6 = (EV_KEY, KEY_A, 0)

        combi_1 = (ev_5, ev_3)
        combi_2 = (ev_3, ev_5)

        _key_to_code = {
            (ev_1,): 61,
            (ev_3,): DISABLE_CODE,
            combi_1: 62,
            combi_2: 63
        }

        uinput = UInput()
        forward_to = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, self.source, forward_to)

        def expect_writecounts(uinput_count, forwarded_count):
            self.assertEqual(uinput.write_count, uinput_count)
            self.assertEqual(forward_to.write_count, forwarded_count)

        """single keys"""

        # down
        keycode_mapper.handle_keycode(new_event(*ev_1))
        keycode_mapper.handle_keycode(new_event(*ev_3))
        self.assertIn(ev_1[:2], unreleased)
        self.assertIn(ev_3[:2], unreleased)
        expect_writecounts(1, 0)
        # up
        keycode_mapper.handle_keycode(new_event(*ev_2))
        keycode_mapper.handle_keycode(new_event(*ev_4))
        expect_writecounts(2, 0)
        self.assertNotIn(ev_1[:2], unreleased)
        self.assertNotIn(ev_3[:2], unreleased)

        self.assertEqual(len(uinput_write_history), 2)
        self.assertEqual(uinput_write_history[0].t, (EV_KEY, 61, 1))
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, 61, 0))

        """a combination that ends in a disabled key"""

        # ev_5 should be forwarded and the combination triggered
        keycode_mapper.handle_keycode(new_event(*combi_1[0]))  # ev_5
        keycode_mapper.handle_keycode(new_event(*combi_1[1]))  # ev_3
        expect_writecounts(3, 1)
        self.assertEqual(len(uinput_write_history), 4)
        self.assertEqual(uinput_write_history[2].t, (EV_KEY, KEY_A, 1))
        self.assertEqual(uinput_write_history[3].t, (EV_KEY, 62, 1))
        self.assertIn(combi_1[0][:2], unreleased)
        self.assertIn(combi_1[1][:2], unreleased)
        # since this event did not trigger anything, key is None
        self.assertEqual(unreleased[combi_1[0][:2]].triggered_key, None)
        # that one triggered something from _key_to_code, so the key is that
        self.assertEqual(unreleased[combi_1[1][:2]].triggered_key, combi_1)

        # release the last key of the combi first, it should
        # release what the combination maps to
        event = new_event(combi_1[1][0], combi_1[1][1], 0)
        keycode_mapper.handle_keycode(event)
        expect_writecounts(4, 1)
        self.assertEqual(len(uinput_write_history), 5)
        self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 62, 0))
        self.assertIn(combi_1[0][:2], unreleased)
        self.assertNotIn(combi_1[1][:2], unreleased)

        event = new_event(combi_1[0][0], combi_1[0][1], 0)
        keycode_mapper.handle_keycode(event)
        expect_writecounts(4, 2)
        self.assertEqual(len(uinput_write_history), 6)
        self.assertEqual(uinput_write_history[-1].t, (EV_KEY, KEY_A, 0))
        self.assertNotIn(combi_1[0][:2], unreleased)
        self.assertNotIn(combi_1[1][:2], unreleased)

        """a combination that starts with a disabled key"""

        # only the combination should get triggered
        keycode_mapper.handle_keycode(new_event(*combi_2[0]))
        keycode_mapper.handle_keycode(new_event(*combi_2[1]))
        expect_writecounts(5, 2)
        self.assertEqual(len(uinput_write_history), 7)
        self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 63, 1))

        # release the last key of the combi first, it should
        # release what the combination maps to
        event = new_event(combi_2[1][0], combi_2[1][1], 0)
        keycode_mapper.handle_keycode(event)
        self.assertEqual(len(uinput_write_history), 8)
        self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 63, 0))
        expect_writecounts(6, 2)

        # the first key of combi_2 is disabled, so it won't write another
        # key-up event
        event = new_event(combi_2[0][0], combi_2[0][1], 0)
        keycode_mapper.handle_keycode(event)
        self.assertEqual(len(uinput_write_history), 8)
        expect_writecounts(6, 2)
    def test_hold_two(self):
        # holding two macros at the same time,
        # the first one is triggered by a combination
        history = []

        code_1 = 100
        code_2 = 101
        code_3 = 102
        code_a = 103
        code_b = 104
        code_c = 105
        system_mapping.clear()
        system_mapping._set('1', code_1)
        system_mapping._set('2', code_2)
        system_mapping._set('3', code_3)
        system_mapping._set('a', code_a)
        system_mapping._set('b', code_b)
        system_mapping._set('c', code_c)

        key_0 = (EV_KEY, 10)
        key_1 = (EV_KEY, 11)
        key_2 = (EV_ABS, ABS_HAT0X)
        down_0 = (*key_0, 1)
        down_1 = (*key_1, 1)
        down_2 = (*key_2, -1)
        up_0 = (*key_0, 0)
        up_1 = (*key_1, 0)
        up_2 = (*key_2, 0)

        macro_mapping = {
            (down_0, down_1): parse('k(1).h(k(2)).k(3)', self.mapping),
            (down_2,): parse('k(a).h(k(b)).k(c)', self.mapping)
        }

        def handler(*args):
            history.append(args)

        loop = asyncio.get_event_loop()

        context = Context(self.mapping)
        context.macros = macro_mapping

        uinput_1 = UInput()
        context.uinput = uinput_1

        keycode_mapper = KeycodeMapper(context, self.source, uinput_1)

        keycode_mapper.macro_write = handler

        # key up won't do anything
        keycode_mapper.handle_keycode(new_event(*up_0))
        keycode_mapper.handle_keycode(new_event(*up_1))
        keycode_mapper.handle_keycode(new_event(*up_2))
        loop.run_until_complete(asyncio.sleep(0.1))
        self.assertEqual(len(active_macros), 0)

        """start macros"""

        uinput_2 = UInput()
        context.uinput = uinput_2

        keycode_mapper = KeycodeMapper(context, self.source, uinput_2)

        keycode_mapper.macro_write = handler

        keycode_mapper.handle_keycode(new_event(*down_0))
        self.assertEqual(uinput_2.write_count, 1)
        keycode_mapper.handle_keycode(new_event(*down_1))
        keycode_mapper.handle_keycode(new_event(*down_2))
        self.assertEqual(uinput_2.write_count, 1)

        # let the mainloop run for some time so that the macro does its stuff
        sleeptime = 500
        keystroke_sleep = config.get('macros.keystroke_sleep_ms', 10)
        loop.run_until_complete(asyncio.sleep(sleeptime / 1000))

        self.assertEqual(len(active_macros), 2)
        self.assertTrue(active_macros[key_1].is_holding())
        self.assertTrue(active_macros[key_1].running)
        self.assertTrue(active_macros[key_2].is_holding())
        self.assertTrue(active_macros[key_2].running)

        self.assertIn(down_0[:2], unreleased)
        self.assertIn(down_1[:2], unreleased)
        self.assertIn(down_2[:2], unreleased)

        """stop macros"""

        keycode_mapper = KeycodeMapper(context, self.source, None)

        # releasing the last key of a combination releases the whole macro
        keycode_mapper.handle_keycode(new_event(*up_1))
        keycode_mapper.handle_keycode(new_event(*up_2))

        self.assertIn(down_0[:2], unreleased)
        self.assertNotIn(down_1[:2], unreleased)
        self.assertNotIn(down_2[:2], unreleased)

        loop.run_until_complete(asyncio.sleep(keystroke_sleep * 10 / 1000))

        self.assertFalse(active_macros[key_1].is_holding())
        self.assertFalse(active_macros[key_1].running)
        self.assertFalse(active_macros[key_2].is_holding())
        self.assertFalse(active_macros[key_2].running)

        events = calculate_event_number(sleeptime, 1, 1) * 2

        self.assertGreater(len(history), events * 0.9)
        self.assertLess(len(history), events * 1.1)

        self.assertIn((EV_KEY, code_a, 1), history)
        self.assertIn((EV_KEY, code_a, 0), history)
        self.assertIn((EV_KEY, code_b, 1), history)
        self.assertIn((EV_KEY, code_b, 0), history)
        self.assertIn((EV_KEY, code_c, 1), history)
        self.assertIn((EV_KEY, code_c, 0), history)
        self.assertIn((EV_KEY, code_1, 1), history)
        self.assertIn((EV_KEY, code_1, 0), history)
        self.assertIn((EV_KEY, code_2, 1), history)
        self.assertIn((EV_KEY, code_2, 0), history)
        self.assertIn((EV_KEY, code_3, 1), history)
        self.assertIn((EV_KEY, code_3, 0), history)
        self.assertGreater(history.count((EV_KEY, code_b, 1)), 1)
        self.assertGreater(history.count((EV_KEY, code_b, 0)), 1)
        self.assertGreater(history.count((EV_KEY, code_2, 1)), 1)
        self.assertGreater(history.count((EV_KEY, code_2, 0)), 1)

        # it's stopped and won't write stuff anymore
        count_before = len(history)
        loop.run_until_complete(asyncio.sleep(0.2))
        count_after = len(history)
        self.assertEqual(count_before, count_after)
    def test_combination_keycode_macro_mix(self):
        # ev_1 triggers macro, ev_1 + ev_2 triggers key while the macro is
        # still running
        system_mapping.clear()
        system_mapping._set('a', 92)

        down_1 = (EV_ABS, ABS_HAT1X, 1)
        down_2 = (EV_ABS, ABS_HAT1Y, -1)
        up_1 = (EV_ABS, ABS_HAT1X, 0)
        up_2 = (EV_ABS, ABS_HAT1Y, 0)

        macro_mapping = {(down_1,): parse('h(k(a))', self.mapping)}
        _key_to_code = {(down_1, down_2): 91}

        macro_history = []
        def handler(*args):
            # handler prevents uinput_write_history form growing
            macro_history.append(args)

        uinput = UInput()
        forward_to = UInput()

        loop = asyncio.get_event_loop()

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        context.macros = macro_mapping
        keycode_mapper = KeycodeMapper(context, self.source, forward_to)

        keycode_mapper.macro_write = handler

        # macro starts
        keycode_mapper.handle_keycode(new_event(*down_1))
        loop.run_until_complete(asyncio.sleep(0.05))
        self.assertEqual(len(uinput_write_history), 0)
        self.assertGreater(len(macro_history), 1)
        self.assertIn(down_1[:2], unreleased)
        self.assertIn((EV_KEY, 92, 1), macro_history)

        # combination triggered
        keycode_mapper.handle_keycode(new_event(*down_2))
        self.assertIn(down_1[:2], unreleased)
        self.assertIn(down_2[:2], unreleased)
        self.assertEqual(uinput_write_history[0].t, (EV_KEY, 91, 1))

        len_a = len(macro_history)
        loop.run_until_complete(asyncio.sleep(0.05))
        len_b = len(macro_history)
        # still running
        self.assertGreater(len_b, len_a)

        # release
        keycode_mapper.handle_keycode(new_event(*up_1))
        self.assertNotIn(down_1[:2], unreleased)
        self.assertIn(down_2[:2], unreleased)
        loop.run_until_complete(asyncio.sleep(0.05))
        len_c = len(macro_history)
        loop.run_until_complete(asyncio.sleep(0.05))
        len_d = len(macro_history)
        # not running anymore
        self.assertEqual(len_c, len_d)

        keycode_mapper.handle_keycode(new_event(*up_2))
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, 91, 0))
        self.assertEqual(len(uinput_write_history), 2)
        self.assertNotIn(down_1[:2], unreleased)
        self.assertNotIn(down_2[:2], unreleased)
    def test_hold_2(self):
        # test irregular input patterns
        code_a = 100
        code_b = 101
        code_c = 102
        code_d = 103
        system_mapping.clear()
        system_mapping._set('a', code_a)
        system_mapping._set('b', code_b)
        system_mapping._set('c', code_c)
        system_mapping._set('d', code_d)

        macro_mapping = {
            ((EV_KEY, 1, 1),): parse('h(k(b))', self.mapping),
            ((EV_KEY, 2, 1),): parse('k(c).r(1, r(1, r(1, h(k(a))))).k(d)', self.mapping),
            ((EV_KEY, 3, 1),): parse('h(k(b))', self.mapping)
        }

        history = []

        def handler(*args):
            history.append(args)

        context = Context(self.mapping)
        context.macros = macro_mapping
        keycode_mapper = KeycodeMapper(context, self.source, None)

        keycode_mapper.macro_write = handler
        keycode_mapper.macro_write = handler
        keycode_mapper.macro_write = handler

        """start macro 2"""

        keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 1))
        loop = asyncio.get_event_loop()

        loop.run_until_complete(asyncio.sleep(0.1))
        # starting code_c written
        self.assertEqual(history.count((EV_KEY, code_c, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 0)), 1)

        # spam garbage events
        for _ in range(5):
            keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1))
            keycode_mapper.handle_keycode(new_event(EV_KEY, 3, 1))
            loop.run_until_complete(asyncio.sleep(0.05))
            self.assertTrue(active_macros[(EV_KEY, 1)].is_holding())
            self.assertTrue(active_macros[(EV_KEY, 1)].running)
            self.assertTrue(active_macros[(EV_KEY, 2)].is_holding())
            self.assertTrue(active_macros[(EV_KEY, 2)].running)
            self.assertTrue(active_macros[(EV_KEY, 3)].is_holding())
            self.assertTrue(active_macros[(EV_KEY, 3)].running)

        # there should only be one code_c in the events, because no key
        # up event was ever done so the hold just continued
        self.assertEqual(history.count((EV_KEY, code_c, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 0)), 1)
        # without an key up event on 2, it won't write code_d
        self.assertNotIn((code_d, 1), history)
        self.assertNotIn((code_d, 0), history)

        # stop macro 2
        keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 0))
        loop.run_until_complete(asyncio.sleep(0.1))

        # it stopped and didn't restart, so the count stays at 1
        self.assertEqual(history.count((EV_KEY, code_c, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 0)), 1)
        # and the trailing d was written
        self.assertEqual(history.count((EV_KEY, code_d, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_d, 0)), 1)

        # it's stopped and won't write stuff anymore
        count_before = history.count((EV_KEY, code_a, 1))
        self.assertGreater(count_before, 1)
        loop.run_until_complete(asyncio.sleep(0.1))
        count_after = history.count((EV_KEY, code_a, 1))
        self.assertEqual(count_before, count_after)

        """restart macro 2"""

        history = []

        keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 1))
        loop.run_until_complete(asyncio.sleep(0.1))
        self.assertEqual(history.count((EV_KEY, code_c, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 0)), 1)

        # spam garbage events again, this time key-up events on all other
        # macros
        for _ in range(5):
            keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 0))
            keycode_mapper.handle_keycode(new_event(EV_KEY, 3, 0))
            loop.run_until_complete(asyncio.sleep(0.05))
            self.assertFalse(active_macros[(EV_KEY, 1)].is_holding())
            self.assertFalse(active_macros[(EV_KEY, 1)].running)
            self.assertTrue(active_macros[(EV_KEY, 2)].is_holding())
            self.assertTrue(active_macros[(EV_KEY, 2)].running)
            self.assertFalse(active_macros[(EV_KEY, 3)].is_holding())
            self.assertFalse(active_macros[(EV_KEY, 3)].running)

        # stop macro 2
        keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 0))
        loop.run_until_complete(asyncio.sleep(0.1))
        # was started only once
        self.assertEqual(history.count((EV_KEY, code_c, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_c, 0)), 1)
        # and the trailing d was also written only once
        self.assertEqual(history.count((EV_KEY, code_d, 1)), 1)
        self.assertEqual(history.count((EV_KEY, code_d, 0)), 1)

        # stop all macros
        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 0))
        keycode_mapper.handle_keycode(new_event(EV_KEY, 3, 0))
        loop.run_until_complete(asyncio.sleep(0.1))

        # it's stopped and won't write stuff anymore
        count_before = len(history)
        loop.run_until_complete(asyncio.sleep(0.1))
        count_after = len(history)
        self.assertEqual(count_before, count_after)

        self.assertFalse(active_macros[(EV_KEY, 1)].is_holding())
        self.assertFalse(active_macros[(EV_KEY, 1)].running)
        self.assertFalse(active_macros[(EV_KEY, 2)].is_holding())
        self.assertFalse(active_macros[(EV_KEY, 2)].running)
        self.assertFalse(active_macros[(EV_KEY, 3)].is_holding())
        self.assertFalse(active_macros[(EV_KEY, 3)].running)
    def test_hold(self):
        history = []

        code_a = 100
        code_b = 101
        code_c = 102
        system_mapping.clear()
        system_mapping._set('a', code_a)
        system_mapping._set('b', code_b)
        system_mapping._set('c', code_c)

        macro_mapping = {
            ((EV_KEY, 1, 1),): parse('k(a).h(k(b)).k(c)', self.mapping)
        }

        def handler(*args):
            history.append(args)

        context = Context(self.mapping)
        context.macros = macro_mapping
        keycode_mapper = KeycodeMapper(context, self.source, None)

        keycode_mapper.macro_write = handler

        """start macro"""

        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1))

        loop = asyncio.get_event_loop()

        # let the mainloop run for some time so that the macro does its stuff
        sleeptime = 500
        keystroke_sleep = config.get('macros.keystroke_sleep_ms', 10)
        loop.run_until_complete(asyncio.sleep(sleeptime / 1000))

        self.assertTrue(active_macros[(EV_KEY, 1)].is_holding())
        self.assertTrue(active_macros[(EV_KEY, 1)].running)

        """stop macro"""

        keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 0))

        loop.run_until_complete(asyncio.sleep(keystroke_sleep * 10 / 1000))

        events = calculate_event_number(sleeptime, 1, 1)

        self.assertGreater(len(history), events * 0.9)
        self.assertLess(len(history), events * 1.1)

        self.assertIn((EV_KEY, code_a, 1), history)
        self.assertIn((EV_KEY, code_a, 0), history)
        self.assertIn((EV_KEY, code_b, 1), history)
        self.assertIn((EV_KEY, code_b, 0), history)
        self.assertIn((EV_KEY, code_c, 1), history)
        self.assertIn((EV_KEY, code_c, 0), history)
        self.assertGreater(history.count((EV_KEY, code_b, 1)), 1)
        self.assertGreater(history.count((EV_KEY, code_b, 0)), 1)

        # it's stopped and won't write stuff anymore
        count_before = len(history)
        loop.run_until_complete(asyncio.sleep(0.2))
        count_after = len(history)
        self.assertEqual(count_before, count_after)

        self.assertFalse(active_macros[(EV_KEY, 1)].is_holding())
        self.assertFalse(active_macros[(EV_KEY, 1)].running)
    def test_combination_keycode_2(self):
        combination_1 = (
            (EV_KEY, 1, 1),
            (EV_ABS, ABS_Y, MIN_ABS),
            (EV_KEY, 3, 1),
            (EV_KEY, 4, 1)
        )
        combination_2 = (
            # should not be triggered, combination_1 should be prioritized
            # when all of its keys are down
            (EV_KEY, 2, 1),
            (EV_KEY, 3, 1),
            (EV_KEY, 4, 1)
        )

        down_5 = (EV_KEY, 5, 1)
        up_5 = (EV_KEY, 5, 0)
        up_4 = (EV_KEY, 4, 0)

        def sign_value(key):
            return key[0], key[1], key[2] / abs(key[2])

        _key_to_code = {
            # key_to_code is supposed to only contain normalized values
            tuple([sign_value(a) for a in combination_1]): 101,
            combination_2: 102,
            (down_5,): 103
        }

        uinput = UInput()

        source = InputDevice('/dev/input/event30')

        # ABS_Y is part of the combination, which only works if the joystick
        # is configured as D-Pad
        self.mapping.set('gamepad.joystick.left_purpose', BUTTONS)
        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = _key_to_code
        keycode_mapper = KeycodeMapper(context, source, uinput)

        # 10 and 11: insert some more arbitrary key-down events,
        # they should not break the combinations
        keycode_mapper.handle_keycode(new_event(EV_KEY, 10, 1))
        keycode_mapper.handle_keycode(new_event(*combination_1[0]))
        keycode_mapper.handle_keycode(new_event(*combination_1[1]))
        keycode_mapper.handle_keycode(new_event(*combination_1[2]))
        keycode_mapper.handle_keycode(new_event(EV_KEY, 11, 1))
        keycode_mapper.handle_keycode(new_event(*combination_1[3]))
        # combination_1 should have been triggered now

        self.assertEqual(len(uinput_write_history), 6)
        # the first events are written and then the triggered combination,
        # while the triggering event is the only one that is omitted
        self.assertEqual(uinput_write_history[1].t, combination_1[0])
        self.assertEqual(uinput_write_history[2].t, combination_1[1])
        self.assertEqual(uinput_write_history[3].t, combination_1[2])
        self.assertEqual(uinput_write_history[5].t, (EV_KEY, 101, 1))

        # while the combination is down, another unrelated key can be used
        keycode_mapper.handle_keycode(new_event(*down_5))
        # the keycode_mapper searches for subsets of the current held-down
        # keys to activate combinations, down_5 should not trigger them
        # again.
        self.assertEqual(len(uinput_write_history), 7)
        self.assertEqual(uinput_write_history[6].t, (EV_KEY, 103, 1))

        # release the combination by releasing the last key, and release
        # the unrelated key
        keycode_mapper.handle_keycode(new_event(*up_4))
        keycode_mapper.handle_keycode(new_event(*up_5))
        self.assertEqual(len(uinput_write_history), 9)

        self.assertEqual(uinput_write_history[7].t, (EV_KEY, 101, 0))
        self.assertEqual(uinput_write_history[8].t, (EV_KEY, 103, 0))
    def test_d_pad(self):
        ev_1 = (EV_ABS, ABS_HAT0X, 1)
        ev_2 = (EV_ABS, ABS_HAT0X, -1)
        ev_3 = (EV_ABS, ABS_HAT0X, 0)

        ev_4 = (EV_ABS, ABS_HAT0Y, 1)
        ev_5 = (EV_ABS, ABS_HAT0Y, -1)
        ev_6 = (EV_ABS, ABS_HAT0Y, 0)

        uinput = UInput()
        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = {
            (ev_1,): 51,
            (ev_2,): 52,
            (ev_4,): 54,
            (ev_5,): 55,
        }

        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        # a bunch of d-pad key down events at once
        keycode_mapper.handle_keycode(new_event(*ev_1))
        keycode_mapper.handle_keycode(new_event(*ev_4))
        self.assertEqual(len(unreleased), 2)

        self.assertEqual(
            unreleased.get(ev_1[:2]).target_type_code,
            (EV_KEY, context.key_to_code[(ev_1,)])
        )
        self.assertEqual(unreleased.get(ev_1[:2]).input_event_tuple, ev_1)
        self.assertEqual(unreleased.get(ev_1[:2]).triggered_key, (ev_1,))  # as seen in key_to_code

        self.assertEqual(
            unreleased.get(ev_4[:2]).target_type_code,
            (EV_KEY, context.key_to_code[(ev_4,)]), ev_4
        )
        self.assertEqual(unreleased.get(ev_4[:2]).input_event_tuple, ev_4)
        self.assertEqual(unreleased.get(ev_4[:2]).triggered_key, (ev_4,))

        # release all of them
        keycode_mapper.handle_keycode(new_event(*ev_3))
        keycode_mapper.handle_keycode(new_event(*ev_6))
        self.assertEqual(len(unreleased), 0)

        # repeat with other values
        keycode_mapper.handle_keycode(new_event(*ev_2))
        keycode_mapper.handle_keycode(new_event(*ev_5))
        self.assertEqual(len(unreleased), 2)
        self.assertEqual(
            unreleased.get(ev_2[:2]).target_type_code,
            (EV_KEY, context.key_to_code[(ev_2,)])
        )
        self.assertEqual(
            unreleased.get(ev_2[:2]).input_event_tuple,
            ev_2
        )
        self.assertEqual(
            unreleased.get(ev_5[:2]).target_type_code,
            (EV_KEY, context.key_to_code[(ev_5,)])
        )
        self.assertEqual(
            unreleased.get(ev_5[:2]).input_event_tuple,
            ev_5
        )

        # release all of them again
        keycode_mapper.handle_keycode(new_event(*ev_3))
        keycode_mapper.handle_keycode(new_event(*ev_6))
        self.assertEqual(len(unreleased), 0)

        self.assertEqual(len(uinput_write_history), 8)

        self.assertEqual(uinput_write_history[0].t, (EV_KEY, 51, 1))
        self.assertEqual(uinput_write_history[1].t, (EV_KEY, 54, 1))

        self.assertEqual(uinput_write_history[2].t, (EV_KEY, 51, 0))
        self.assertEqual(uinput_write_history[3].t, (EV_KEY, 54, 0))

        self.assertEqual(uinput_write_history[4].t, (EV_KEY, 52, 1))
        self.assertEqual(uinput_write_history[5].t, (EV_KEY, 55, 1))

        self.assertEqual(uinput_write_history[6].t, (EV_KEY, 52, 0))
        self.assertEqual(uinput_write_history[7].t, (EV_KEY, 55, 0))
    def test_wheel_combination_release_failure(self):
        # test based on a bug that once occurred
        # 1 | 22.6698, ((1, 276, 1)) -------------- forwarding
        # 2 | 22.9904, ((1, 276, 1), (2, 8, -1)) -- maps to 30
        # 3 | 23.0103, ((1, 276, 1), (2, 8, -1)) -- duplicate key down
        # 4 | ... 34 more duplicate key downs (scrolling)
        # 5 | 23.7104, ((1, 276, 1), (2, 8, -1)) -- duplicate key down
        # 6 | 23.7283, ((1, 276, 0)) -------------- forwarding release
        # 7 | 23.7303, ((2, 8, -1)) --------------- forwarding
        # 8 | 23.7865, ((2, 8, 0)) ---------------- not forwarding release
        # line 7 should have been "duplicate key down" as well
        # line 8 should have released 30, instead it was never released

        scroll = (2, 8, -1)
        scroll_up = (2, 8, 0)
        btn_down = (1, 276, 1)
        btn_up = (1, 276, 0)
        combination = ((1, 276, 1), (2, 8, -1))

        system_mapping.clear()
        system_mapping._set('a', 30)
        k2c = {combination: 30}

        uinput = UInput()

        context = Context(self.mapping)
        context.uinput = uinput
        context.key_to_code = k2c
        keycode_mapper = KeycodeMapper(context, self.source, uinput)

        keycode_mapper.handle_keycode(new_event(*btn_down))
        # "forwarding"
        self.assertEqual(uinput_write_history[0].t, btn_down)

        keycode_mapper.handle_keycode(new_event(*scroll))
        # "maps to 30"
        self.assertEqual(uinput_write_history[1].t, (1, 30, 1))

        for _ in range(5):
            # keep scrolling
            # "duplicate key down"
            keycode_mapper.handle_keycode(new_event(*scroll))

        # nothing new since all of them were duplicate key downs
        self.assertEqual(len(uinput_write_history), 2)

        keycode_mapper.handle_keycode(new_event(*btn_up))
        # "forwarding release"
        self.assertEqual(uinput_write_history[2].t, btn_up)

        # one more scroll event. since the combination is still not released,
        # it should be ignored as duplicate key-down
        self.assertEqual(len(uinput_write_history), 3)
        # "forwarding" (should be "duplicate key down")
        keycode_mapper.handle_keycode(new_event(*scroll))
        self.assertEqual(len(uinput_write_history), 3)

        # the failure to release the mapped key
        # forward=False is what the debouncer uses, because a
        # "scroll release" doesn't actually exist so it is not actually
        # written if it doesn't release any mapping
        keycode_mapper.handle_keycode(new_event(*scroll_up), forward=False)

        # 30 should be released
        self.assertEqual(uinput_write_history[3].t, (1, 30, 0))
        self.assertEqual(len(uinput_write_history), 4)