From e4d824cdd6bd456b607b68b6dac46827afeb8fd6 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Fri, 1 Jan 2021 14:09:28 +0100 Subject: [PATCH] more tests, disabling keys --- data/key-mapper.glade | 26 ------- keymapper/config.py | 2 +- keymapper/dev/injector.py | 4 + keymapper/dev/keycode_mapper.py | 55 ++++++++++--- keymapper/gtk/row.py | 1 - keymapper/gtk/window.py | 16 ++-- keymapper/key.py | 24 +++++- keymapper/mapping.py | 5 ++ keymapper/presets.py | 4 +- keymapper/state.py | 4 +- tests/testcases/test_injector.py | 9 ++- tests/testcases/test_integration.py | 24 +++++- tests/testcases/test_key.py | 104 +++++++++++++++++++++++++ tests/testcases/test_keycode_mapper.py | 88 ++++++++++++++++++++- tests/testcases/test_mapping.py | 3 + 15 files changed, 315 insertions(+), 54 deletions(-) create mode 100644 tests/testcases/test_key.py diff --git a/data/key-mapper.glade b/data/key-mapper.glade index 4646ae54..c1c20b40 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -816,32 +816,6 @@ 2 - - - True - False - - - False - True - 3 - - - - - True - False - This is the keycode you just pressed, and that you see in the mappings to the right. - 10 - 10 - 4 - - - False - True - 4 - - False diff --git a/keymapper/config.py b/keymapper/config.py index 430870b9..94eae4fb 100644 --- a/keymapper/config.py +++ b/keymapper/config.py @@ -177,7 +177,7 @@ class GlobalConfig(ConfigBase): def __init__(self): self.path = os.path.join(CONFIG_PATH, 'config.json') - # migrate from < 0.4.1, add the .json ending + # migrate from < 0.4.0, add the .json ending deprecated_path = os.path.join(CONFIG_PATH, 'config') if os.path.exists(deprecated_path) and not os.path.exists(self.path): logger.info('Moving "%s" to "%s"', deprecated_path, self.path) diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index d2a7c1cb..3f946fd5 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -38,6 +38,7 @@ from keymapper.dev.keycode_mapper import handle_keycode, \ from keymapper.dev.ev_abs_mapper import ev_abs_mapper, JOYSTICK from keymapper.dev.macros import parse, is_this_a_macro from keymapper.state import system_mapping +from keymapper.mapping import DISABLE_CODE DEV_NAME = 'key-mapper' @@ -261,6 +262,9 @@ class KeycodeInjector: # Furthermore, support all injected keycodes for code in self._key_to_code.values(): + if code == DISABLE_CODE: + continue + if code not in capabilities[EV_KEY]: capabilities[EV_KEY].append(code) diff --git a/keymapper/dev/keycode_mapper.py b/keymapper/dev/keycode_mapper.py index f4d2c7d0..931d6f14 100644 --- a/keymapper/dev/keycode_mapper.py +++ b/keymapper/dev/keycode_mapper.py @@ -27,8 +27,9 @@ import asyncio from evdev.ecodes import EV_KEY, EV_ABS -from keymapper.logger import logger +from keymapper.logger import logger, is_debug from keymapper.util import sign +from keymapper.mapping import DISABLE_CODE from keymapper.dev.ev_abs_mapper import JOYSTICK @@ -118,6 +119,25 @@ def subsets(combination): )) +def log(key, msg, *args): + """Function that logs nicely formatted spams.""" + if not is_debug(): + return + + msg = msg % args + str_key = str(key) + str_key = str_key.replace(',)', ')') + + spacing = ' ' + '-' * max(0, 30 - len(str_key)) + if len(spacing) == 1: + spacing = '' + + msg = f'{str_key}{spacing} {msg}' + + logger.spam(msg) + return msg + + def handle_keycode(key_to_code, macros, event, uinput): """Write mapped keycodes, forward unmapped ones and manage macros. @@ -172,6 +192,9 @@ def handle_keycode(key_to_code, macros, event, uinput): else: # no subset found, just use the key. all indices are tuples of tuples, # both for combinations and single keys. + if event.value == 1 and len(combination) > 1: + log(combination, 'unknown combination') + key = (key,) active_macro = active_macros.get(type_code) @@ -183,15 +206,20 @@ def handle_keycode(key_to_code, macros, event, uinput): # Tell the macro for that keycode that the key is released and # let it decide what to do with that information. active_macro.release_key() - logger.spam('%s, releasing macro', key) + log(key, 'releasing macro') if type_code in unreleased: target_type, target_code = unreleased[type_code][0] - logger.spam('%s, releasing %s', key, target_code) del unreleased[type_code] - write(uinput, (target_type, target_code, 0)) + + if target_code == DISABLE_CODE: + log(key, 'releasing disabled key') + else: + log(key, 'releasing %s', target_code) + write(uinput, (target_type, target_code, 0)) else: - logger.spam('%s, unexpected key up', key) + # disabled keys can still be used in combinations btw + log(key, 'unexpected key up') # everything that can be released is released now return @@ -205,7 +233,7 @@ def handle_keycode(key_to_code, macros, event, uinput): # This avoids spawning a second macro while the first one is not # finished, especially since gamepad-triggers report a ton of # events with a positive value. - logger.spam('%s, macro already running', key) + log(key, 'macro already running') return # it would write a key usually @@ -213,7 +241,7 @@ def handle_keycode(key_to_code, macros, event, uinput): # duplicate key-down. skip this event. Avoid writing millions of # key-down events when a continuous value is reported, for example # for gamepad triggers - logger.spam('%s, duplicate key down', key) + log(key, 'duplicate key down') return """starting new macros or injecting new keys""" @@ -223,20 +251,25 @@ def handle_keycode(key_to_code, macros, event, uinput): macro = macros[key] active_macros[type_code] = macro macro.press_key() - logger.spam('%s, maps to macro %s', key, macro.code) + log(key, 'maps to macro %s', macro.code) asyncio.ensure_future(macro.run()) return if key in key_to_code: target_code = key_to_code[key] - logger.spam('%s, maps to %s', key, target_code) unreleased[type_code] = ((EV_KEY, target_code), event_tuple) + + if target_code == DISABLE_CODE: + log(key, 'disabled') + return + + log(key, 'maps to %s', target_code) write(uinput, (EV_KEY, target_code, 1)) return - logger.spam('%s, forwarding', key) + log(key, 'forwarding') unreleased[type_code] = ((event_tuple[:2]), event_tuple) write(uinput, event_tuple) return - logger.error('%s, unhandled. %s %s', key, unreleased, active_macros) + logger.error(key, '%s unhandled. %s %s', unreleased, active_macros) diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index 97e97d68..43dd129e 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -168,7 +168,6 @@ class Row(Gtk.ListBoxRow): return # it's legal to display the keycode - self.window.get('status_bar').remove_all(CTX_KEYCODE) # always ask for get_child to set the label, otherwise line breaking # has to be configured again. diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 394cd57b..4fb8e2f4 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -54,9 +54,6 @@ CTX_ERROR = 3 CTX_WARNING = 4 -# TODO status warning for ctrl in key combis - - def get_selected_row_bg(): """Get the background color that a row is going to have when selected.""" # ListBoxRows can be selected, but either they are always selectable @@ -365,8 +362,13 @@ class Window: if key is None: return True - # only show the latest key, becomes too long otherwise - self.get('keycode').set_text(to_string(key).split('+')[-1].strip()) + if key.is_problematic(): + self.show_status( + CTX_WARNING, + 'ctrl, alt and shift may not combine properly', + 'Your system will probably reinterpret combinations with ' + + 'those after they are injected, and by doing so break them.' + ) # inform the currently selected row about the new keycode row, focused = self.get_focused_row() @@ -397,8 +399,8 @@ class Window: if context_id == CTX_WARNING: self.get('warning_status_icon').show() - if len(message) > 40: - message = message[:37] + '...' + if len(message) > 48: + message = message[:50] + '...' status_bar = self.get('status_bar') status_bar.push(context_id, message) diff --git a/keymapper/key.py b/keymapper/key.py index 7475b897..b9d7ada8 100644 --- a/keymapper/key.py +++ b/keymapper/key.py @@ -24,7 +24,7 @@ import itertools -from keymapper.util import sign +from evdev import ecodes def verify(key): @@ -35,6 +35,14 @@ def verify(key): raise ValueError(f'Can only use numbers, but got {key}') +# having shift in combinations modifies the configured output, +# ctrl might not work at all +DIFFICULT_COMBINATIONS = [ + ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT, + ecodes.KEY_LEFTCTRL, ecodes.KEY_RIGHTCTRL, + ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTALT +] + class Key: """Represents one or more pressed down keys. @@ -112,6 +120,20 @@ class Key: # compare two instances of Key return self.keys == other.keys + def is_problematic(self): + """Is this combination going to work properly on all systems?""" + if len(self.keys) <= 1: + return False + + for sub_key in self.keys: + if sub_key[0] != ecodes.EV_KEY: + continue + + if sub_key[1] in DIFFICULT_COMBINATIONS: + return True + + return False + def get_permutations(self): """Get a list of Key objects representing all possible permutations. diff --git a/keymapper/mapping.py b/keymapper/mapping.py index b01d7f0c..a1fbc52b 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -32,6 +32,11 @@ from keymapper.config import ConfigBase, config from keymapper.key import Key +DISABLE_NAME = 'disable' + +DISABLE_CODE = -1 + + def split_key(key): """Take a key like "1,2,3" and return a 3-tuple of ints.""" key = key.strip() diff --git a/keymapper/presets.py b/keymapper/presets.py index eceb26a5..d716ecce 100644 --- a/keymapper/presets.py +++ b/keymapper/presets.py @@ -32,13 +32,13 @@ from keymapper.getdevices import get_devices def migrate_path(): - """Migrate the folder structure from < 0.4.1. + """Migrate the folder structure from < 0.4.0. Move existing presets into the new subfolder "presets" """ new_preset_folder = os.path.join(CONFIG_PATH, 'presets') if not os.path.exists(get_preset_path()) and os.path.exists(CONFIG_PATH): - logger.info('Migrating presets from < 0.4.1...') + logger.info('Migrating presets from < 0.4.0...') devices = os.listdir(CONFIG_PATH) mkdir(get_preset_path()) for device in devices: diff --git a/keymapper/state.py b/keymapper/state.py index f6f2a3c4..ab4f35f8 100644 --- a/keymapper/state.py +++ b/keymapper/state.py @@ -29,7 +29,7 @@ import subprocess import evdev from keymapper.logger import logger -from keymapper.mapping import Mapping +from keymapper.mapping import Mapping, DISABLE_NAME, DISABLE_CODE from keymapper.paths import get_config_path, touch, USER @@ -95,6 +95,8 @@ class SystemMapping: if name.startswith('KEY') or name.startswith('BTN'): self._set(name, ecode) + self._set(DISABLE_NAME, DISABLE_CODE) + def update(self, mapping): """Update this with new keys. diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index 2be1dd9f..07d508ce 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -29,7 +29,7 @@ from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X from keymapper.dev.injector import is_numlock_on, set_numlock, \ ensure_numlock, KeycodeInjector, is_in_capabilities from keymapper.state import custom_mapping, system_mapping -from keymapper.mapping import Mapping +from keymapper.mapping import Mapping, DISABLE_CODE, DISABLE_NAME from keymapper.config import config from keymapper.key import Key from keymapper.dev.macros import parse @@ -78,6 +78,7 @@ class TestInjector(unittest.TestCase): mapping = Mapping() mapping.change(Key(EV_KEY, 80, 1), 'a') + mapping.change(Key(EV_KEY, 81, 1), DISABLE_NAME) macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))' macro = parse(macro_code, mapping) @@ -92,6 +93,7 @@ class TestInjector(unittest.TestCase): shift_l = system_mapping.get('ShIfT_L') one = system_mapping.get(1) two = system_mapping.get('2') + btn_left = system_mapping.get('BtN_lEfT') self.injector = KeycodeInjector('foo', mapping) fake_device = FakeDevice() @@ -107,12 +109,16 @@ class TestInjector(unittest.TestCase): self.assertIn(one, keys) self.assertIn(two, keys) self.assertIn(shift_l, keys) + self.assertNotIn(DISABLE_CODE, keys) + # abs_to_rel is false, so mouse capabilities are not needed + self.assertNotIn(btn_left, keys) self.assertNotIn(evdev.ecodes.EV_SYN, capabilities_1) self.assertNotIn(evdev.ecodes.EV_FF, capabilities_1) self.assertNotIn(evdev.ecodes.EV_REL, capabilities_1) self.assertNotIn(evdev.ecodes.EV_ABS, capabilities_1) + # abs_to_rel makes sure that BTN_LEFT is present capabilities_2 = self.injector._modify_capabilities( {60: macro}, fake_device, @@ -123,6 +129,7 @@ class TestInjector(unittest.TestCase): self.assertIn(one, keys) self.assertIn(two, keys) self.assertIn(shift_l, keys) + self.assertIn(btn_left, keys) def test_grab(self): # path is from the fixtures diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index e2727bda..b910e4e5 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -25,7 +25,7 @@ import grp import os import unittest import evdev -from evdev.ecodes import EV_KEY, EV_ABS, BTN_LEFT, BTN_TOOL_DOUBLETAP +from evdev.ecodes import EV_KEY, EV_ABS, KEY_LEFTSHIFT import json from unittest.mock import patch from importlib.util import spec_from_loader, module_from_spec @@ -524,6 +524,12 @@ class TestIntegration(unittest.TestCase): self.assertEqual(custom_mapping.get_character(combination_5), 'e') self.assertEqual(custom_mapping.get_character(combination_6), 'e') + error_icon = self.window.get('error_status_icon') + warning_icon = self.window.get('warning_status_icon') + + self.assertFalse(error_icon.get_visible()) + self.assertFalse(warning_icon.get_visible()) + def test_remove_row(self): """Comprehensive test for rows 2.""" # sleeps are added to be able to visually follow and debug the test. @@ -581,6 +587,19 @@ class TestIntegration(unittest.TestCase): # of rows won't change. remove(row_3, None, 'c', 1) + def test_problematic_combination(self): + combination = Key((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)) + self.change_empty_row(combination, 'b') + status = self.window.get('status_bar') + text = status.get_message_area().get_children()[0].get_label() + self.assertIn('shift', text) + + error_icon = self.window.get('error_status_icon') + warning_icon = self.window.get('warning_status_icon') + + self.assertFalse(error_icon.get_visible()) + self.assertTrue(warning_icon.get_visible()) + def test_rename_and_save(self): custom_mapping.change(Key(EV_KEY, 14, 1), 'a', None) self.assertEqual(self.window.selected_preset, 'new preset') @@ -603,12 +622,14 @@ class TestIntegration(unittest.TestCase): def test_check_macro_syntax(self): status = self.window.get('status_bar') error_icon = self.window.get('error_status_icon') + warning_icon = self.window.get('warning_status_icon') custom_mapping.change(Key(EV_KEY, 9, 1), 'k(1))', None) self.window.on_save_preset_clicked(None) tooltip = status.get_tooltip_text().lower() self.assertIn('brackets', tooltip) self.assertTrue(error_icon.get_visible()) + self.assertFalse(warning_icon.get_visible()) custom_mapping.change(Key(EV_KEY, 9, 1), 'k(1)', None) self.window.on_save_preset_clicked(None) @@ -616,6 +637,7 @@ class TestIntegration(unittest.TestCase): self.assertNotIn('brackets', tooltip) self.assertIn('saved', tooltip) self.assertFalse(error_icon.get_visible()) + self.assertFalse(warning_icon.get_visible()) self.assertEqual(custom_mapping.get_character(Key(EV_KEY, 9, 1)), 'k(1)') diff --git a/tests/testcases/test_key.py b/tests/testcases/test_key.py new file mode 100644 index 00000000..f540609b --- /dev/null +++ b/tests/testcases/test_key.py @@ -0,0 +1,104 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper 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. +# +# key-mapper 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 key-mapper. If not, see . + + +import unittest + +from evdev.ecodes import KEY_LEFTSHIFT, KEY_RIGHTALT, KEY_LEFTCTRL + +from keymapper.key import Key + + +class TestKey(unittest.TestCase): + def test_key(self): + # its very similar to regular tuples, but with some extra stuff + key_1 = Key((1, 3, 1), (1, 5, 1)) + self.assertEqual(str(key_1), 'Key((1, 3, 1), (1, 5, 1))') + self.assertEqual(len(key_1), 2) + self.assertEqual(key_1[0], (1, 3, 1)) + self.assertEqual(key_1[1], (1, 5, 1)) + + key_2 = Key((1, 3, 1)) + self.assertEqual(str(key_2), 'Key((1, 3, 1),)') + self.assertEqual(len(key_2), 1) + self.assertNotEqual(key_2, key_1) + self.assertNotEqual(hash(key_2), hash(key_1)) + + key_3 = Key(1, 3, 1) + self.assertEqual(str(key_3), 'Key((1, 3, 1),)') + self.assertEqual(len(key_3), 1) + self.assertEqual(key_3, key_2) + self.assertEqual(key_3, (1, 3, 1)) + self.assertEqual(hash(key_3), hash(key_2)) + self.assertEqual(hash(key_3), hash((1, 3, 1))) + + key_4 = Key(key_3) + self.assertEqual(str(key_4), 'Key((1, 3, 1),)') + self.assertEqual(len(key_4), 1) + self.assertEqual(key_4, key_3) + self.assertEqual(hash(key_4), hash(key_3)) + + key_5 = Key(key_4, key_4, (1, 7, 1)) + self.assertEqual(str(key_5), 'Key((1, 3, 1), (1, 3, 1), (1, 7, 1))') + self.assertEqual(len(key_5), 3) + self.assertNotEqual(key_5, key_4) + self.assertNotEqual(hash(key_5), hash(key_4)) + self.assertEqual(key_5, ((1, 3, 1), (1, 3, 1), (1, 7, 1))) + self.assertEqual(hash(key_5), hash(((1, 3, 1), (1, 3, 1), (1, 7, 1)))) + + def test_get_permutations(self): + key_1 = Key((1, 3, 1)) + self.assertEqual(len(key_1.get_permutations()), 1) + self.assertEqual(key_1.get_permutations()[0], key_1) + + key_2 = Key((1, 3, 1), (1, 5, 1)) + self.assertEqual(len(key_2.get_permutations()), 1) + self.assertEqual(key_2.get_permutations()[0], key_2) + + key_3 = Key((1, 3, 1), (1, 5, 1), (1, 7, 1)) + self.assertEqual(len(key_3.get_permutations()), 2) + self.assertEqual( + key_3.get_permutations()[0], + Key((1, 3, 1), (1, 5, 1), (1, 7, 1)) + ) + self.assertEqual( + key_3.get_permutations()[1], + ((1, 5, 1), (1, 3, 1), (1, 7, 1)) + ) + + def test_is_problematic(self): + key_1 = Key((1, KEY_LEFTSHIFT, 1), (1, 5, 1)) + self.assertTrue(key_1.is_problematic()) + + key_2 = Key((1, KEY_RIGHTALT, 1), (1, 5, 1)) + self.assertTrue(key_2.is_problematic()) + + key_3 = Key((1, 3, 1), (1, KEY_LEFTCTRL, 1)) + self.assertTrue(key_3.is_problematic()) + + key_4 = Key(1, 3, 1) + self.assertFalse(key_4.is_problematic()) + + key_5 = Key((1, 3, 1), (1, 5, 1)) + self.assertFalse(key_5.is_problematic()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testcases/test_keycode_mapper.py b/tests/testcases/test_keycode_mapper.py index 97130f4e..0aee8245 100644 --- a/tests/testcases/test_keycode_mapper.py +++ b/tests/testcases/test_keycode_mapper.py @@ -28,11 +28,11 @@ from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_A, ABS_X, \ EV_REL, REL_X, BTN_TL from keymapper.dev.keycode_mapper import should_map_event_as_btn, \ - active_macros, handle_keycode, unreleased, subsets + active_macros, handle_keycode, unreleased, subsets, log from keymapper.state import system_mapping from keymapper.dev.macros import parse from keymapper.config import config -from keymapper.mapping import Mapping +from keymapper.mapping import Mapping, DISABLE_CODE from tests.test import InputEvent, UInput, uinput_write_history, \ cleanup @@ -822,6 +822,90 @@ class TestKeycodeMapper(unittest.TestCase): self.assertEqual(uinput_write_history[0].t, (EV_KEY, 21, 1)) self.assertEqual(uinput_write_history[1].t, (EV_KEY, 21, 0)) + 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) + 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() + + """single keys""" + + # down + handle_keycode(_key_to_code, {}, InputEvent(*ev_1), uinput) + handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput) + # up + handle_keycode(_key_to_code, {}, InputEvent(*ev_2), uinput) + handle_keycode(_key_to_code, {}, InputEvent(*ev_4), uinput) + + 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 + handle_keycode(_key_to_code, {}, InputEvent(*combi_1[0]), uinput) + handle_keycode(_key_to_code, {}, InputEvent(*combi_1[1]), uinput) + 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)) + + # release the last key of the combi first, it should + # release what the combination maps to + event = InputEvent(combi_1[1][0], combi_1[1][1], 0) + handle_keycode(_key_to_code, {}, event, uinput) + self.assertEqual(len(uinput_write_history), 5) + self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 62, 0)) + + event = InputEvent(combi_1[0][0], combi_1[0][1], 0) + handle_keycode(_key_to_code, {}, event, uinput) + self.assertEqual(len(uinput_write_history), 6) + self.assertEqual(uinput_write_history[-1].t, (EV_KEY, KEY_A, 0)) + + """a combination that starts with a disabled key""" + + # only the combination should get triggered + handle_keycode(_key_to_code, {}, InputEvent(*combi_2[0]), uinput) + handle_keycode(_key_to_code, {}, InputEvent(*combi_2[1]), uinput) + 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 = InputEvent(combi_2[1][0], combi_2[1][1], 0) + handle_keycode(_key_to_code, {}, event, uinput) + self.assertEqual(len(uinput_write_history), 8) + self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 63, 0)) + + # the first key of combi_2 is disabled, so it won't write another + # key-up event + event = InputEvent(combi_2[0][0], combi_2[0][1], 0) + handle_keycode(_key_to_code, {}, event, uinput) + self.assertEqual(len(uinput_write_history), 8) + + def test_log(self): + msg1 = log(((1, 2, 1),), 'foo %s bar', 1234) + self.assertEqual(msg1, '((1, 2, 1)) ------------------- foo 1234 bar') + + msg2 = log(((1, 200, -1), (1, 5, 1)), 'foo %s', (1, 2)) + self.assertEqual(msg2, '((1, 200, -1), (1, 5, 1)) ----- foo (1, 2)') + if __name__ == "__main__": unittest.main() diff --git a/tests/testcases/test_mapping.py b/tests/testcases/test_mapping.py index f3670f0a..5fa3764a 100644 --- a/tests/testcases/test_mapping.py +++ b/tests/testcases/test_mapping.py @@ -63,6 +63,7 @@ class TestSystemMapping(unittest.TestCase): # only xmodmap stuff should be present self.assertNotIn('key_a', content) self.assertNotIn('KEY_A', content) + self.assertNotIn('disable', content) def test_system_mapping(self): system_mapping = SystemMapping() @@ -102,6 +103,8 @@ class TestSystemMapping(unittest.TestCase): self.assertIn('btn_left', names) self.assertIn('btn_right', names) + self.assertEqual(system_mapping.get('disable'), -1) + class TestMapping(unittest.TestCase): def setUp(self):