mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-12 01:10:38 +00:00
471 lines
16 KiB
Python
471 lines
16 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
# input-remapper - GUI for device specific keyboard mappings
|
|
# Copyright (C) 2023 sezanzeb <proxima@sezanzeb.de>
|
|
#
|
|
# 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 functools import partial
|
|
|
|
from evdev.ecodes import (
|
|
EV_REL,
|
|
REL_X,
|
|
EV_KEY,
|
|
REL_Y,
|
|
REL_WHEEL,
|
|
REL_WHEEL_HI_RES,
|
|
KEY_1,
|
|
)
|
|
from pydantic import ValidationError
|
|
|
|
from inputremapper.configs.mapping import Mapping, UIMapping
|
|
from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME
|
|
from inputremapper.configs.input_config import InputCombination, InputConfig
|
|
from inputremapper.gui.messages.message_broker import MessageType
|
|
|
|
|
|
class TestMapping(unittest.IsolatedAsyncioTestCase):
|
|
def test_init(self):
|
|
"""Test init and that defaults are set."""
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 2}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "a",
|
|
}
|
|
m = Mapping(**cfg)
|
|
self.assertEqual(
|
|
m.input_combination, InputCombination([InputConfig(type=1, code=2)])
|
|
)
|
|
self.assertEqual(m.target_uinput, "keyboard")
|
|
self.assertEqual(m.output_symbol, "a")
|
|
|
|
self.assertIsNone(m.output_code)
|
|
self.assertIsNone(m.output_type)
|
|
|
|
self.assertEqual(m.macro_key_sleep_ms, 0)
|
|
self.assertEqual(m.deadzone, 0.1)
|
|
self.assertEqual(m.gain, 1)
|
|
self.assertEqual(m.expo, 0)
|
|
self.assertEqual(m.rel_rate, 60)
|
|
self.assertEqual(m.rel_to_abs_input_cutoff, 2)
|
|
self.assertEqual(m.release_timeout, 0.05)
|
|
|
|
def test_is_wheel_output(self):
|
|
mapping = Mapping(
|
|
input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]),
|
|
target_uinput="keyboard",
|
|
output_type=EV_REL,
|
|
output_code=REL_Y,
|
|
)
|
|
self.assertFalse(mapping.is_wheel_output())
|
|
self.assertFalse(mapping.is_high_res_wheel_output())
|
|
|
|
mapping = Mapping(
|
|
input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]),
|
|
target_uinput="keyboard",
|
|
output_type=EV_REL,
|
|
output_code=REL_WHEEL,
|
|
)
|
|
self.assertTrue(mapping.is_wheel_output())
|
|
self.assertFalse(mapping.is_high_res_wheel_output())
|
|
|
|
mapping = Mapping(
|
|
input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]),
|
|
target_uinput="keyboard",
|
|
output_type=EV_REL,
|
|
output_code=REL_WHEEL_HI_RES,
|
|
)
|
|
self.assertFalse(mapping.is_wheel_output())
|
|
self.assertTrue(mapping.is_high_res_wheel_output())
|
|
|
|
def test_get_output_type_code(self):
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 2}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "a",
|
|
}
|
|
m = Mapping(**cfg)
|
|
a = system_mapping.get("a")
|
|
self.assertEqual(m.get_output_type_code(), (EV_KEY, a))
|
|
|
|
m.output_symbol = "key(a)"
|
|
self.assertIsNone(m.get_output_type_code())
|
|
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 2}, {"type": 3, "code": 1}],
|
|
"target_uinput": "keyboard",
|
|
"output_type": 2,
|
|
"output_code": 3,
|
|
}
|
|
m = Mapping(**cfg)
|
|
self.assertEqual(m.get_output_type_code(), (2, 3))
|
|
|
|
def test_strips_output_symbol(self):
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 2}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "\t a \n",
|
|
}
|
|
m = Mapping(**cfg)
|
|
a = system_mapping.get("a")
|
|
self.assertEqual(m.get_output_type_code(), (EV_KEY, a))
|
|
|
|
def test_combination_changed_callback(self):
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 1}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "a",
|
|
}
|
|
m = Mapping(**cfg)
|
|
arguments = []
|
|
|
|
def callback(*args):
|
|
arguments.append(tuple(args))
|
|
|
|
m.set_combination_changed_callback(callback)
|
|
m.input_combination = [{"type": 1, "code": 2}]
|
|
m.input_combination = [{"type": 1, "code": 3}]
|
|
|
|
# make sure a copy works as expected and keeps the callback
|
|
m2 = m.copy()
|
|
m2.input_combination = [{"type": 1, "code": 4}]
|
|
m2.remove_combination_changed_callback()
|
|
m.remove_combination_changed_callback()
|
|
m.input_combination = [{"type": 1, "code": 5}]
|
|
m2.input_combination = [{"type": 1, "code": 6}]
|
|
self.assertEqual(
|
|
arguments,
|
|
[
|
|
(
|
|
InputCombination([{"type": 1, "code": 2}]),
|
|
InputCombination([{"type": 1, "code": 1}]),
|
|
),
|
|
(
|
|
InputCombination([{"type": 1, "code": 3}]),
|
|
InputCombination([{"type": 1, "code": 2}]),
|
|
),
|
|
(
|
|
InputCombination([{"type": 1, "code": 4}]),
|
|
InputCombination([{"type": 1, "code": 3}]),
|
|
),
|
|
],
|
|
)
|
|
m.remove_combination_changed_callback()
|
|
|
|
def test_init_fails(self):
|
|
"""Test that the init fails with invalid data."""
|
|
test = partial(self.assertRaises, ValidationError, Mapping)
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 2}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "a",
|
|
}
|
|
Mapping(**cfg)
|
|
|
|
# missing output symbol
|
|
del cfg["output_symbol"]
|
|
test(**cfg)
|
|
cfg["output_code"] = 1
|
|
test(**cfg)
|
|
cfg["output_type"] = 1
|
|
Mapping(**cfg)
|
|
|
|
# matching type, code and symbol
|
|
a = system_mapping.get("a")
|
|
cfg["output_code"] = a
|
|
cfg["output_symbol"] = "a"
|
|
cfg["output_type"] = EV_KEY
|
|
Mapping(**cfg)
|
|
|
|
# macro + type and code
|
|
cfg["output_symbol"] = "key(a)"
|
|
test(**cfg)
|
|
cfg["output_symbol"] = "a"
|
|
Mapping(**cfg)
|
|
|
|
# mismatching type, code and symbol
|
|
cfg["output_symbol"] = "b"
|
|
test(**cfg)
|
|
del cfg["output_type"]
|
|
del cfg["output_code"]
|
|
Mapping(**cfg) # no error
|
|
|
|
# empty symbol string without type and code
|
|
cfg["output_symbol"] = ""
|
|
test(**cfg)
|
|
cfg["output_symbol"] = "a"
|
|
|
|
# missing target
|
|
del cfg["target_uinput"]
|
|
test(**cfg)
|
|
|
|
# unknown target
|
|
cfg["target_uinput"] = "foo"
|
|
test(**cfg)
|
|
cfg["target_uinput"] = "keyboard"
|
|
Mapping(**cfg)
|
|
|
|
# missing input_combination
|
|
del cfg["input_combination"]
|
|
test(**cfg)
|
|
cfg["input_combination"] = [{"type": 1, "code": 2}]
|
|
Mapping(**cfg)
|
|
|
|
# no macro and not a known symbol
|
|
cfg["output_symbol"] = "qux"
|
|
test(**cfg)
|
|
cfg["output_symbol"] = "key(a)"
|
|
Mapping(**cfg)
|
|
|
|
# invalid macro
|
|
cfg["output_symbol"] = "key('a')"
|
|
test(**cfg)
|
|
cfg["output_symbol"] = "a"
|
|
Mapping(**cfg)
|
|
|
|
# map axis but no output type and code given
|
|
cfg["input_combination"] = [{"type": 3, "code": 0}]
|
|
test(**cfg)
|
|
# output symbol=disable is allowed
|
|
cfg["output_symbol"] = DISABLE_NAME
|
|
Mapping(**cfg)
|
|
del cfg["output_symbol"]
|
|
cfg["output_code"] = 1
|
|
cfg["output_type"] = 3
|
|
Mapping(**cfg)
|
|
|
|
# empty symbol string is allowed when type and code is set
|
|
cfg["output_symbol"] = ""
|
|
Mapping(**cfg)
|
|
del cfg["output_symbol"]
|
|
|
|
# multiple axis as axis in event combination
|
|
cfg["input_combination"] = [{"type": 3, "code": 0}, {"type": 3, "code": 1}]
|
|
test(**cfg)
|
|
cfg["input_combination"] = [{"type": 3, "code": 0}]
|
|
Mapping(**cfg)
|
|
|
|
del cfg["output_type"]
|
|
del cfg["output_code"]
|
|
cfg["input_combination"] = [{"type": 1, "code": 2}]
|
|
cfg["output_symbol"] = "a"
|
|
Mapping(**cfg)
|
|
|
|
# map EV_ABS as key with trigger point out of range
|
|
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": 100}]
|
|
test(**cfg)
|
|
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": 99}]
|
|
Mapping(**cfg)
|
|
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": -100}]
|
|
test(**cfg)
|
|
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": -99}]
|
|
Mapping(**cfg)
|
|
|
|
cfg["input_combination"] = [{"type": 1, "code": 2}]
|
|
Mapping(**cfg)
|
|
|
|
# deadzone out of range
|
|
test(**cfg, deadzone=1.01)
|
|
test(**cfg, deadzone=-0.01)
|
|
Mapping(**cfg, deadzone=1)
|
|
Mapping(**cfg, deadzone=0)
|
|
|
|
# expo out of range
|
|
test(**cfg, expo=1.01)
|
|
test(**cfg, expo=-1.01)
|
|
Mapping(**cfg, expo=1)
|
|
Mapping(**cfg, expo=-1)
|
|
|
|
# negative rate
|
|
test(**cfg, rel_rate=-1)
|
|
test(**cfg, rel_rate=0)
|
|
|
|
Mapping(**cfg, rel_rate=1)
|
|
Mapping(**cfg, rel_rate=200)
|
|
|
|
# negative rel_to_abs_input_cutoff
|
|
test(**cfg, rel_to_abs_input_cutoff=-1)
|
|
test(**cfg, rel_to_abs_input_cutoff=0)
|
|
Mapping(**cfg, rel_to_abs_input_cutoff=1)
|
|
Mapping(**cfg, rel_to_abs_input_cutoff=3)
|
|
|
|
# negative release timeout
|
|
test(**cfg, release_timeout=-0.1)
|
|
test(**cfg, release_timeout=0)
|
|
Mapping(**cfg, release_timeout=0.05)
|
|
Mapping(**cfg, release_timeout=0.3)
|
|
|
|
# analog output but no analog input
|
|
cfg = {
|
|
"input_combination": [{"type": 3, "code": 1, "analog_threshold": -1}],
|
|
"target_uinput": "gamepad",
|
|
"output_type": 3,
|
|
"output_code": 1,
|
|
}
|
|
test(**cfg)
|
|
cfg["input_combination"] = [{"type": 2, "code": 1, "analog_threshold": -1}]
|
|
test(**cfg)
|
|
cfg["output_type"] = 2
|
|
test(**cfg)
|
|
cfg["input_combination"] = [{"type": 3, "code": 1, "analog_threshold": -1}]
|
|
test(**cfg)
|
|
|
|
def test_revalidate_at_assignment(self):
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 1}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "a",
|
|
}
|
|
m = Mapping(**cfg)
|
|
test = partial(self.assertRaises, ValidationError, m.__setattr__)
|
|
|
|
# invalid input event
|
|
test("input_combination", "1,2,3,4")
|
|
|
|
# unknown target
|
|
test("target_uinput", "foo")
|
|
|
|
# invalid macro
|
|
test("output_symbol", "key()")
|
|
|
|
# we could do a lot more tests here but since pydantic uses the same validation
|
|
# code as for the initialization we only need to make sure that the
|
|
# assignment validation is active
|
|
|
|
def test_set_invalid_combination_with_callback(self):
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 1}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "a",
|
|
}
|
|
m = Mapping(**cfg)
|
|
m.set_combination_changed_callback(lambda *args: None)
|
|
self.assertRaises(ValidationError, m.__setattr__, "input_combination", "1,2")
|
|
m.input_combination = [{"type": 1, "code": 2}]
|
|
m.input_combination = [{"type": 1, "code": 2}]
|
|
|
|
def test_is_valid(self):
|
|
cfg = {
|
|
"input_combination": [{"type": 1, "code": 1}],
|
|
"target_uinput": "keyboard",
|
|
"output_symbol": "a",
|
|
}
|
|
m = Mapping(**cfg)
|
|
self.assertTrue(m.is_valid())
|
|
|
|
def test_wrong_target(self):
|
|
mapping = Mapping(
|
|
input_combination=[{"type": EV_KEY, "code": KEY_1}],
|
|
target_uinput="keyboard",
|
|
output_symbol="a",
|
|
)
|
|
mapping.set_combination_changed_callback(lambda *args: None)
|
|
self.assertRaisesRegex(
|
|
ValidationError,
|
|
# the error should mention
|
|
# - the symbol
|
|
# - the current incorrect target
|
|
# - the target that works for this symbol
|
|
".*BTN_A.*keyboard.*gamepad",
|
|
mapping.__setattr__,
|
|
"output_symbol",
|
|
"BTN_A",
|
|
)
|
|
|
|
def test_wrong_target_for_macro(self):
|
|
mapping = Mapping(
|
|
input_combination=[{"type": EV_KEY, "code": KEY_1}],
|
|
target_uinput="keyboard",
|
|
output_symbol="key(a)",
|
|
)
|
|
mapping.set_combination_changed_callback(lambda *args: None)
|
|
self.assertRaisesRegex(
|
|
ValidationError,
|
|
# the error should mention
|
|
# - the symbol
|
|
# - the current incorrect target
|
|
# - the target that works for this symbol
|
|
".*BTN_A.*keyboard.*gamepad",
|
|
mapping.__setattr__,
|
|
"output_symbol",
|
|
"key(BTN_A)",
|
|
)
|
|
|
|
|
|
class TestUIMapping(unittest.IsolatedAsyncioTestCase):
|
|
def test_init(self):
|
|
"""Should be able to initialize without throwing errors."""
|
|
UIMapping()
|
|
|
|
def test_is_valid(self):
|
|
"""Should be invalid at first and become valid once all data is provided."""
|
|
mapping = UIMapping()
|
|
self.assertFalse(mapping.is_valid())
|
|
|
|
mapping.input_combination = [{"type": EV_KEY, "code": KEY_1}]
|
|
mapping.output_symbol = "a"
|
|
self.assertFalse(mapping.is_valid())
|
|
mapping.target_uinput = "keyboard"
|
|
self.assertTrue(mapping.is_valid())
|
|
|
|
def test_updates_validation_error(self):
|
|
mapping = UIMapping()
|
|
self.assertGreaterEqual(len(mapping.get_error().errors()), 2)
|
|
mapping.input_combination = [{"type": EV_KEY, "code": KEY_1}]
|
|
mapping.output_symbol = "a"
|
|
self.assertIn(
|
|
"1 validation error for Mapping\ntarget_uinput",
|
|
str(mapping.get_error()),
|
|
)
|
|
mapping.target_uinput = "keyboard"
|
|
self.assertTrue(mapping.is_valid())
|
|
self.assertIsNone(mapping.get_error())
|
|
|
|
def test_copy_returns_ui_mapping(self):
|
|
"""Copy should also be a UIMapping with all the invalid data."""
|
|
mapping = UIMapping()
|
|
mapping_2 = mapping.copy()
|
|
self.assertIsInstance(mapping_2, UIMapping)
|
|
self.assertEqual(
|
|
mapping_2.input_combination, InputCombination.empty_combination()
|
|
)
|
|
self.assertIsNone(mapping_2.output_symbol)
|
|
|
|
def test_get_bus_massage(self):
|
|
mapping = UIMapping()
|
|
mapping_2 = mapping.get_bus_message()
|
|
self.assertEqual(mapping_2.message_type, MessageType.mapping)
|
|
|
|
with self.assertRaises(TypeError):
|
|
# the massage should be immutable
|
|
mapping_2.output_symbol = "a"
|
|
self.assertIsNone(mapping_2.output_symbol)
|
|
|
|
# the original should be not immutable
|
|
mapping.output_symbol = "a"
|
|
self.assertEqual(mapping.output_symbol, "a")
|
|
|
|
def test_has_input_defined(self):
|
|
mapping = UIMapping()
|
|
self.assertFalse(mapping.has_input_defined())
|
|
mapping.input_combination = InputCombination([InputConfig(type=EV_KEY, code=1)])
|
|
self.assertTrue(mapping.has_input_defined())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|