Files
InputRemapper/tests/unit/test_input_config.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

560 lines
20 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
#
# This file is part of input-remapper.
#
# input-remapper is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# input-remapper is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
import unittest
from evdev.ecodes import (
EV_KEY,
EV_ABS,
EV_REL,
BTN_C,
BTN_B,
BTN_A,
BTN_LEFT,
BTN_MIDDLE,
BTN_RIGHT,
BTN_SIDE,
BTN_EXTRA,
BTN_FORWARD,
BTN_BACK,
BTN_TASK,
REL_X,
REL_Y,
REL_WHEEL,
REL_HWHEEL,
ABS_RY,
ABS_X,
ABS_HAT0Y,
ABS_HAT0X,
KEY_A,
KEY_LEFTSHIFT,
KEY_RIGHTALT,
KEY_LEFTCTRL,
)
from inputremapper.configs.input_config import InputCombination, InputConfig
from tests.lib.test_setup import test_setup
@test_setup
class TestInputConfig(unittest.TestCase):
def test_input_config(self):
test_cases = [
# basic test, nothing fancy here
{
"input": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
},
"properties": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
"input_match_hash": (EV_KEY, KEY_A, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_KEY, KEY_A),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "a",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_KEY, KEY_A, "foo", None)),
},
],
},
# removes analog_threshold
{
"input": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
"analog_threshold": None,
"input_match_hash": (EV_KEY, KEY_A, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_KEY, KEY_A),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "a",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_KEY, KEY_A, "foo", None)),
},
],
},
# abs to btn
{
"input": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": 10,
"input_match_hash": (EV_ABS, ABS_X, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_ABS, ABS_X),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Joystick-X Right 10%",
},
{
"name": "description",
"args": (),
"kwargs": {"exclude_threshold": True},
"return": "Joystick-X Right",
},
{
"name": "description",
"args": (),
"kwargs": {
"exclude_threshold": True,
"exclude_direction": True,
},
"return": "Joystick-X",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_ABS, ABS_X, "foo", 10)),
},
],
},
# abs to btn with d-pad
{
"input": {
"type": EV_ABS,
"code": ABS_HAT0Y,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_ABS,
"code": ABS_HAT0Y,
"origin_hash": "foo",
"analog_threshold": 10,
"input_match_hash": (EV_ABS, ABS_HAT0Y, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_ABS, ABS_HAT0Y),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "DPad-Y Down 10%",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_ABS, ABS_HAT0Y, "foo", 10)),
},
],
},
# rel to btn
{
"input": {
"type": EV_REL,
"code": REL_Y,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_REL,
"code": REL_Y,
"origin_hash": "foo",
"analog_threshold": 10,
"input_match_hash": (EV_REL, REL_Y, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_REL, REL_Y),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Y Down 10",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_REL, REL_Y, "foo", 10)),
},
],
},
# abs as axis
{
"input": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": 0,
},
"properties": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": None,
"input_match_hash": (EV_ABS, ABS_X, "foo"),
"defines_analog_input": True,
"type_and_code": (EV_ABS, ABS_X),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Joystick-X",
},
{
"name": "description",
"args": (),
"kwargs": {
"exclude_threshold": True,
"exclude_direction": True,
},
"return": "Joystick-X",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_ABS, ABS_X, "foo", None)),
},
],
},
# rel as axis
{
"input": {
"type": EV_REL,
"code": REL_WHEEL,
"origin_hash": "foo",
},
"properties": {
"type": EV_REL,
"code": REL_WHEEL,
"origin_hash": "foo",
"analog_threshold": None,
"input_match_hash": (EV_REL, REL_WHEEL, "foo"),
"defines_analog_input": True,
"type_and_code": (EV_REL, REL_WHEEL),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Wheel",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_REL, REL_WHEEL, "foo", None)),
},
],
},
]
for test_case in test_cases:
input_config = InputConfig(**test_case["input"])
for property_, value in test_case["properties"].items():
self.assertEqual(
value,
getattr(input_config, property_),
f"property mismatch for input: {test_case['input']} "
f"property: {property_} expected value: {value}",
)
for method in test_case["methods"]:
self.assertEqual(
method["return"],
getattr(input_config, method["name"])(
*method["args"], **method["kwargs"]
),
f"wrong method return for input: {test_case['input']} "
f"method: {method}",
)
def test_is_immutable(self):
input_config = InputConfig(type=1, code=2)
with self.assertRaises(TypeError):
input_config.origin_hash = "foo"
@test_setup
class TestInputCombination(unittest.TestCase):
def test_eq(self):
a = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
b = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
self.assertEqual(a, b)
def test_not_eq(self):
a = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="2345"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="bcde"),
]
)
b = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
self.assertNotEqual(a, b)
def test_can_be_used_as_dict_key(self):
dict_ = {
InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
): "foo"
}
key = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
self.assertEqual(dict_.get(key), "foo")
def test_get_permutations(self):
key_1 = InputCombination(InputCombination.from_tuples((1, 3, 1)))
self.assertEqual(len(key_1.get_permutations()), 1)
self.assertEqual(key_1.get_permutations()[0], key_1)
key_2 = InputCombination(InputCombination.from_tuples((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 = InputCombination(
InputCombination.from_tuples((1, 3, 1), (1, 5, 1), (1, 7, 1))
)
self.assertEqual(len(key_3.get_permutations()), 2)
self.assertEqual(
key_3.get_permutations()[0],
InputCombination(
InputCombination.from_tuples((1, 3, 1), (1, 5, 1), (1, 7, 1))
),
)
self.assertEqual(
key_3.get_permutations()[1],
InputCombination(
InputCombination.from_tuples((1, 5, 1), (1, 3, 1), (1, 7, 1))
),
)
def test_is_problematic(self):
key_1 = InputCombination(
InputCombination.from_tuples((1, KEY_LEFTSHIFT, 1), (1, 5, 1))
)
self.assertTrue(key_1.is_problematic())
key_2 = InputCombination(
InputCombination.from_tuples((1, KEY_RIGHTALT, 1), (1, 5, 1))
)
self.assertTrue(key_2.is_problematic())
key_3 = InputCombination(
InputCombination.from_tuples((1, 3, 1), (1, KEY_LEFTCTRL, 1))
)
self.assertTrue(key_3.is_problematic())
key_4 = InputCombination(InputCombination.from_tuples((1, 3, 1)))
self.assertFalse(key_4.is_problematic())
key_5 = InputCombination(InputCombination.from_tuples((1, 3, 1), (1, 5, 1)))
self.assertFalse(key_5.is_problematic())
def test_init(self):
self.assertRaises(TypeError, lambda: InputCombination(1))
self.assertRaises(TypeError, lambda: InputCombination(None))
self.assertRaises(TypeError, lambda: InputCombination([1]))
self.assertRaises(TypeError, lambda: InputCombination((1,)))
self.assertRaises(TypeError, lambda: InputCombination((1, 2)))
self.assertRaises(TypeError, lambda: InputCombination("1"))
self.assertRaises(TypeError, lambda: InputCombination("(1,2,3)"))
self.assertRaises(
TypeError,
lambda: InputCombination(((1, 2, 3), (1, 2, 3), None)),
)
# those don't raise errors
InputCombination(({"type": 1, "code": 2}, {"type": 1, "code": 1}))
InputCombination(({"type": 1, "code": 2},))
InputCombination(({"type": "1", "code": "2"},))
InputCombination([InputConfig(type=1, code=2, analog_threshold=3)])
InputCombination(
(
{"type": 1, "code": 2},
{"type": "1", "code": "2"},
InputConfig(type=1, code=2),
)
)
def test_to_config(self):
c1 = InputCombination([InputConfig(type=1, code=2, analog_threshold=3)])
c2 = InputCombination(
(
InputConfig(type=1, code=2, analog_threshold=3),
InputConfig(type=4, code=5, analog_threshold=6),
)
)
# analog_threshold is removed for key events
self.assertEqual(c1.to_config(), ({"type": 1, "code": 2},))
self.assertEqual(
c2.to_config(),
({"type": 1, "code": 2}, {"type": 4, "code": 5, "analog_threshold": 6}),
)
def test_beautify(self):
# not an integration test, but I have all the selection_label tests here already
self.assert_beautify_single(EV_KEY, KEY_A, 1, "a")
self.assert_beautify_single(EV_KEY, KEY_A, 1, "a")
self.assert_beautify_single(EV_ABS, ABS_HAT0Y, -1, "DPad-Y Up")
self.assert_beautify_single(EV_KEY, BTN_A, 1, "Button A")
self.assert_beautify_single(EV_KEY, 1234, 1, "unknown (1, 1234)")
self.assert_beautify_single(EV_ABS, ABS_HAT0X, -1, "DPad-X Left")
self.assert_beautify_single(EV_ABS, ABS_HAT0Y, -1, "DPad-Y Up")
self.assert_beautify_single(EV_KEY, BTN_A, 1, "Button A")
self.assert_beautify_single(EV_ABS, ABS_X, 1, "Joystick-X Right")
self.assert_beautify_single(EV_ABS, ABS_RY, 1, "Joystick-RY Down")
self.assert_beautify_single(EV_REL, REL_HWHEEL, 1, "Wheel Right")
self.assert_beautify_single(EV_REL, REL_WHEEL, -1, "Wheel Down")
# region "mouse buttons"
self.assert_beautify_single(EV_KEY, BTN_LEFT, 1, "Mouse Button LEFT")
self.assert_beautify_single(EV_KEY, BTN_MIDDLE, 1, "Mouse Button MIDDLE")
self.assert_beautify_single(EV_KEY, BTN_RIGHT, 1, "Mouse Button RIGHT")
self.assert_beautify_single(EV_KEY, BTN_SIDE, 1, "Mouse Button 4")
self.assert_beautify_single(EV_KEY, BTN_EXTRA, 1, "Mouse Button 5")
self.assert_beautify_single(EV_KEY, BTN_FORWARD, 1, "Mouse Button 6")
self.assert_beautify_single(EV_KEY, BTN_BACK, 1, "Mouse Button 7")
self.assert_beautify_single(EV_KEY, BTN_TASK, 1, "Mouse Button 8")
# Mouse buttons 9+ do not have an evdev name, and so must be expressed
# as an offset from BTN_LEFT AKA "Button 1". Subtract 1 for base index
# so that `btn_mouse_base + 1` would be "Button 1".
btn_mouse_base: int = BTN_LEFT - 1
self.assert_beautify_single(EV_KEY, btn_mouse_base + 9, 1, "Mouse Button 9")
self.assert_beautify_single(EV_KEY, btn_mouse_base + 10, 1, "Mouse Button 10")
# endregion "mouse buttons"
# combinations
self.assertEqual(
InputCombination(
InputCombination.from_tuples(
(EV_KEY, BTN_A, 1),
(EV_KEY, BTN_B, 1),
(EV_KEY, BTN_C, 1),
),
).beautify(),
"Button A + Button B + Button C",
)
def test_find_analog_input_config(self):
analog_input = InputConfig(type=EV_REL, code=REL_X)
combination = InputCombination(
(
InputConfig(type=EV_KEY, code=BTN_MIDDLE),
InputConfig(type=EV_REL, code=REL_Y, analog_threshold=1),
analog_input,
)
)
self.assertIsNone(combination.find_analog_input_config(type_=EV_ABS))
self.assertEqual(
combination.find_analog_input_config(type_=EV_REL), analog_input
)
self.assertEqual(combination.find_analog_input_config(), analog_input)
combination = InputCombination(
(
InputConfig(type=EV_REL, code=REL_X, analog_threshold=1),
InputConfig(type=EV_KEY, code=BTN_MIDDLE),
)
)
self.assertIsNone(combination.find_analog_input_config(type_=EV_ABS))
self.assertIsNone(combination.find_analog_input_config(type_=EV_REL))
self.assertIsNone(combination.find_analog_input_config())
# region helper methods
def assert_beautify_single(self, type_, code, direction, expected_beautified_name):
"""
Assert that the beautified name of the event tuple `(type_, code,
direction)` matches the `expected_beautified_name`.
"""
self.assertEqual(
InputCombination(
InputCombination.from_tuples((type_, code, direction))
).beautify(),
expected_beautified_name,
)
# endregion
if __name__ == "__main__":
unittest.main()