Files
InputRemapper/tests/unit/test_data_manager.py
Blomios 9ef2c1439c feat(app-binding): lier les presets aux applications
Ajoute la configuration des associations application/preset, le service de suivi du focus, l'intégration GUI et les fichiers d'installation nécessaires.

Ajoute les tests unitaires ciblés des bindings, du service de focus, des migrations et des composants GUI associés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 23:52:34 +02:00

1137 lines
44 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 os
import time
import unittest
from itertools import permutations
from typing import List
from unittest.mock import MagicMock, call
from inputremapper.configs.global_config import GlobalConfig
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import UIMapping, MappingData
from inputremapper.configs.paths import PathUtils
from inputremapper.configs.preset import Preset
from inputremapper.configs.keyboard_layout import keyboard_layout
from inputremapper.exceptions import DataManagementError
from inputremapper.groups import _Groups
from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
from inputremapper.gui.messages.message_broker import (
MessageBroker,
MessageType,
)
from inputremapper.gui.messages.message_data import (
GroupData,
CombinationUpdate,
)
from inputremapper.gui.reader_client import ReaderClient
from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput
from tests.lib.fixtures import prepare_presets
from tests.lib.patches import FakeDaemonProxy
from tests.lib.test_setup import test_setup
class Listener:
def __init__(self):
self.calls: List = []
def __call__(self, data):
self.calls.append(data)
@test_setup
class TestDataManager(unittest.TestCase):
def setUp(self) -> None:
self.message_broker = MessageBroker()
self.reader = ReaderClient(self.message_broker, _Groups())
self.uinputs = GlobalUInputs(FrontendUInput)
self.uinputs.prepare_all()
self.global_config = GlobalConfig()
self.data_manager = DataManager(
self.message_broker,
self.global_config,
self.reader,
FakeDaemonProxy(),
self.uinputs,
keyboard_layout,
)
def test_load_group_provides_presets(self):
"""we should get all preset of a group, when loading it"""
prepare_presets()
response: List[GroupData] = []
def listener(data: GroupData):
response.append(data)
self.message_broker.subscribe(MessageType.group, listener)
self.data_manager.load_group("Foo Device 2")
for preset_name in response[0].presets:
self.assertIn(
preset_name,
(
"preset1",
"preset2",
"preset3",
),
)
self.assertEqual(response[0].group_key, "Foo Device 2")
def test_load_group_without_presets_provides_none(self):
"""We should get no presets when loading a group without presets."""
response: List[GroupData] = []
def listener(data: GroupData):
response.append(data)
self.message_broker.subscribe(MessageType.group, listener)
self.data_manager.load_group(group_key="Foo Device 2")
self.assertEqual(len(response[0].presets), 0)
def test_load_non_existing_group(self):
"""we should not be able to load an unknown group"""
with self.assertRaises(DataManagementError):
self.data_manager.load_group(group_key="Some Unknown Device")
def test_cannot_load_preset_without_group(self):
"""Loading a preset without a loaded group raises a DataManagementError."""
prepare_presets()
self.assertRaises(
DataManagementError,
self.data_manager.load_preset,
name="preset1",
)
def test_load_preset(self):
"""loading an existing preset should be possible"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device")
listener = Listener()
self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.load_preset(name="preset1")
mappings = listener.calls[0].mappings
preset_name = listener.calls[0].name
expected_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset1"))
expected_preset.load()
expected_mappings = list(expected_preset)
self.assertEqual(preset_name, "preset1")
for mapping in expected_mappings:
self.assertIn(mapping, mappings)
def test_cannot_load_non_existing_preset(self):
"""Loading a non-existing preset should raise a KeyError."""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device")
self.assertRaises(
FileNotFoundError,
self.data_manager.load_preset,
name="unknownPreset",
)
def test_save_preset(self):
"""Modified preses should be saved to the disc."""
prepare_presets()
# make sure the correct preset is loaded
self.data_manager.load_group(group_key="Foo Device")
self.data_manager.load_preset(name="preset1")
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=1)])
)
mapping: MappingData = listener.calls[0]
control_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset1"))
control_preset.load()
self.assertEqual(
control_preset.get_mapping(
InputCombination([InputConfig(type=1, code=1)])
).output_symbol,
mapping.output_symbol,
)
# change the mapping provided with the mapping_changed event and save
self.data_manager.update_mapping(output_symbol="key(a)")
self.data_manager.save()
# reload the control_preset
control_preset.empty()
control_preset.load()
self.assertEqual(
control_preset.get_mapping(
InputCombination([InputConfig(type=1, code=1)])
).output_symbol,
"key(a)",
)
def test_copy_preset(self):
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
listener = Listener()
self.message_broker.subscribe(MessageType.group, listener)
self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.copy_preset("foo")
# we expect the first data to be group data and the second
# one a preset data of the new copy
presets_in_group = [preset for preset in listener.calls[0].presets]
self.assertIn("preset2", presets_in_group)
self.assertIn("foo", presets_in_group)
self.assertEqual(listener.calls[1].name, "foo")
# this should pass without error:
self.data_manager.load_preset("preset2")
self.data_manager.copy_preset("preset2")
def test_cannot_copy_preset(self):
prepare_presets()
self.assertRaises(
DataManagementError,
self.data_manager.copy_preset,
"foo",
)
self.data_manager.load_group("Foo Device 2")
self.assertRaises(
DataManagementError,
self.data_manager.copy_preset,
"foo",
)
def test_copy_preset_to_existing_name_raises_error(self):
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.assertRaises(
ValueError,
self.data_manager.copy_preset,
"preset3",
)
def test_rename_preset(self):
"""should be able to rename a preset"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
listener = Listener()
self.message_broker.subscribe(MessageType.group, listener)
self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.rename_preset(new_name="new preset")
# we expect the first data to be group data and the second
# one a preset data
presets_in_group = [preset for preset in listener.calls[0].presets]
self.assertNotIn("preset2", presets_in_group)
self.assertIn("new preset", presets_in_group)
self.assertEqual(listener.calls[1].name, "new preset")
# this should pass without error:
self.data_manager.load_preset(name="new preset")
self.data_manager.rename_preset(new_name="new preset")
def test_rename_preset_sets_autoload_correct(self):
"""when renaming a preset the autoload status should still be set correctly"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
listener = Listener()
self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.load_preset(name="preset2") # sends PresetData
# sends PresetData with updated name, e. e. should be equal
self.data_manager.rename_preset(new_name="foo")
self.assertEqual(listener.calls[0].autoload, listener.calls[1].autoload)
def test_cannot_rename_preset(self):
"""rename preset should raise a DataManagementError if a preset
with the new name already exists in the current group"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.assertRaises(
ValueError,
self.data_manager.rename_preset,
new_name="preset3",
)
def test_cannot_rename_preset_without_preset(self):
prepare_presets()
self.assertRaises(
DataManagementError,
self.data_manager.rename_preset,
new_name="foo",
)
self.data_manager.load_group(group_key="Foo Device 2")
self.assertRaises(
DataManagementError,
self.data_manager.rename_preset,
new_name="foo",
)
def test_add_preset(self):
"""should be able to add a preset"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
listener = Listener()
self.message_broker.subscribe(MessageType.group, listener)
# should emit group_changed
self.data_manager.create_preset(name="new preset")
presets_in_group = [preset for preset in listener.calls[0].presets]
self.assertIn("preset2", presets_in_group)
self.assertIn("preset3", presets_in_group)
self.assertIn("new preset", presets_in_group)
def test_cannot_add_preset(self):
"""adding a preset with the same name as an already existing
preset (of the current group) should raise a DataManagementError"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.assertRaises(
DataManagementError,
self.data_manager.create_preset,
name="preset3",
)
def test_cannot_add_preset_without_group(self):
self.assertRaises(
DataManagementError,
self.data_manager.create_preset,
name="foo",
)
def test_delete_preset(self):
"""should be able to delete the current preset"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
listener = Listener()
self.message_broker.subscribe(MessageType.group, listener)
self.message_broker.subscribe(MessageType.preset, listener)
self.message_broker.subscribe(MessageType.mapping, listener)
# should emit only group_changed
self.data_manager.delete_preset()
presets_in_group = [preset for preset in listener.calls[0].presets]
self.assertEqual(len(presets_in_group), 2)
self.assertNotIn("preset2", presets_in_group)
self.assertEqual(len(listener.calls), 1)
def test_delete_preset_sanitized(self):
"""should be able to delete the current preset"""
Preset(PathUtils.get_preset_path("Qux/[Device]?", "bla")).save()
Preset(PathUtils.get_preset_path("Qux/[Device]?", "foo")).save()
self.assertTrue(
os.path.exists(PathUtils.get_preset_path("Qux/[Device]?", "bla"))
)
self.data_manager.load_group(group_key="Qux/[Device]?")
self.data_manager.load_preset(name="bla")
listener = Listener()
self.message_broker.subscribe(MessageType.group, listener)
self.message_broker.subscribe(MessageType.preset, listener)
self.message_broker.subscribe(MessageType.mapping, listener)
# should emit only group_changed
self.data_manager.delete_preset()
presets_in_group = [preset for preset in listener.calls[0].presets]
self.assertEqual(len(presets_in_group), 1)
self.assertNotIn("bla", presets_in_group)
self.assertIn("foo", presets_in_group)
self.assertEqual(len(listener.calls), 1)
self.assertFalse(
os.path.exists(PathUtils.get_preset_path("Qux/[Device]?", "bla"))
)
def test_load_mapping(self):
"""should be able to load a mapping"""
preset, _, _ = prepare_presets()
expected_mapping = preset.get_mapping(
InputCombination([InputConfig(type=1, code=1)])
)
self.data_manager.load_group(group_key="Foo Device")
self.data_manager.load_preset(name="preset1")
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=1)])
)
mapping = listener.calls[0]
self.assertEqual(mapping, expected_mapping)
def test_cannot_load_non_existing_mapping(self):
"""loading a mapping tha is not present in the preset should raise a KeyError"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.assertRaises(
KeyError,
self.data_manager.load_mapping,
combination=InputCombination([InputConfig(type=1, code=1)]),
)
def test_cannot_load_mapping_without_preset(self):
"""loading a mapping if no preset is loaded
should raise an DataManagementError"""
prepare_presets()
self.assertRaises(
DataManagementError,
self.data_manager.load_mapping,
combination=InputCombination([InputConfig(type=1, code=1)]),
)
self.data_manager.load_group("Foo Device")
self.assertRaises(
DataManagementError,
self.data_manager.load_mapping,
combination=InputCombination([InputConfig(type=1, code=1)]),
)
def test_load_event(self):
prepare_presets()
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock.assert_called_once_with(InputConfig(type=1, code=1))
self.assertEqual(
self.data_manager.active_input_config, InputConfig(type=1, code=1)
)
def test_cannot_load_event_when_mapping_not_set(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
with self.assertRaises(DataManagementError):
self.data_manager.load_input_config(InputConfig(type=1, code=1))
def test_cannot_load_event_when_not_in_mapping_combination(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
with self.assertRaises(ValueError):
self.data_manager.load_input_config(InputConfig(type=1, code=5))
def test_update_event(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
self.data_manager.update_input_config(InputConfig(type=1, code=5))
self.assertEqual(
self.data_manager.active_input_config, InputConfig(type=1, code=5)
)
def test_update_event_sends_messages(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.message_broker.subscribe(MessageType.combination_update, mock)
self.message_broker.subscribe(MessageType.mapping, mock)
self.data_manager.update_input_config(InputConfig(type=1, code=5))
expected = [
call(
CombinationUpdate(
InputCombination([InputConfig(type=1, code=1)]),
InputCombination([InputConfig(type=1, code=5)]),
)
),
call(self.data_manager.active_mapping.get_bus_message()),
call(InputConfig(type=1, code=5)),
]
mock.assert_has_calls(expected, any_order=False)
def test_cannot_update_event_when_resulting_combination_exists(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
with self.assertRaises(KeyError):
self.data_manager.update_input_config(InputConfig(type=1, code=2))
def test_cannot_update_event_when_not_loaded(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
with self.assertRaises(DataManagementError):
self.data_manager.update_input_config(InputConfig(type=1, code=2))
def test_update_mapping_emits_mapping_changed(self):
"""update mapping should emit a mapping_changed event"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=4)])
)
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.update_mapping(
name="foo",
output_symbol="f",
release_timeout=0.3,
)
response = listener.calls[0]
self.assertEqual(response.name, "foo")
self.assertEqual(response.output_symbol, "f")
self.assertEqual(response.release_timeout, 0.3)
def test_updated_mapping_can_be_saved(self):
"""make sure that updated changes can be saved"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=4)])
)
self.data_manager.update_mapping(
name="foo",
output_symbol="f",
release_timeout=0.3,
)
self.data_manager.save()
preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load()
mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)]))
self.assertEqual(mapping.format_name(), "foo")
self.assertEqual(mapping.output_symbol, "f")
self.assertEqual(mapping.release_timeout, 0.3)
def test_updated_mapping_saves_invalid_mapping(self):
"""make sure that updated changes can be saved even if they are not valid"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=4)])
)
self.data_manager.update_mapping(
output_symbol="bar", # not a macro and not a valid symbol
)
self.data_manager.save()
preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load()
mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)]))
self.assertIsNotNone(mapping.get_error())
self.assertEqual(mapping.output_symbol, "bar")
def test_update_mapping_combination_sends_massage(self):
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=4)])
)
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.message_broker.subscribe(MessageType.combination_update, listener)
# we expect a message for combination update first, and then for mapping
self.data_manager.update_mapping(
input_combination=InputCombination(
InputCombination.from_tuples((1, 5), (1, 6))
)
)
self.assertEqual(listener.calls[0].message_type, MessageType.combination_update)
self.assertEqual(
listener.calls[0].old_combination,
InputCombination([InputConfig(type=1, code=4)]),
)
self.assertEqual(
listener.calls[0].new_combination,
InputCombination(InputCombination.from_tuples((1, 5), (1, 6))),
)
self.assertEqual(listener.calls[1].message_type, MessageType.mapping)
self.assertEqual(
listener.calls[1].input_combination,
InputCombination(InputCombination.from_tuples((1, 5), (1, 6))),
)
def test_cannot_update_mapping_combination(self):
"""updating a mapping with an already existing combination
should raise a KeyError"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=4)])
)
self.assertRaises(
KeyError,
self.data_manager.update_mapping,
input_combination=InputCombination([InputConfig(type=1, code=3)]),
)
def test_cannot_update_mapping(self):
"""updating a mapping should not be possible if the mapping was not loaded"""
prepare_presets()
self.assertRaises(
DataManagementError,
self.data_manager.update_mapping,
name="foo",
)
self.data_manager.load_group(group_key="Foo Device 2")
self.assertRaises(
DataManagementError,
self.data_manager.update_mapping,
name="foo",
)
self.data_manager.load_preset("preset2")
self.assertRaises(
DataManagementError,
self.data_manager.update_mapping,
name="foo",
)
def test_create_mapping(self):
"""should be able to add a mapping to the current preset"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.create_mapping() # emits preset_changed
self.data_manager.load_mapping(combination=InputCombination.empty_combination())
self.assertEqual(listener.calls[0].name, "preset2")
self.assertEqual(len(listener.calls[0].mappings), 3)
self.assertEqual(listener.calls[1], UIMapping())
def test_cannot_create_mapping_without_preset(self):
"""adding a mapping if not preset is loaded
should raise an DataManagementError"""
prepare_presets()
self.assertRaises(DataManagementError, self.data_manager.create_mapping)
self.data_manager.load_group(group_key="Foo Device 2")
self.assertRaises(DataManagementError, self.data_manager.create_mapping)
def test_delete_mapping(self):
"""should be able to delete a mapping"""
prepare_presets()
old_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"))
old_preset.load()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination([InputConfig(type=1, code=3)])
)
listener = Listener()
self.message_broker.subscribe(MessageType.preset, listener)
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.delete_mapping() # emits preset
self.data_manager.save()
deleted_mapping = old_preset.get_mapping(
InputCombination([InputConfig(type=1, code=3)])
)
mappings = listener.calls[0].mappings
preset_name = listener.calls[0].name
expected_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"))
expected_preset.load()
expected_mappings = list(expected_preset)
self.assertEqual(preset_name, "preset2")
for mapping in expected_mappings:
self.assertIn(mapping, mappings)
self.assertNotIn(deleted_mapping, mappings)
def test_cannot_delete_mapping(self):
"""deleting a mapping should not be possible if the mapping was not loaded"""
prepare_presets()
self.assertRaises(DataManagementError, self.data_manager.delete_mapping)
self.data_manager.load_group(group_key="Foo Device 2")
self.assertRaises(DataManagementError, self.data_manager.delete_mapping)
self.data_manager.load_preset(name="preset2")
self.assertRaises(DataManagementError, self.data_manager.delete_mapping)
def test_set_autoload(self):
"""should be able to set the autoload status"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device")
listener = Listener()
self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.load_preset(name="preset1") # sends updated preset data
self.data_manager.set_autoload(True) # sends updated preset data
self.data_manager.set_autoload(False) # sends updated preset data
self.assertFalse(listener.calls[0].autoload)
self.assertTrue(listener.calls[1].autoload)
self.assertFalse(listener.calls[2].autoload)
def test_each_device_can_have_autoload(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset1")
self.data_manager.set_autoload(True)
# switch to another device
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.set_autoload(True)
# now check that both are set to autoload
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset1")
self.assertTrue(self.data_manager.get_autoload())
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.assertTrue(self.data_manager.get_autoload())
def test_cannot_set_autoload_without_preset(self):
prepare_presets()
self.assertRaises(
DataManagementError,
self.data_manager.set_autoload,
True,
)
self.data_manager.load_group(group_key="Foo Device 2")
self.assertRaises(
DataManagementError,
self.data_manager.set_autoload,
True,
)
def test_finds_newest_group(self):
Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save()
time.sleep(0.01)
Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save()
self.assertEqual(self.data_manager.get_newest_group_key(), "Bar Device")
def test_finds_newest_preset(self):
Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save()
time.sleep(0.01)
Preset(PathUtils.get_preset_path("Foo Device", "preset 2")).save()
self.data_manager.load_group("Foo Device")
self.assertEqual(self.data_manager.get_newest_preset_name(), "preset 2")
def test_newest_group_ignores_unknown_filetypes(self):
Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save()
time.sleep(0.01)
Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save()
# not a preset, ignore
time.sleep(0.01)
path = os.path.join(PathUtils.get_preset_path("Foo Device"), "picture.png")
os.mknod(path)
self.assertEqual(self.data_manager.get_newest_group_key(), "Bar Device")
def test_newest_preset_ignores_unknown_filetypes(self):
Preset(PathUtils.get_preset_path("Bar Device", "preset 1")).save()
time.sleep(0.01)
Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save()
time.sleep(0.01)
Preset(PathUtils.get_preset_path("Bar Device", "preset 3")).save()
# not a preset, ignore
time.sleep(0.01)
path = os.path.join(PathUtils.get_preset_path("Bar Device"), "picture.png")
os.mknod(path)
self.data_manager.load_group("Bar Device")
self.assertEqual(self.data_manager.get_newest_preset_name(), "preset 3")
def test_newest_group_ignores_unknown_groups(self):
Preset(PathUtils.get_preset_path("Bar Device", "preset 1")).save()
time.sleep(0.01)
# not a known group
Preset(PathUtils.get_preset_path("unknown_group", "preset 2")).save()
self.assertEqual(self.data_manager.get_newest_group_key(), "Bar Device")
def test_newest_group_and_preset_raises_file_not_found(self):
"""should raise file not found error when all preset folders are empty"""
self.assertRaises(FileNotFoundError, self.data_manager.get_newest_group_key)
os.makedirs(PathUtils.get_preset_path("Bar Device"))
self.assertRaises(FileNotFoundError, self.data_manager.get_newest_group_key)
self.data_manager.load_group("Bar Device")
self.assertRaises(FileNotFoundError, self.data_manager.get_newest_preset_name)
def test_newest_preset_raises_data_management_error(self):
"""should raise data management error without an active group"""
self.assertRaises(DataManagementError, self.data_manager.get_newest_preset_name)
def test_newest_preset_only_searches_active_group(self):
Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save()
time.sleep(0.01)
Preset(PathUtils.get_preset_path("Foo Device", "preset 3")).save()
time.sleep(0.01)
Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save()
self.data_manager.load_group("Foo Device")
self.assertEqual(self.data_manager.get_newest_preset_name(), "preset 3")
def test_available_preset_name_default(self):
self.data_manager.load_group("Foo Device")
self.assertEqual(
self.data_manager.get_available_preset_name(), DEFAULT_PRESET_NAME
)
def test_available_preset_name_adds_number_to_default(self):
Preset(PathUtils.get_preset_path("Foo Device", DEFAULT_PRESET_NAME)).save()
self.data_manager.load_group("Foo Device")
self.assertEqual(
self.data_manager.get_available_preset_name(), f"{DEFAULT_PRESET_NAME} 2"
)
def test_available_preset_name_returns_provided_name(self):
self.data_manager.load_group("Foo Device")
self.assertEqual(self.data_manager.get_available_preset_name("bar"), "bar")
def test_available_preset_name__adds_number_to_provided_name(self):
Preset(PathUtils.get_preset_path("Foo Device", "bar")).save()
self.data_manager.load_group("Foo Device")
self.assertEqual(self.data_manager.get_available_preset_name("bar"), "bar 2")
def test_available_preset_name_raises_data_management_error(self):
"""should raise DataManagementError when group is not set"""
self.assertRaises(
DataManagementError, self.data_manager.get_available_preset_name
)
def test_get_preset_names(self):
self.data_manager.load_group("Qux/[Device]?")
Preset(PathUtils.get_preset_path("Qux/[Device]?", "new preset")).save()
# get_preset_names uses glob, the special characters in the device name
# don't break it.
self.assertEqual(self.data_manager.get_preset_names(), ("new preset",))
def test_available_preset_name_sanitized(self):
self.data_manager.load_group("Qux/[Device]?")
self.assertEqual(
self.data_manager.get_available_preset_name(), DEFAULT_PRESET_NAME
)
Preset(PathUtils.get_preset_path("Qux/[Device]?", DEFAULT_PRESET_NAME)).save()
self.assertEqual(
self.data_manager.get_available_preset_name(), f"{DEFAULT_PRESET_NAME} 2"
)
Preset(PathUtils.get_preset_path("Qux/[Device]?", "foo")).save()
self.assertEqual(self.data_manager.get_available_preset_name("foo"), "foo 2")
def test_available_preset_name_increments_default(self):
Preset(PathUtils.get_preset_path("Foo Device", DEFAULT_PRESET_NAME)).save()
Preset(
PathUtils.get_preset_path("Foo Device", f"{DEFAULT_PRESET_NAME} 2")
).save()
Preset(
PathUtils.get_preset_path("Foo Device", f"{DEFAULT_PRESET_NAME} 3")
).save()
self.data_manager.load_group("Foo Device")
self.assertEqual(
self.data_manager.get_available_preset_name(), f"{DEFAULT_PRESET_NAME} 4"
)
def test_available_preset_name_increments_provided_name(self):
Preset(PathUtils.get_preset_path("Foo Device", "foo")).save()
Preset(PathUtils.get_preset_path("Foo Device", "foo 1")).save()
Preset(PathUtils.get_preset_path("Foo Device", "foo 2")).save()
self.data_manager.load_group("Foo Device")
self.assertEqual(self.data_manager.get_available_preset_name("foo 1"), "foo 3")
def test_should_publish_groups(self):
listener = Listener()
self.message_broker.subscribe(MessageType.groups, listener)
self.data_manager.publish_groups()
data = listener.calls[0]
# we expect a list of tuples with the group key and their device types
self.assertEqual(
data.groups,
{
"Foo Device": ["keyboard"],
"Foo Device 2": ["gamepad", "keyboard", "mouse"],
"Bar Device": ["keyboard"],
"gamepad": ["gamepad"],
"Qux/[Device]?": ["keyboard"],
},
)
def test_should_load_group(self):
prepare_presets()
listener = Listener()
self.message_broker.subscribe(MessageType.group, listener)
self.data_manager.load_group("Foo Device 2")
self.assertEqual(self.data_manager.active_group.key, "Foo Device 2")
data = (
GroupData("Foo Device 2", (p1, p2, p3))
for p1, p2, p3 in permutations(("preset3", "preset2", "preset1"))
)
self.assertIn(listener.calls[0], data)
def test_should_start_reading_active_group(self):
def f(*_):
raise AssertionError()
self.reader.set_group = f
self.assertRaises(AssertionError, self.data_manager.load_group, "Foo Device")
def test_should_send_uinputs(self):
listener = Listener()
self.message_broker.subscribe(MessageType.uinputs, listener)
self.data_manager.publish_uinputs()
data = listener.calls[0]
# we expect a list of tuples with the group key and their device types
self.assertEqual(
data.uinputs,
{
"gamepad": self.uinputs.get_uinput("gamepad").capabilities(),
"keyboard": self.uinputs.get_uinput("keyboard").capabilities(),
"mouse": self.uinputs.get_uinput("mouse").capabilities(),
"keyboard + mouse": self.uinputs.get_uinput(
"keyboard + mouse"
).capabilities(),
},
)
def test_cannot_stop_injecting_without_group(self):
self.assertRaises(DataManagementError, self.data_manager.stop_injecting)
def test_cannot_start_injecting_without_preset(self):
self.data_manager.load_group("Foo Device")
self.assertRaises(DataManagementError, self.data_manager.start_injecting)
def test_cannot_get_injector_state_without_group(self):
self.assertRaises(DataManagementError, self.data_manager.get_state)
class FakeFocusServiceClient:
"""A FocusServiceClient stand-in that never touches the session bus."""
def __init__(self, status=None):
self._status = status
self.calls: List = []
self.handler = None
def get_status(self):
self.calls.append("get_status")
return self._status
def reload(self):
self.calls.append("reload")
def set_enabled(self, enabled):
self.calls.append(("set_enabled", enabled))
def connect_focus_changed(self, callback):
self.handler = callback
self.calls.append("connect")
return True
def disconnect_focus_changed(self):
self.calls.append("disconnect")
self.handler = None
@test_setup
class TestDataManagerAppBindings(unittest.TestCase):
def _make_data_manager(self, status=None) -> DataManager:
message_broker = MessageBroker()
uinputs = GlobalUInputs(FrontendUInput)
uinputs.prepare_all()
self.focus_service = FakeFocusServiceClient(status=status)
return DataManager(
message_broker,
GlobalConfig(),
ReaderClient(message_broker, _Groups()),
FakeDaemonProxy(),
uinputs,
keyboard_layout,
focus_service=self.focus_service,
)
def setUp(self) -> None:
self.data_manager = self._make_data_manager()
self.message_broker = self.data_manager.message_broker
@staticmethod
def _binding(app_id="firefox", presets=()):
from inputremapper.configs.app_binding import AppBinding, BoundPreset
return AppBinding(
app_id=app_id,
presets=[BoundPreset(group_key=g, preset=p) for g, p in presets],
)
def test_publish_app_bindings_unreachable(self):
from inputremapper.gui.messages.message_data import AppBindingsData
listener = Listener()
self.message_broker.subscribe(MessageType.app_bindings, listener)
self.data_manager.publish_app_bindings()
data: AppBindingsData = listener.calls[0]
self.assertFalse(data.reachable)
self.assertFalse(data.supported)
self.assertIsNone(data.backend)
self.assertFalse(data.enabled)
self.assertEqual(data.bindings, ())
def test_publish_app_bindings_reachable(self):
self.data_manager = self._make_data_manager(status={"backend": "xorg"})
self.message_broker = self.data_manager.message_broker
listener = Listener()
self.message_broker.subscribe(MessageType.app_bindings, listener)
self.data_manager.publish_app_bindings()
data = listener.calls[0]
self.assertTrue(data.reachable)
self.assertTrue(data.supported)
self.assertEqual(data.backend, "xorg")
def test_publish_app_bindings_reachable_but_unsupported(self):
# service reachable but reports no backend (e.g. GNOME-Wayland / null)
self.data_manager = self._make_data_manager(status={"backend": None})
self.message_broker = self.data_manager.message_broker
listener = Listener()
self.message_broker.subscribe(MessageType.app_bindings, listener)
self.data_manager.publish_app_bindings()
data = listener.calls[0]
self.assertTrue(data.reachable)
self.assertFalse(data.supported)
def test_set_and_get_app_bindings(self):
bindings = [self._binding("firefox", [("Foo Device", "preset1")])]
listener = Listener()
self.message_broker.subscribe(MessageType.app_bindings, listener)
self.data_manager.set_app_bindings(bindings)
self.assertEqual(self.data_manager.get_app_bindings(), bindings)
self.assertIn("reload", self.focus_service.calls)
# publishes an app_bindings update
self.assertEqual(listener.calls[-1].bindings, tuple(bindings))
def test_set_and_get_app_binding_enabled(self):
listener = Listener()
self.message_broker.subscribe(MessageType.app_bindings, listener)
self.data_manager.set_app_binding_enabled(True)
self.assertTrue(self.data_manager.get_app_binding_enabled())
self.assertIn(("set_enabled", True), self.focus_service.calls)
self.assertTrue(listener.calls[-1].enabled)
def test_start_and_stop_app_detection(self):
self.data_manager.start_app_detection()
self.assertIn("connect", self.focus_service.calls)
self.assertIsNotNone(self.focus_service.handler)
self.data_manager.stop_app_detection()
self.assertIn("disconnect", self.focus_service.calls)
self.assertIsNone(self.focus_service.handler)
def test_on_focus_changed_publishes_focused_app(self):
from inputremapper.gui.messages.message_data import FocusAppData
listener = Listener()
self.message_broker.subscribe(MessageType.focused_app, listener)
self.data_manager.start_app_detection()
# simulate the service emitting a focus_changed signal
self.focus_service.handler({"app_id": "firefox", "title": "Mozilla"})
data: FocusAppData = listener.calls[0]
self.assertEqual(data.app_id, "firefox")
self.assertEqual(data.title, "Mozilla")
def test_on_focus_changed_handles_missing_fields(self):
from inputremapper.gui.messages.message_data import FocusAppData
listener = Listener()
self.message_broker.subscribe(MessageType.focused_app, listener)
self.data_manager.start_app_detection()
self.focus_service.handler({})
data: FocusAppData = listener.calls[0]
self.assertEqual(data.app_id, "")
self.assertEqual(data.title, "")
def test_get_presets_for_group(self):
prepare_presets()
presets = self.data_manager.get_presets_for_group("Foo Device")
# newest first
self.assertEqual(presets, ("preset3", "preset2", "preset1"))
def test_get_presets_for_unknown_group(self):
self.assertEqual(
self.data_manager.get_presets_for_group("Does Not Exist"), tuple()
)