diff --git a/.coveragerc b/.coveragerc
index 99c060bf..46fc78a9 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -6,3 +6,12 @@ debug = multiproc
omit =
# not used currently due to problems
/usr/lib/python3.9/site-packages/inputremapper/ipc/socket.py
+
+[report]
+exclude_lines =
+ pragma: no cover
+
+ # Don't complain about abstract methods, they aren't run:
+ @(abc\.)?abstractmethod
+ # Don't cover Protocol classes
+ class .*\(.*Protocol.*\):
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1c73fd75..2d4b7ebf 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,7 +20,7 @@ jobs:
run: |
# Install deps as root since we will run tests as root
sudo scripts/ci-install-deps.sh
- sudo pip install .
+ sudo pip install --no-binary :all: .
- name: Run tests
run: |
# FIXME: Had some permissions issues, currently worked around by running tests as root
diff --git a/README.md b/README.md
index 0062501e..27d5e9f2 100644
--- a/README.md
+++ b/README.md
@@ -16,10 +16,9 @@
#### Known Issues (Beta Branch)
- * The GUI is currently is not adapted to reflect all new features and might behave strange.
- Mapping a gamepad to mouse is currently not possible in the GUI.
- Also mapping joystick or mouse axis to buttons might not work.
- Those are only limitations of the GUI, when editing the preset manually all those features are still available.
+ * Mapping relative axis to relative axis (mouse to mouse) is not possible
+ * Mapping relative axis to absolute axis (mouse to gamepad) is not possible
+ * Mapping absolute axis to absolute axis (gamepad to gamepad) is not possible
## Installation
diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk
index f48adb60..e70809ee 100755
--- a/bin/input-remapper-gtk
+++ b/bin/input-remapper-gtk
@@ -18,13 +18,15 @@
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see .
-
"""Starts the user interface."""
+from __future__ import annotations
-
+import os
import sys
import atexit
+import logging
from argparse import ArgumentParser
+
from inputremapper.gui.gettext import _, LOCALE_DIR
import gi
@@ -38,7 +40,24 @@ from gi.repository import Gtk
Gtk.init()
from inputremapper.logger import logger, update_verbosity, log_info
-from inputremapper.configs.migrations import migrate
+
+
+def start_processes() -> DaemonProxy:
+ """Start helper and daemon via pkexec to run in the background."""
+ # this function is overwritten in tests
+ daemon = Daemon.connect()
+
+ debug = " -d" if logger.level <= logging.DEBUG else ""
+ cmd = f"pkexec input-remapper-control --command helper {debug}"
+
+ logger.debug("Running `%s`", cmd)
+ exit_code = os.system(cmd)
+
+ if exit_code != 0:
+ logger.error("Failed to pkexec the helper, code %d", exit_code)
+ sys.exit(11)
+
+ return daemon
if __name__ == '__main__':
@@ -55,21 +74,42 @@ if __name__ == '__main__':
logger.debug('Using locale directory: {}'.format(LOCALE_DIR))
# import input-remapper stuff after setting the log verbosity
+ from inputremapper.gui.message_broker import MessageBroker, MessageType
+ from inputremapper.configs.system_mapping import system_mapping
+ from inputremapper.gui.data_manager import DataManager
from inputremapper.gui.user_interface import UserInterface
- from inputremapper.daemon import Daemon
- from inputremapper.configs.global_config import global_config
+ from inputremapper.gui.controller import Controller
+ from inputremapper.injection.global_uinputs import GlobalUInputs
+ from inputremapper.groups import _Groups
+ from inputremapper.gui.reader import Reader
+ from inputremapper.daemon import Daemon, DaemonProxy
+ from inputremapper.configs.global_config import GlobalConfig
+ from inputremapper.configs.migrations import migrate
migrate()
- global_config.load_config()
- user_interface = UserInterface()
+ message_broker = MessageBroker()
+
+ # create the reader before we start the helper (start_processes) otherwise it
+ # can come to race conditions with the creation of pipes
+ reader = Reader(message_broker, _Groups())
+ daemon = start_processes()
+
+ data_manager = DataManager(
+ message_broker, GlobalConfig(), reader, daemon, GlobalUInputs(), system_mapping
+ )
+ controller = Controller(message_broker, data_manager)
+ user_interface = UserInterface(message_broker, controller)
+ controller.set_gui(user_interface)
+
+ message_broker.signal(MessageType.init)
def stop():
- if isinstance(user_interface.dbus, Daemon):
+ if isinstance(daemon, Daemon):
# have fun debugging completely unrelated tests if you remove this
- user_interface.dbus.stop_all()
+ daemon.stop_all()
- user_interface.on_close()
+ controller.close()
atexit.register(stop)
diff --git a/bin/input-remapper-helper b/bin/input-remapper-helper
index badf944d..4881729c 100755
--- a/bin/input-remapper-helper
+++ b/bin/input-remapper-helper
@@ -29,6 +29,7 @@ import signal
from argparse import ArgumentParser
from inputremapper.logger import update_verbosity
+from inputremapper.groups import _Groups
if __name__ == '__main__':
@@ -53,6 +54,6 @@ if __name__ == '__main__':
os.kill(os.getpid(), signal.SIGKILL)
atexit.register(on_exit)
-
- helper = RootHelper()
+ groups = _Groups()
+ helper = RootHelper(groups)
helper.run()
diff --git a/data/input-remapper.glade b/data/input-remapper.glade
index ae7868d4..9cd5e21e 100644
--- a/data/input-remapper.glade
+++ b/data/input-remapper.glade
@@ -22,6 +22,11 @@
2edit-copy
+
-
-
-First, select your device (like your keyboard) from the large dropdown on the top.
-Then you can already edit your keys, as shown in the screenshots.
+First, select your device (like your keyboard) from the large dropdown on the top,
+and add a mapping.
+Then you can already edit your inputs, as shown in the screenshots.
In the text input field, type the key to which you would like to map this key.
More information about the possible mappings can be found [below](#key-names).
-Changes are saved automatically. Afterwards press the "Apply" button.
+Changes are saved automatically.
+Press the "Apply" button to activate (inject) the mapping you created.
-To change the mapping, you need to use the "Stop Injection" button, so that
-the application can read the original keycode. It would otherwise be
-invisible since the daemon maps it independently of the GUI.
+If you later want to modify the Input of your mapping you need to use the
+"Stop Injection" button, so that the application can read your original input.
+It would otherwise be invisible since the daemon maps it independently of the GUI.
## Troubleshooting
@@ -35,31 +37,22 @@ No injection should be running anymore.
## Combinations
-Change the key of your mapping (`Change Key` - Button) and hold a few of your
-device keys down. Releasing them will make your text cursor jump into the
-mapping column to type in what you want to map it to.
+You can use combinations of different inputs to trigger a mapping: While you recorde
+the input (`Recorde Input` - Button) press multiple keys and/or move axis at once.
+The mapping will be triggered as soon as all the recorded inputs are pressed.
-Combinations involving Modifiers might not work. Configuring a combination
-of two keys to output a single key will require you to push down the first
-key, which of course ends up injecting that first key. Then the second key
-will trigger the mapping, because the combination is complete. This is
-not a bug. Otherwise every combination would have to automatically disable
-all keys that are involved in it.
+If you use an axis an input you can modify the threshold at which the mapping is
+activated in the `Advanced Input Configuration`.
-For example a combination of `LEFTSHIFT + a` for `b` would write "B" instead,
-because shift will be activated before you hit the "a". Therefore the
-environment will see shift and a "b", which will then be capitalized.
-
-Consider using a different key for the combination than shift. You could use
-`KP1 + a` and map `KP1` to `disable`.
-
-The second option is to release the modifier in your combination by writing
-the modifier one more time. This will write lowercase "b" characters. To make
-this work shift has to be injected via key-mappers devices though, which just
-means it has to be forwarded. So the complete mapping for this would look like:
-
-- `Shift L + a` -> `key(Shift_L).hold(b)`
-- `Shift L` -> `Shift_L`
+A mapping with an input combination is only injected once all combination keys
+are pressed. This means all the input keys you press before the combination is complete
+will be injected unmodified. In some cases this can be desirable, in others not.
+In the `Advanced Input Configuration` is the `Release Input` toggle.
+This will release all inputs which are part of the combination before the mapping is
+injected. Consider a mapping `Shift+1 -> a` this will inject a lowercase `a` if the
+toggle is on and an uppercase `A` if it is off. The exact behaviour if the toggle is off
+is dependent on keys (are modifiers involved?), the order in which they are pressed and
+on your environment (X11/Wayland). By default the toggle is on.
## Writing Combinations
@@ -111,6 +104,18 @@ and it won't be able to inject anything a usb keyboard wouldn't been able to. Th
the benefit of being compatible to all display servers, but means the environment will
ultimately decide which character to write.
+## Analog Axis
+
+It is possible to map analog inputs to analog outputs. E.g. use a gamepad as a mouse.
+For this you need to create a mapping and recorde the input axis. Then go to
+`Advanced Input Configuration` and select `Use as Analog`. Make sure to select a target
+which supports analog axis and switch to the `Analog Axis` tab.
+There you can select an output axis and use the different sliders to configure the
+sensitivity, non-linearity and other parameters as you like.
+
+It is also possible to use an analog output with an input combination.
+This will result in the analog axis to be only injected if the combination is pressed
+
# External tools
Repositories listed here are made by input-remappers users. Feel free to extend. Beware,
@@ -131,7 +136,7 @@ Note for the Beta branch: All configuration files are copied to:
The default configuration is stored at `~/.config/input-remapper/config.json`,
which doesn't include any mappings, but rather other parameters that
-are interesting for injections. The current default configuration as of 1.5
+are interesting for injections. The current default configuration as of 1.6
looks like, with an example autoload entry:
```json
@@ -139,7 +144,7 @@ looks like, with an example autoload entry:
"autoload": {
"Logitech USB Keyboard": "preset name"
},
- "version": "1.5"
+ "version": "1.6"
}
```
diff --git a/readme/usage_1.png b/readme/usage_1.png
index 1eac2f9c..72e28260 100644
Binary files a/readme/usage_1.png and b/readme/usage_1.png differ
diff --git a/readme/usage_2.png b/readme/usage_2.png
index c10427ca..037667fe 100644
Binary files a/readme/usage_2.png and b/readme/usage_2.png differ
diff --git a/scripts/ci-install-deps.sh b/scripts/ci-install-deps.sh
index 6bb2a311..6ebb4528 100755
--- a/scripts/ci-install-deps.sh
+++ b/scripts/ci-install-deps.sh
@@ -4,7 +4,7 @@ set -xeuo pipefail
# native deps
# gettext required to generate translations, others are python deps
-sudo apt-get install -y gettext python3-evdev python3-pydbus python3-pydantic
+sudo apt-get install -y gettext python3-evdev python3-pydbus python3-pydantic python3-gi gir1.2-gtk-3.0 gir1.2-gtksource-4
# ensure pip and setuptools/wheel up to date so can install all pip modules
python -m pip install --upgrade pip
diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py
new file mode 100644
index 00000000..5ff56b0c
--- /dev/null
+++ b/tests/integration/test_components.py
@@ -0,0 +1,1142 @@
+import unittest
+from typing import Optional, Tuple
+from unittest.mock import MagicMock, patch
+from evdev.ecodes import EV_KEY, KEY_A, KEY_B, KEY_C
+
+import gi
+
+from inputremapper.input_event import InputEvent
+
+gi.require_version("Gtk", "3.0")
+gi.require_version("GLib", "2.0")
+gi.require_version("GtkSource", "4")
+from gi.repository import Gtk, GLib, GtkSource
+
+
+from tests.test import quick_cleanup, spy
+from inputremapper.gui.utils import CTX_ERROR, CTX_WARNING, gtk_iteration
+from inputremapper.gui.message_broker import (
+ MessageBroker,
+ MessageType,
+ GroupData,
+ GroupsData,
+ UInputsData,
+ PresetData,
+ CombinationUpdate,
+ StatusData,
+)
+from inputremapper.groups import DeviceType
+from inputremapper.gui.components import (
+ DeviceSelection,
+ TargetSelection,
+ PresetSelection,
+ MappingListBox,
+ SelectionLabel,
+ CodeEditor,
+ RecordingToggle,
+ StatusBar,
+ AutoloadSwitch,
+ ReleaseCombinationSwitch,
+ CombinationListbox,
+ EventEntry,
+ AnalogInputSwitch,
+ TriggerThresholdInput,
+ ReleaseTimeoutInput,
+ OutputAxisSelector,
+ KeyAxisStack,
+ Sliders,
+ TransformationDrawArea,
+)
+from inputremapper.configs.mapping import MappingData
+from inputremapper.event_combination import EventCombination
+
+
+class ComponentBaseTest(unittest.TestCase):
+ """test a gui component
+
+ ensures to tearDown self.gui
+ all gtk objects must be a child of self.gui in order to ensure proper cleanup"""
+
+ def setUp(self) -> None:
+ self.message_broker = MessageBroker()
+ self.controller_mock = MagicMock()
+ self.gui = MagicMock()
+
+ def tearDown(self) -> None:
+ super().tearDown()
+ self.message_broker.signal(MessageType.terminate)
+ GLib.timeout_add(0, self.gui.destroy)
+ GLib.timeout_add(0, Gtk.main_quit)
+ Gtk.main()
+ quick_cleanup()
+
+
+class TestDeviceSelection(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestDeviceSelection, self).setUp()
+ self.gui = Gtk.ComboBox()
+ self.selection = DeviceSelection(
+ self.message_broker, self.controller_mock, self.gui
+ )
+ self.message_broker.send(
+ GroupsData(
+ {
+ "foo": [DeviceType.GAMEPAD, DeviceType.KEYBOARD],
+ "bar": [],
+ "baz": [DeviceType.GRAPHICS_TABLET],
+ }
+ )
+ )
+
+ def test_populates_devices(self):
+ names = [row[0] for row in self.gui.get_model()]
+ self.assertEqual(names, ["foo", "bar", "baz"])
+ icons = [row[1] for row in self.gui.get_model()]
+ self.assertEqual(icons, ["input-gaming", None, "input-tablet"])
+
+ self.message_broker.send(
+ GroupsData(
+ {
+ "kuu": [DeviceType.KEYBOARD],
+ "qux": [DeviceType.GAMEPAD],
+ }
+ )
+ )
+ names = [row[0] for row in self.gui.get_model()]
+ self.assertEqual(names, ["kuu", "qux"])
+ icons = [row[1] for row in self.gui.get_model()]
+ self.assertEqual(icons, ["input-keyboard", "input-gaming"])
+
+ def test_selects_correct_device(self):
+ self.message_broker.send(GroupData("bar", ()))
+ self.assertEqual(self.gui.get_active_id(), "bar")
+ self.message_broker.send(GroupData("baz", ()))
+ self.assertEqual(self.gui.get_active_id(), "baz")
+
+ def test_loads_group(self):
+ self.gui.set_active_id("bar")
+ self.controller_mock.load_group.assert_called_once_with("bar")
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(GroupData("bar", ()))
+ self.controller_mock.load_group.assert_not_called()
+
+
+class TestTargetSelection(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestTargetSelection, self).setUp()
+ self.gui = Gtk.ComboBox()
+ self.selection = TargetSelection(
+ self.message_broker, self.controller_mock, self.gui
+ )
+ self.message_broker.send(
+ UInputsData(
+ {
+ "foo": {},
+ "bar": {},
+ "baz": {},
+ }
+ )
+ )
+
+ def test_populates_devices(self):
+ names = [row[0] for row in self.gui.get_model()]
+ self.assertEqual(names, ["foo", "bar", "baz"])
+
+ self.message_broker.send(
+ UInputsData(
+ {
+ "kuu": {},
+ "qux": {},
+ }
+ )
+ )
+ names = [row[0] for row in self.gui.get_model()]
+ self.assertEqual(names, ["kuu", "qux"])
+
+ def test_updates_mapping(self):
+ self.gui.set_active_id("baz")
+ self.controller_mock.update_mapping.called_once_with(target_uinput="baz")
+
+ def test_selects_correct_target(self):
+ self.message_broker.send(MappingData(target_uinput="baz"))
+ self.assertEqual(self.gui.get_active_id(), "baz")
+ self.message_broker.send(MappingData(target_uinput="bar"))
+ self.assertEqual(self.gui.get_active_id(), "bar")
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(MappingData(target_uinput="baz"))
+ self.controller_mock.update_mapping.assert_not_called()
+
+ def test_disabled_with_invalid_mapping(self):
+ self.controller_mock.is_empty_mapping.return_value = True
+ self.message_broker.send(MappingData())
+ self.assertFalse(self.gui.get_sensitive())
+ self.assertLess(self.gui.get_opacity(), 0.8)
+
+ def test_enabled_with_valid_mapping(self):
+ self.controller_mock.is_empty_mapping.return_value = False
+ self.message_broker.send(MappingData())
+ self.assertTrue(self.gui.get_sensitive())
+ self.assertEqual(self.gui.get_opacity(), 1)
+
+
+class TestPresetSelection(ComponentBaseTest):
+ def setUp(self) -> None:
+ super().setUp()
+ self.gui = Gtk.ComboBoxText()
+ self.selection = PresetSelection(
+ self.message_broker, self.controller_mock, self.gui
+ )
+ self.message_broker.send(GroupData("foo", ("preset1", "preset2")))
+
+ def test_populates_presets(self):
+ names = [row[0] for row in self.gui.get_model()]
+ self.assertEqual(names, ["preset1", "preset2"])
+ self.message_broker.send(GroupData("foo", ("preset3", "preset4")))
+ names = [row[0] for row in self.gui.get_model()]
+ self.assertEqual(names, ["preset3", "preset4"])
+
+ def test_selects_preset(self):
+ self.message_broker.send(
+ PresetData("preset2", (("m1", EventCombination((1, 2, 3))),))
+ )
+ self.assertEqual(self.gui.get_active_id(), "preset2")
+ self.message_broker.send(
+ PresetData("preset1", (("m1", EventCombination((1, 2, 3))),))
+ )
+ self.assertEqual(self.gui.get_active_id(), "preset1")
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(
+ PresetData("preset2", (("m1", EventCombination((1, 2, 3))),))
+ )
+ self.controller_mock.load_preset.assert_not_called()
+
+ def test_loads_preset(self):
+ self.gui.set_active_id("preset2")
+ self.controller_mock.load_preset.assert_called_once_with("preset2")
+
+
+class TestMappingListbox(ComponentBaseTest):
+ def setUp(self) -> None:
+ super().setUp()
+ self.gui = Gtk.ListBox()
+ self.listbox = MappingListBox(
+ self.message_broker, self.controller_mock, self.gui
+ )
+
+ self.message_broker.send(
+ PresetData(
+ "preset1",
+ (
+ ("mapping1", EventCombination((1, KEY_C, 1))),
+ ("", EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)])),
+ ("mapping2", EventCombination((1, KEY_B, 1))),
+ ),
+ )
+ )
+
+ def get_selected_row(self) -> SelectionLabel:
+ row = None
+
+ def find_row(r: SelectionLabel):
+ nonlocal row
+ if r.is_selected():
+ row = r
+
+ self.gui.foreach(find_row)
+ assert row is not None
+ return row
+
+ def select_row(self, combination: EventCombination):
+ def select(row: SelectionLabel):
+ if row.combination == combination:
+ self.gui.select_row(row)
+
+ self.gui.foreach(select)
+
+ def test_populates_listbox(self):
+ labels = {row.name for row in self.gui.get_children()}
+ self.assertEqual(labels, {"mapping1", "mapping2", "a + b"})
+
+ def test_alphanumerically_sorted(self):
+ labels = [row.name for row in self.gui.get_children()]
+ self.assertEqual(labels, ["a + b", "mapping1", "mapping2"])
+
+ def test_activates_correct_row(self):
+ self.message_broker.send(
+ MappingData(
+ name="mapping1", event_combination=EventCombination((1, KEY_C, 1))
+ )
+ )
+ selected = self.get_selected_row()
+ self.assertEqual(selected.name, "mapping1")
+ self.assertEqual(selected.combination, EventCombination((1, KEY_C, 1)))
+
+ def test_loads_mapping(self):
+ self.select_row(EventCombination((1, KEY_B, 1)))
+ self.controller_mock.load_mapping.assert_called_once_with(
+ EventCombination((1, KEY_B, 1))
+ )
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(
+ MappingData(
+ name="mapping1", event_combination=EventCombination((1, KEY_C, 1))
+ )
+ )
+ self.controller_mock.load_mapping.assert_not_called()
+
+ def test_sorts_empty_mapping_to_bottom(self):
+ self.message_broker.send(
+ PresetData(
+ "preset1",
+ (
+ ("qux", EventCombination((1, KEY_C, 1))),
+ ("foo", EventCombination.empty_combination()),
+ ("bar", EventCombination((1, KEY_B, 1))),
+ ),
+ )
+ )
+ bottom_row: SelectionLabel = self.gui.get_row_at_index(2)
+ self.assertEqual(bottom_row.combination, EventCombination.empty_combination())
+ self.message_broker.send(
+ PresetData(
+ "preset1",
+ (
+ ("foo", EventCombination.empty_combination()),
+ ("qux", EventCombination((1, KEY_C, 1))),
+ ("bar", EventCombination((1, KEY_B, 1))),
+ ),
+ )
+ )
+ bottom_row: SelectionLabel = self.gui.get_row_at_index(2)
+ self.assertEqual(bottom_row.combination, EventCombination.empty_combination())
+
+
+class TestSelectionLabel(ComponentBaseTest):
+ def setUp(self) -> None:
+ super().setUp()
+ self.gui = Gtk.ListBox()
+ self.label = SelectionLabel(
+ self.message_broker,
+ self.controller_mock,
+ "",
+ EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ )
+ self.gui.insert(self.label, -1)
+
+ def test_shows_combination_without_name(self):
+ self.assertEqual(self.label.label.get_label(), "a + b")
+
+ def test_shows_name_when_given(self):
+ self.gui = SelectionLabel(
+ self.message_broker,
+ self.controller_mock,
+ "foo",
+ EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ )
+ self.assertEqual(self.gui.label.get_label(), "foo")
+
+ def test_updates_combination_when_selected(self):
+ self.gui.select_row(self.label)
+ self.assertEqual(
+ self.label.combination, EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)])
+ )
+ self.message_broker.send(
+ CombinationUpdate(
+ EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ EventCombination((1, KEY_A, 1)),
+ )
+ )
+ self.assertEqual(self.label.combination, EventCombination((1, KEY_A, 1)))
+
+ def test_doesnt_update_combination_when_not_selected(self):
+ self.assertEqual(
+ self.label.combination, EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)])
+ )
+ self.message_broker.send(
+ CombinationUpdate(
+ EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ EventCombination((1, KEY_A, 1)),
+ )
+ )
+ self.assertEqual(
+ self.label.combination, EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)])
+ )
+
+ def test_updates_name_when_mapping_changed_and_combination_matches(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ name="foo",
+ )
+ )
+ self.assertEqual(self.label.label.get_label(), "foo")
+
+ def test_ignores_mapping_when_combination_does_not_match(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_C, 1)]),
+ name="foo",
+ )
+ )
+ self.assertEqual(self.label.label.get_label(), "a + b")
+
+ def test_edit_button_visibility(self):
+ # start off invisible
+ self.assertFalse(self.label.edit_btn.get_visible())
+
+ # load the mapping associated with the ListBoxRow
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ )
+ )
+ self.assertTrue(self.label.edit_btn.get_visible())
+
+ # load a different row
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_C, 1)]),
+ )
+ )
+ self.assertFalse(self.label.edit_btn.get_visible())
+
+ def test_enter_edit_mode_focuses_name_input(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ )
+ )
+ self.label.edit_btn.clicked()
+ self.controller_mock.set_focus.assert_called_once_with(self.label.name_input)
+
+ def test_enter_edit_mode_updates_visibility(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ )
+ )
+
+ self.assertTrue(self.label.label.get_visible())
+ self.assertFalse(self.label.name_input.get_visible())
+
+ self.label.edit_btn.clicked()
+ self.assertTrue(self.label.name_input.get_visible())
+ self.assertFalse(self.label.label.get_visible())
+
+ self.label.name_input.activate() # aka hit the return key
+ self.assertTrue(self.label.label.get_visible())
+ self.assertFalse(self.label.name_input.get_visible())
+
+ def test_update_name(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ )
+ )
+ self.label.edit_btn.clicked()
+
+ self.label.name_input.set_text("foo")
+ self.label.name_input.activate()
+ self.controller_mock.update_mapping.assert_called_once_with(name="foo")
+
+ def test_name_input_contains_combination_when_name_not_set(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ )
+ )
+ self.label.edit_btn.clicked()
+ self.assertEqual(self.label.name_input.get_text(), "a + b")
+
+ def test_name_input_contains_name(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ name="foo",
+ )
+ )
+ self.label.edit_btn.clicked()
+ self.assertEqual(self.label.name_input.get_text(), "foo")
+
+ def test_removes_name_when_name_matches_combination(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
+ name="foo",
+ )
+ )
+ self.label.edit_btn.clicked()
+ self.label.name_input.set_text("a + b")
+ self.label.name_input.activate()
+ self.controller_mock.update_mapping.assert_called_once_with(name="")
+
+
+class TestCodeEditor(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestCodeEditor, self).setUp()
+ self.gui = GtkSource.View()
+ self.editor = CodeEditor(self.message_broker, self.controller_mock, self.gui)
+ self.controller_mock.is_empty_mapping.return_value = False
+
+ def get_text(self) -> str:
+ buffer = self.gui.get_buffer()
+ return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)
+
+ def test_shows_output_symbol(self):
+ self.message_broker.send(MappingData(output_symbol="foo"))
+ self.assertEqual(self.get_text(), "foo")
+
+ def test_shows_record_input_first_message_when_mapping_is_empty(self):
+ self.controller_mock.is_empty_mapping.return_value = True
+ self.message_broker.send(MappingData(output_symbol="foo"))
+ self.assertEqual(self.get_text(), "Record the input first")
+
+ def test_inactive_when_mapping_is_empty(self):
+ self.controller_mock.is_empty_mapping.return_value = True
+ self.message_broker.send(MappingData(output_symbol="foo"))
+ self.assertFalse(self.gui.get_sensitive())
+ self.assertLess(self.gui.get_opacity(), 0.6)
+
+ def test_active_when_mapping_is_not_empty(self):
+ self.message_broker.send(MappingData(output_symbol="foo"))
+ self.assertTrue(self.gui.get_sensitive())
+ self.assertEqual(self.gui.get_opacity(), 1)
+
+ def test_expands_to_multiline(self):
+ self.message_broker.send(MappingData(output_symbol="foo\nbar"))
+ self.assertIn("multiline", self.gui.get_style_context().list_classes())
+
+ def test_shows_line_numbers_when_multiline(self):
+ self.message_broker.send(MappingData(output_symbol="foo\nbar"))
+ self.assertTrue(self.gui.get_show_line_numbers())
+
+ def test_no_multiline_when_macro_not_multiline(self):
+ self.message_broker.send(MappingData(output_symbol="foo"))
+ self.assertNotIn("multiline", self.gui.get_style_context().list_classes())
+
+ def test_no_line_numbers_macro_not_multiline(self):
+ self.message_broker.send(MappingData(output_symbol="foo"))
+ self.assertFalse(self.gui.get_show_line_numbers())
+
+ def test_is_empty_when_mapping_has_no_output_symbol(self):
+ self.message_broker.send(MappingData())
+ self.assertEqual(self.get_text(), "")
+
+ def test_updates_mapping(self):
+ self.message_broker.send(MappingData())
+ buffer = self.gui.get_buffer()
+ buffer.set_text("foo")
+ self.controller_mock.update_mapping.assert_called_once_with(output_symbol="foo")
+
+ def test_avoids_infinite_recursion_when_loading_mapping(self):
+ self.message_broker.send(MappingData(output_symbol="foo"))
+ self.controller_mock.update_mapping.assert_not_called()
+
+ def test_gets_focus_when_input_recording_finises(self):
+ self.message_broker.signal(MessageType.recording_finished)
+ self.controller_mock.set_focus.assert_called_once_with(self.gui)
+
+
+class TestRecordingToggle(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestRecordingToggle, self).setUp()
+ self.gui = Gtk.ToggleButton()
+ self.toggle = RecordingToggle(
+ self.message_broker, self.controller_mock, self.gui
+ )
+
+ def assert_recording(self):
+ self.assertEqual(self.gui.get_label(), "Recording ...")
+ self.assertTrue(self.gui.get_active())
+
+ def assert_not_recording(self):
+ self.assertEqual(self.gui.get_label(), "Record Input")
+ self.assertFalse(self.gui.get_active())
+
+ def test_starts_recording(self):
+ self.gui.set_active(True)
+ self.controller_mock.start_key_recording.assert_called_once()
+
+ def test_stops_recording_when_clicked(self):
+ self.gui.set_active(True)
+ self.gui.set_active(False)
+ self.controller_mock.stop_key_recording.assert_called_once()
+
+ def test_not_recording_initially(self):
+ self.assert_not_recording()
+
+ def test_shows_recording_when_toggled(self):
+ self.gui.set_active(True)
+ self.assert_recording()
+
+ def test_shows_not_recording_after_toggle(self):
+ self.gui.set_active(True)
+ self.gui.set_active(False)
+ self.assert_not_recording()
+
+ def test_shows_not_recording_when_recording_finished(self):
+ self.gui.set_active(True)
+ self.message_broker.signal(MessageType.recording_finished)
+ self.assert_not_recording()
+
+
+class TestStatusBar(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestStatusBar, self).setUp()
+ self.gui = Gtk.Statusbar()
+ self.err_icon = Gtk.Image()
+ self.warn_icon = Gtk.Image()
+ self.statusbar = StatusBar(
+ self.message_broker,
+ self.controller_mock,
+ self.gui,
+ self.err_icon,
+ self.warn_icon,
+ )
+ self.message_broker.signal(MessageType.init)
+
+ def assert_empty(self):
+ self.assertFalse(self.err_icon.get_visible())
+ self.assertFalse(self.warn_icon.get_visible())
+ self.assertEqual(self.get_text(), "")
+ self.assertIsNone(self.get_tooltip())
+
+ def assert_error_status(self):
+ self.assertTrue(self.err_icon.get_visible())
+ self.assertFalse(self.warn_icon.get_visible())
+
+ def assert_warning_status(self):
+ self.assertFalse(self.err_icon.get_visible())
+ self.assertTrue(self.warn_icon.get_visible())
+
+ def get_text(self) -> str:
+ return self.gui.get_message_area().get_children()[0].get_text()
+
+ def get_tooltip(self) -> Optional[str]:
+ return self.gui.get_tooltip_text()
+
+ def test_starts_empty(self):
+ self.assert_empty()
+
+ def test_shows_error_status(self):
+ self.message_broker.send(StatusData(CTX_ERROR, "msg", "tooltip"))
+ self.assertEqual(self.get_text(), "msg")
+ self.assertEqual(self.get_tooltip(), "tooltip")
+ self.assert_error_status()
+
+ def test_shows_warning_status(self):
+ self.message_broker.send(StatusData(CTX_WARNING, "msg", "tooltip"))
+ self.assertEqual(self.get_text(), "msg")
+ self.assertEqual(self.get_tooltip(), "tooltip")
+ self.assert_warning_status()
+
+ def test_shows_newest_message(self):
+ self.message_broker.send(StatusData(CTX_ERROR, "msg", "tooltip"))
+ self.message_broker.send(StatusData(CTX_WARNING, "msg2", "tooltip2"))
+ self.assertEqual(self.get_text(), "msg2")
+ self.assertEqual(self.get_tooltip(), "tooltip2")
+ self.assert_warning_status()
+
+ def test_data_without_message_removes_messages(self):
+ self.message_broker.send(StatusData(CTX_WARNING, "msg", "tooltip"))
+ self.message_broker.send(StatusData(CTX_WARNING, "msg2", "tooltip2"))
+ self.message_broker.send(StatusData(CTX_WARNING))
+ self.assert_empty()
+
+ def test_restores_message_from_not_removed_ctx_id(self):
+ self.message_broker.send(StatusData(CTX_ERROR, "msg", "tooltip"))
+ self.message_broker.send(StatusData(CTX_WARNING, "msg2", "tooltip2"))
+ self.message_broker.send(StatusData(CTX_WARNING))
+ self.assertEqual(self.get_text(), "msg")
+ self.assert_error_status()
+
+ # works also the other way round
+ self.message_broker.send(StatusData(CTX_ERROR))
+ self.message_broker.send(StatusData(CTX_WARNING, "msg", "tooltip"))
+ self.message_broker.send(StatusData(CTX_ERROR, "msg2", "tooltip2"))
+ self.message_broker.send(StatusData(CTX_ERROR))
+ self.assertEqual(self.get_text(), "msg")
+ self.assert_warning_status()
+
+ def test_sets_msg_as_tooltip_if_tooltip_is_none(self):
+ self.message_broker.send(StatusData(CTX_ERROR, "msg"))
+ self.assertEqual(self.get_tooltip(), "msg")
+
+
+class TestAutoloadSwitch(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestAutoloadSwitch, self).setUp()
+ self.gui = Gtk.Switch()
+ self.switch = AutoloadSwitch(
+ self.message_broker, self.controller_mock, self.gui
+ )
+
+ def test_sets_autoload(self):
+ self.gui.set_active(True)
+ self.controller_mock.set_autoload.assert_called_once_with(True)
+ self.controller_mock.reset_mock()
+ self.gui.set_active(False)
+ self.controller_mock.set_autoload.assert_called_once_with(False)
+
+ def test_updates_state(self):
+ self.message_broker.send(PresetData(None, None, autoload=True))
+ self.assertTrue(self.gui.get_active())
+ self.message_broker.send(PresetData(None, None, autoload=False))
+ self.assertFalse(self.gui.get_active())
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(PresetData(None, None, autoload=True))
+ self.message_broker.send(PresetData(None, None, autoload=False))
+ self.controller_mock.set_autoload.assert_not_called()
+
+
+class TestReleaseCombinationSwitch(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestReleaseCombinationSwitch, self).setUp()
+ self.gui = Gtk.Switch()
+ self.switch = ReleaseCombinationSwitch(
+ self.message_broker, self.controller_mock, self.gui
+ )
+
+ def test_updates_mapping(self):
+ self.gui.set_active(True)
+ self.controller_mock.update_mapping.assert_called_once_with(
+ release_combination_keys=True
+ )
+ self.controller_mock.reset_mock()
+ self.gui.set_active(False)
+ self.controller_mock.update_mapping.assert_called_once_with(
+ release_combination_keys=False
+ )
+
+ def test_updates_state(self):
+ self.message_broker.send(MappingData(release_combination_keys=True))
+ self.assertTrue(self.gui.get_active())
+ self.message_broker.send(MappingData(release_combination_keys=False))
+ self.assertFalse(self.gui.get_active())
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(MappingData(release_combination_keys=True))
+ self.message_broker.send(MappingData(release_combination_keys=False))
+ self.controller_mock.update_mapping.assert_not_called()
+
+
+class TestEventEntry(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestEventEntry, self).setUp()
+ self.gui = EventEntry(InputEvent.from_string("3,0,1"), self.controller_mock)
+
+ def test_move_event(self):
+ self.gui._up_btn.clicked()
+ self.controller_mock.move_event_in_combination.assert_called_once_with(
+ InputEvent.from_string("3,0,1"), "up"
+ )
+ self.controller_mock.reset_mock()
+
+ self.gui._down_btn.clicked()
+ self.controller_mock.move_event_in_combination.assert_called_once_with(
+ InputEvent.from_string("3,0,1"), "down"
+ )
+
+
+class TestCombinationListbox(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestCombinationListbox, self).setUp()
+ self.gui = Gtk.ListBox()
+ self.listbox = CombinationListbox(
+ self.message_broker, self.controller_mock, self.gui
+ )
+ self.controller_mock.is_empty_mapping.return_value = False
+ self.message_broker.send(
+ MappingData(event_combination="1,1,1+3,0,1+1,2,1", target_uinput="keyboard")
+ )
+
+ def get_selected_row(self) -> EventEntry:
+ row = None
+
+ def find_row(r: EventEntry):
+ nonlocal row
+ if r.is_selected():
+ row = r
+
+ self.gui.foreach(find_row)
+ assert row is not None
+ return row
+
+ def select_row(self, event: InputEvent):
+ def select(row: EventEntry):
+ if row.input_event == event:
+ self.gui.select_row(row)
+
+ self.gui.foreach(select)
+
+ def test_loads_selected_row(self):
+ self.select_row(InputEvent.from_string("1,2,1"))
+ self.controller_mock.load_event.assert_called_once_with(
+ InputEvent.from_string("1,2,1")
+ )
+
+ def test_does_not_create_rows_when_mapping_is_empty(self):
+ self.controller_mock.is_empty_mapping.return_value = True
+ self.message_broker.send(MappingData(event_combination="1,1,1+3,0,1"))
+ self.assertEqual(len(self.gui.get_children()), 0)
+
+ def test_selects_row_when_selected_event_message_arrives(self):
+ self.message_broker.send(InputEvent.from_string("3,0,1"))
+ self.assertEqual(
+ self.get_selected_row().input_event, InputEvent.from_string("3,0,1")
+ )
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(InputEvent.from_string("3,0,1"))
+ self.controller_mock.load_event.assert_not_called()
+
+
+class TestAnalogInputSwitch(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestAnalogInputSwitch, self).setUp()
+ self.gui = Gtk.Switch()
+ self.switch = AnalogInputSwitch(
+ self.message_broker, self.controller_mock, self.gui
+ )
+
+ def test_updates_event_as_analog(self):
+ self.gui.set_active(True)
+ self.controller_mock.set_event_as_analog.assert_called_once_with(True)
+ self.controller_mock.reset_mock()
+ self.gui.set_active(False)
+ self.controller_mock.set_event_as_analog.assert_called_once_with(False)
+
+ def test_updates_state(self):
+ self.message_broker.send(InputEvent.from_string("3,0,0"))
+ self.assertTrue(self.gui.get_active())
+ self.message_broker.send(InputEvent.from_string("3,0,10"))
+ self.assertFalse(self.gui.get_active())
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(InputEvent.from_string("3,0,0"))
+ self.message_broker.send(InputEvent.from_string("3,0,-10"))
+ self.controller_mock.set_event_as_analog.assert_not_called()
+
+ def test_disables_switch_when_key_event(self):
+ self.message_broker.send(InputEvent.from_string("1,1,1"))
+ self.assertLess(self.gui.get_opacity(), 0.6)
+ self.assertFalse(self.gui.get_sensitive())
+
+ def test_enables_switch_when_axis_event(self):
+ self.message_broker.send(InputEvent.from_string("1,1,1"))
+ self.message_broker.send(InputEvent.from_string("3,0,10"))
+ self.assertEqual(self.gui.get_opacity(), 1)
+ self.assertTrue(self.gui.get_sensitive())
+
+ self.message_broker.send(InputEvent.from_string("1,1,1"))
+ self.message_broker.send(InputEvent.from_string("2,0,10"))
+ self.assertEqual(self.gui.get_opacity(), 1)
+ self.assertTrue(self.gui.get_sensitive())
+
+
+class TestTriggerThresholdInput(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestTriggerThresholdInput, self).setUp()
+ self.gui = Gtk.SpinButton()
+ self.input = TriggerThresholdInput(
+ self.message_broker, self.controller_mock, self.gui
+ )
+ self.message_broker.send(InputEvent.from_string("3,0,-10"))
+
+ def assert_abs_event_config(self):
+ self.assertEqual(self.gui.get_range(), (-99, 99))
+ self.assertTrue(self.gui.get_sensitive())
+ self.assertEqual(self.gui.get_opacity(), 1)
+
+ def assert_rel_event_config(self):
+ self.assertEqual(self.gui.get_range(), (-999, 999))
+ self.assertTrue(self.gui.get_sensitive())
+ self.assertEqual(self.gui.get_opacity(), 1)
+
+ def assert_key_event_config(self):
+ self.assertFalse(self.gui.get_sensitive())
+ self.assertLess(self.gui.get_opacity(), 0.6)
+
+ def test_updates_event(self):
+ self.gui.set_value(15)
+ self.controller_mock.update_event.assert_called_once_with(
+ InputEvent.from_string("3,0,15")
+ )
+
+ def test_sets_value_on_selected_event_message(self):
+ self.message_broker.send(InputEvent.from_string("3,0,10"))
+ self.assertEqual(self.gui.get_value(), 10)
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(InputEvent.from_string("3,0,10"))
+ self.controller_mock.update_event.assert_not_called()
+
+ def test_updates_configuration_according_to_selected_event(self):
+ self.assert_abs_event_config()
+ self.message_broker.send(InputEvent.from_string("2,0,-10"))
+ self.assert_rel_event_config()
+ self.message_broker.send(InputEvent.from_string("1,1,1"))
+ self.assert_key_event_config()
+
+
+class TestReleaseTimeoutInput(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestReleaseTimeoutInput, self).setUp()
+ self.gui = Gtk.SpinButton()
+ self.input = ReleaseTimeoutInput(
+ self.message_broker, self.controller_mock, self.gui
+ )
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination("2,0,1"), target_uinput="keyboard"
+ )
+ )
+
+ def test_updates_timeout_on_mapping_message(self):
+ self.message_broker.send(
+ MappingData(event_combination=EventCombination("2,0,1"), release_timeout=1)
+ )
+ self.assertEqual(self.gui.get_value(), 1)
+
+ def test_updates_mapping(self):
+ self.gui.set_value(0.5)
+ self.controller_mock.update_mapping.assert_called_once_with(release_timeout=0.5)
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(
+ MappingData(event_combination=EventCombination("2,0,1"), release_timeout=1)
+ )
+ self.controller_mock.update_mapping.assert_not_called()
+
+ def test_disables_input_based_on_input_combination(self):
+ self.message_broker.send(
+ MappingData(event_combination=EventCombination.from_string("2,0,1+1,1,1"))
+ )
+ self.assertTrue(self.gui.get_sensitive())
+ self.assertEqual(self.gui.get_opacity(), 1)
+
+ self.message_broker.send(
+ MappingData(event_combination=EventCombination.from_string("1,1,1+1,2,1"))
+ )
+ self.assertFalse(self.gui.get_sensitive())
+ self.assertLess(self.gui.get_opacity(), 0.6)
+
+ self.message_broker.send(
+ MappingData(event_combination=EventCombination.from_string("2,0,1+1,1,1"))
+ )
+ self.message_broker.send(
+ MappingData(event_combination=EventCombination.from_string("3,0,1+1,2,1"))
+ )
+ self.assertFalse(self.gui.get_sensitive())
+ self.assertLess(self.gui.get_opacity(), 0.6)
+
+
+class TestOutputAxisSelector(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestOutputAxisSelector, self).setUp()
+ self.gui = Gtk.ComboBox()
+ self.selection = OutputAxisSelector(
+ self.message_broker, self.controller_mock, self.gui
+ )
+
+ self.message_broker.send(
+ UInputsData(
+ {
+ "mouse": {1: [1, 2, 3, 4], 2: [0, 1, 2, 3]},
+ "keyboard": {1: [1, 2, 3, 4]},
+ "gamepad": {2: [0, 1, 2, 3], 3: [0, 1, 2, 3]},
+ }
+ )
+ )
+ self.message_broker.send(
+ MappingData(target_uinput="mouse", event_combination="1,1,1")
+ )
+
+ def set_active_selection(self, selection: Tuple):
+ self.gui.set_active_id(f"{selection[0]}, {selection[1]}")
+
+ def get_active_selection(self) -> Tuple[int, int]:
+ return tuple(int(i) for i in self.gui.get_active_id().split(",")) # type: ignore
+
+ def test_updates_mapping(self):
+ self.set_active_selection((2, 0))
+ self.controller_mock.update_mapping.assert_called_once_with(
+ output_type=2, output_code=0
+ )
+
+ def test_updates_mapping_with_none(self):
+ self.set_active_selection((2, 0))
+ self.controller_mock.reset_mock()
+ self.set_active_selection((None, None))
+ self.controller_mock.update_mapping.assert_called_once_with(
+ output_type=None, output_code=None
+ )
+
+ def test_selects_correct_entry(self):
+ self.assertEqual(self.gui.get_active_id(), "None, None")
+ self.message_broker.send(
+ MappingData(target_uinput="mouse", output_type=2, output_code=3)
+ )
+ self.assertEqual(self.get_active_selection(), (2, 3))
+
+ def test_avoids_infinite_recursion(self):
+ self.message_broker.send(
+ MappingData(target_uinput="mouse", output_type=2, output_code=3)
+ )
+ self.controller_mock.update_mapping.assert_not_called()
+
+ def test_updates_dropdown_model(self):
+ self.assertEqual(len(self.gui.get_model()), 5)
+ self.message_broker.send(MappingData(target_uinput="keyboard"))
+ self.assertEqual(len(self.gui.get_model()), 1)
+ self.message_broker.send(MappingData(target_uinput="gamepad"))
+ self.assertEqual(len(self.gui.get_model()), 9)
+
+
+class TestKeyAxisStack(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestKeyAxisStack, self).setUp()
+ self.gui = Gtk.Stack()
+ self.gui.add_named(Gtk.Box(), "Analog Axis")
+ self.gui.add_named(Gtk.Box(), "Key or Macro")
+ self.stack = KeyAxisStack(self.message_broker, self.controller_mock, self.gui)
+ self.gui.show_all()
+ self.gui.set_visible_child_name("Key or Macro")
+
+ def test_switches_to_axis_when_mapping_has_output_type_code_but_not_symbol(self):
+ self.message_broker.send(MappingData(output_type=2, output_code=0))
+ self.assertEqual(self.gui.get_visible_child_name(), "Analog Axis")
+
+ def test_switches_to_key_when_mapping_has_output_symbol_but_type_code(self):
+ self.gui.set_visible_child_name("Analog Axis")
+ self.message_broker.send(MappingData(output_symbol="a"))
+ self.assertEqual(self.gui.get_visible_child_name(), "Key or Macro")
+
+ def test_does_not_switch_when_mapping_is_ambiguous(self):
+ self.message_broker.send(
+ MappingData(output_type=2, output_code=0, output_symbol="a")
+ )
+ self.assertEqual(self.gui.get_visible_child_name(), "Key or Macro")
+ self.message_broker.send(MappingData(output_type=2, output_symbol="a"))
+ self.assertEqual(self.gui.get_visible_child_name(), "Key or Macro")
+ self.message_broker.send(MappingData(output_code=0, output_symbol="a"))
+ self.assertEqual(self.gui.get_visible_child_name(), "Key or Macro")
+
+ self.gui.set_visible_child_name("Analog Axis")
+ self.message_broker.send(
+ MappingData(output_type=2, output_code=0, output_symbol="a")
+ )
+ self.assertEqual(self.gui.get_visible_child_name(), "Analog Axis")
+ self.message_broker.send(MappingData(output_type=2, output_symbol="a"))
+ self.assertEqual(self.gui.get_visible_child_name(), "Analog Axis")
+ self.message_broker.send(MappingData(output_code=0, output_symbol="a"))
+ self.assertEqual(self.gui.get_visible_child_name(), "Analog Axis")
+
+
+class TestTransformationDrawArea(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestTransformationDrawArea, self).setUp()
+ self.gui = Gtk.Window()
+ self.draw_area = Gtk.DrawingArea()
+ self.gui.add(self.draw_area)
+ self.transform_draw_area = TransformationDrawArea(
+ self.message_broker, self.controller_mock, self.draw_area
+ )
+
+ def test_draws_transform(self):
+ with spy(self.transform_draw_area, "_transformation") as mock:
+ self.gui.show_all()
+ gtk_iteration()
+ mock.assert_called()
+
+ def test_updates_transform_when_mapping_updates(self):
+ old_tf = self.transform_draw_area._transformation
+ self.message_broker.send(MappingData(gain=2))
+ self.assertIsNot(old_tf, self.transform_draw_area._transformation)
+
+ def test_redraws_when_mapping_updates(self):
+ self.gui.show_all()
+ gtk_iteration(20)
+ mock = MagicMock()
+ self.draw_area.connect("draw", mock)
+ self.message_broker.send(MappingData(gain=2))
+ gtk_iteration(20)
+ mock.assert_called()
+
+
+class TestSliders(ComponentBaseTest):
+ def setUp(self) -> None:
+ super(TestSliders, self).setUp()
+ self.gui = Gtk.Box()
+ self.gain = Gtk.Scale()
+ self.deadzone = Gtk.Scale()
+ self.expo = Gtk.Scale()
+
+ # add everything to a box: it will be cleand up properly
+ self.gui.add(self.gain)
+ self.gui.add(self.deadzone)
+ self.gui.add(self.expo)
+
+ self.sliders = Sliders(
+ self.message_broker,
+ self.controller_mock,
+ self.gain,
+ self.deadzone,
+ self.expo,
+ )
+ self.message_broker.send(
+ MappingData(event_combination="3,0,0", target_uinput="mouse")
+ )
+
+ @staticmethod
+ def get_range(range: Gtk.Range) -> Tuple[int, int]:
+ """the Gtk.Range, has no get_range method. this is a workaround"""
+ v = range.get_value()
+ range.set_value(-(2**16))
+ min_ = range.get_value()
+ range.set_value(2**16)
+ max_ = range.get_value()
+ range.set_value(v)
+ return min_, max_
+
+ def test_slider_ranges(self):
+ self.assertEqual(self.get_range(self.gain), (-2, 2))
+ self.assertEqual(self.get_range(self.deadzone), (0, 0.9))
+ self.assertEqual(self.get_range(self.expo), (-1, 1))
+
+ def test_updates_value(self):
+ self.message_broker.send(
+ MappingData(
+ gain=0.5,
+ deadzone=0.6,
+ expo=0.3,
+ )
+ )
+ self.assertEqual(self.gain.get_value(), 0.5)
+ self.assertEqual(self.expo.get_value(), 0.3)
+ self.assertEqual(self.deadzone.get_value(), 0.6)
+
+ def test_gain_updates_mapping(self):
+ self.gain.set_value(0.5)
+ self.controller_mock.update_mapping.assert_called_once_with(gain=0.5)
+
+ def test_expo_updates_mapping(self):
+ self.expo.set_value(0.5)
+ self.controller_mock.update_mapping.assert_called_once_with(expo=0.5)
+
+ def test_deadzone_updates_mapping(self):
+ self.deadzone.set_value(0.5)
+ self.controller_mock.update_mapping.assert_called_once_with(deadzone=0.5)
+
+ def test_avoids_recursion(self):
+ self.message_broker.send(MappingData(gain=0.5))
+ self.controller_mock.update_mapping.assert_not_called()
+ self.message_broker.send(MappingData(expo=0.5))
+ self.controller_mock.update_mapping.assert_not_called()
+ self.message_broker.send(MappingData(deadzone=0.5))
+ self.controller_mock.update_mapping.assert_not_called()
diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py
index dc9db15f..6dcb74b5 100644
--- a/tests/integration/test_gui.py
+++ b/tests/integration/test_gui.py
@@ -20,6 +20,10 @@
# the tests file needs to be imported first to make sure patches are loaded
+from contextlib import contextmanager
+from typing import Tuple, List
+
+from inputremapper.exceptions import DataManagementError
from tests.test import (
get_project_root,
logger,
@@ -31,9 +35,9 @@ from tests.test import (
uinput_write_history_pipe,
MAX_ABS,
EVENT_READ_TIMEOUT,
- send_event_to_reader,
MIN_ABS,
get_ui_mapping,
+ prepare_presets,
)
import sys
@@ -48,13 +52,14 @@ from evdev.ecodes import (
EV_ABS,
KEY_LEFTSHIFT,
KEY_A,
+ KEY_Q,
ABS_RX,
EV_REL,
REL_X,
ABS_X,
)
import json
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock, call
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
@@ -62,22 +67,31 @@ import gi
from inputremapper.input_event import InputEvent
gi.require_version("Gtk", "3.0")
-from gi.repository import Gtk, GLib, Gdk
+gi.require_version("GLib", "2.0")
+gi.require_version("GtkSource", "4")
+from gi.repository import Gtk, GLib, Gdk, GtkSource
from inputremapper.configs.system_mapping import system_mapping, XMODMAP_FILENAME
-from inputremapper.configs.mapping import UIMapping
-from inputremapper.gui.active_preset import active_preset
+from inputremapper.configs.mapping import UIMapping, Mapping
from inputremapper.configs.paths import CONFIG_PATH, get_preset_path, get_config_path
from inputremapper.configs.global_config import global_config, WHEEL, MOUSE, BUTTONS
-from inputremapper.gui.reader import reader
+from inputremapper.groups import _Groups
+from inputremapper.gui.data_manager import DataManager
+from inputremapper.gui.message_broker import (
+ MessageBroker,
+ MessageType,
+ StatusData,
+ CombinationRecorded,
+)
+from inputremapper.gui.components import SelectionLabel, SET_KEY_FIRST
+from inputremapper.gui.reader import Reader
+from inputremapper.gui.controller import Controller
from inputremapper.gui.helper import RootHelper
from inputremapper.gui.utils import gtk_iteration
from inputremapper.gui.user_interface import UserInterface
-from inputremapper.gui.editor.editor import SET_KEY_FIRST
-from inputremapper.injection.injector import RUNNING, FAILED, UNKNOWN
+from inputremapper.injection.injector import RUNNING, FAILED, UNKNOWN, STOPPED
from inputremapper.event_combination import EventCombination
-from inputremapper.daemon import Daemon
-from inputremapper.groups import groups
+from inputremapper.daemon import Daemon, DaemonProxy
# iterate a few times when Gtk.main() is called, but don't block
@@ -89,22 +103,20 @@ Gtk.main = gtk_iteration
Gtk.main_quit = lambda: None
-def launch(argv=None) -> UserInterface:
+def launch(
+ argv=None,
+) -> Tuple[UserInterface, Controller, DataManager, MessageBroker, DaemonProxy]:
"""Start input-remapper-gtk with the command line argument array argv."""
bin_path = os.path.join(get_project_root(), "bin", "input-remapper-gtk")
if not argv:
argv = ["-d"]
- with patch(
- "inputremapper.gui.user_interface.UserInterface.setup_timeouts",
- lambda *args: None,
- ):
- with patch.object(sys, "argv", [""] + [str(arg) for arg in argv]):
- loader = SourceFileLoader("__main__", bin_path)
- spec = spec_from_loader("__main__", loader)
- module = module_from_spec(spec)
- spec.loader.exec_module(module)
+ with patch.object(sys, "argv", [""] + [str(arg) for arg in argv]):
+ loader = SourceFileLoader("__main__", bin_path)
+ spec = spec_from_loader("__main__", loader)
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
gtk_iteration()
@@ -112,58 +124,50 @@ def launch(argv=None) -> UserInterface:
# spams tons of garbage when all tests finish
atexit.unregister(module.stop)
- # to avoid triggering any timeouts while the module loads, patch it and
- # do it afterwards. Because some tests don't want them to be triggered
- # yet and test the windows initial state. This is only a problem on
- # slow computers that take long for the window import.
- module.user_interface.setup_timeouts()
-
- return module.user_interface
-
-
-class FakeDeviceDropdown(Gtk.ComboBoxText):
- def __init__(self, group):
- if type(group) == str:
- group = groups.find(key=group)
-
- self.group = group
-
- def get_active_text(self):
- return self.group.name
-
- def get_active_id(self):
- return self.group.key
-
- def set_active_id(self, key):
- self.group = groups.find(key=key)
+ return (
+ module.user_interface,
+ module.controller,
+ module.data_manager,
+ module.message_broker,
+ module.daemon,
+ )
-class FakePresetDropdown(Gtk.ComboBoxText):
- def __init__(self, name):
- self.name = name
+@contextmanager
+def patch_launch():
+ """patch the launch function such that we don't connect to
+ the dbus and don't use pkexec to start the helper"""
+ original_connect = Daemon.connect
+ original_os_system = os.system
+ Daemon.connect = Daemon
- def get_active_text(self):
- return self.name
+ def os_system(cmd):
+ # instead of running pkexec, fork instead. This will make
+ # the helper aware of all the test patches
+ if "pkexec input-remapper-control --command helper" in cmd:
+ multiprocessing.Process(target=RootHelper(_Groups()).run).start()
+ return 0
- def get_active_id(self):
- return self.name
+ return original_os_system(cmd)
- def set_active_id(self, name):
- self.name = name
+ os.system = os_system
+ yield
+ os.system = original_os_system
+ Daemon.connect = original_connect
def clean_up_integration(test):
- test.user_interface.on_stop_injecting_clicked(None)
+ test.controller.stop_injecting()
gtk_iteration()
- test.user_interface.on_close()
+ test.user_interface.on_gtk_close()
test.user_interface.window.destroy()
gtk_iteration()
cleanup()
# do this now, not when all tests are finished
- test.user_interface.dbus.stop_all()
- if isinstance(test.user_interface.dbus, Daemon):
- atexit.unregister(test.user_interface.dbus.stop_all)
+ test.daemon.stop_all()
+ if isinstance(test.daemon, Daemon):
+ atexit.unregister(test.daemon.stop_all)
class GtkKeyEvent:
@@ -176,33 +180,33 @@ class GtkKeyEvent:
class TestGroupsFromHelper(unittest.TestCase):
def setUp(self):
- self.injector = None
- self.grab = evdev.InputDevice.grab
-
# don't try to connect, return an object instance of it instead
self.original_connect = Daemon.connect
Daemon.connect = Daemon
+ # this is already part of the test. we need a bit of patching and hacking
+ # because we want to discover the groups as early a possible, to reduce startup
+ # time for the application
self.original_os_system = os.system
+ self.helper_started = MagicMock()
def os_system(cmd):
# instead of running pkexec, fork instead. This will make
# the helper aware of all the test patches
if "pkexec input-remapper-control --command helper" in cmd:
- # the forked process should get the initial groups
- groups.refresh()
- multiprocessing.Process(target=RootHelper).start()
- # the gui an empty dict, because it doesn't know any devices
- # without the help of the privileged helper
- groups.set_groups([])
- assert len(groups) == 0
+ self.helper_started() # don't start the helper just log that it was.
return 0
return self.original_os_system(cmd)
os.system = os_system
-
- self.user_interface = launch()
+ (
+ self.user_interface,
+ self.controller,
+ self.data_manager,
+ self.message_broker,
+ self.daemon,
+ ) = launch()
def tearDown(self):
clean_up_integration(self)
@@ -212,42 +216,46 @@ class TestGroupsFromHelper(unittest.TestCase):
def test_knows_devices(self):
# verify that it is working as expected. The gui doesn't have knowledge
# of groups until the root-helper provides them
+ self.data_manager._reader.groups.set_groups([])
gtk_iteration()
- self.assertEqual(len(groups), 0)
+ self.helper_started.assert_called()
+ self.assertEqual(len(self.data_manager.get_group_keys()), 0)
- # perform some iterations so that the gui ends up running
- # consume_newest_keycode, which will make it receive devices.
- # Restore patch, otherwise gtk complains when disabling handlers
+ # start the helper delayed
+ multiprocessing.Process(target=RootHelper(_Groups()).run).start()
+ # perform some iterations so that the reader ends up reading from the pipes
+ # which will make it receive devices.
for _ in range(10):
time.sleep(0.02)
gtk_iteration()
- self.assertIsNotNone(groups.find(key="Foo Device 2"))
- self.assertIsNotNone(groups.find(name="Bar Device"))
- self.assertIsNotNone(groups.find(name="gamepad"))
- self.assertEqual(self.user_interface.group.name, "Foo Device")
+ self.assertIn("Foo Device 2", self.data_manager.get_group_keys())
+ self.assertIn("Foo Device 2", self.data_manager.get_group_keys())
+ self.assertIn("Bar Device", self.data_manager.get_group_keys())
+ self.assertIn("gamepad", self.data_manager.get_group_keys())
+ self.assertEqual(self.data_manager.active_group.name, "Foo Device")
class PatchedConfirmDelete:
- def __init__(self, user_interface, response=Gtk.ResponseType.ACCEPT):
+ def __init__(self, user_interface: UserInterface, response=Gtk.ResponseType.ACCEPT):
self.response = response
self.user_interface = user_interface
self.patch = None
def _confirm_delete_run_patch(self):
"""A patch for the deletion confirmation that briefly shows the dialog."""
- confirm_delete = self.user_interface.confirm_delete
+ confirm_cancel_dialog = self.user_interface.confirm_cancel_dialog
# the emitted signal causes the dialog to close
GLib.timeout_add(
100,
- lambda: confirm_delete.emit("response", self.response),
+ lambda: confirm_cancel_dialog.emit("response", self.response),
)
- Gtk.MessageDialog.run(confirm_delete) # don't recursively call the patch
+ Gtk.MessageDialog.run(confirm_cancel_dialog) # don't recursively call the patch
return self.response
def __enter__(self):
self.patch = patch.object(
- self.user_interface.get("confirm-delete"),
+ self.user_interface.get("confirm-cancel"),
"run",
self._confirm_delete_run_patch,
)
@@ -258,20 +266,56 @@ class PatchedConfirmDelete:
class GuiTestBase(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- cls.injector = None
- cls.grab = evdev.InputDevice.grab
- cls.original_start_processes = UserInterface.start_processes
+ def setUp(self):
+ prepare_presets()
+ with patch_launch():
+ (
+ self.user_interface,
+ self.controller,
+ self.data_manager,
+ self.message_broker,
+ self.daemon,
+ ) = launch()
+
+ get = self.user_interface.get
+ self.device_selection: Gtk.ComboBox = get("device_selection")
+ self.preset_selection: Gtk.ComboBoxText = get("preset_selection")
+ self.selection_label_listbox: Gtk.ListBox = get("selection_label_listbox")
+ self.target_selection: Gtk.ComboBox = get("target-selector")
+ self.recording_toggle: Gtk.ToggleButton = get("key_recording_toggle")
+ self.status_bar: Gtk.Statusbar = get("status_bar")
+ self.autoload_toggle: Gtk.Switch = get("preset_autoload_switch")
+ self.code_editor: GtkSource.View = get("code_editor")
+
+ self.delete_preset_btn: Gtk.Button = get("delete_preset")
+ self.copy_preset_btn: Gtk.Button = get("copy_preset")
+ self.create_preset_btn: Gtk.Button = get("create_preset")
+ self.start_injector_btn: Gtk.Button = get("apply_preset")
+ self.stop_injector_btn: Gtk.Button = get("apply_system_layout")
+ self.rename_btn: Gtk.Button = get("rename-button")
+ self.rename_input: Gtk.Entry = get("preset_name_input")
+ self.create_mapping_btn: Gtk.Button = get("create_mapping_button")
+ self.delete_mapping_btn: Gtk.Button = get("delete-mapping")
+
+ self.grab_fails = False
+
+ def grab(_):
+ if self.grab_fails:
+ raise OSError()
- def start_processes(self):
- """Avoid running pkexec which requires user input, and fork in
- order to pass the fixtures to the helper and daemon process.
- """
- multiprocessing.Process(target=RootHelper).start()
- self.dbus = Daemon()
+ evdev.InputDevice.grab = grab
+
+ global_config._save_config()
+
+ self.throttle()
+
+ self.assertIsNotNone(self.data_manager.active_group)
+ self.assertIsNotNone(self.data_manager.active_preset)
+
+ def tearDown(self):
+ clean_up_integration(self)
- UserInterface.start_processes = start_processes
+ self.throttle()
def _callTestMethod(self, method):
"""Retry all tests if they fail.
@@ -294,60 +338,27 @@ class GuiTestBase(unittest.TestCase):
self.tearDown()
self.setUp()
- def setUp(self):
- self.user_interface = launch()
- self.editor = self.user_interface.editor
- self.toggle = self.editor.get_recording_toggle()
- self.selection_label_listbox = self.user_interface.get(
- "selection_label_listbox"
- )
- self.window = self.user_interface.get("window")
-
- self.grab_fails = False
-
- def grab(_):
- if self.grab_fails:
- raise OSError()
-
- evdev.InputDevice.grab = grab
-
- global_config._save_config()
-
- self.throttle()
-
- self.assertIsNotNone(self.user_interface.group)
- self.assertIsNotNone(self.user_interface.group.key)
- self.assertIsNotNone(self.user_interface.preset_name)
-
- def tearDown(self):
- clean_up_integration(self)
-
- self.throttle()
-
- def throttle(self):
+ def throttle(self, iterations=10):
"""Give GTK some time to process everything."""
# tests suddenly started to freeze my computer up completely and tests started
# to fail. By using this (and by optimizing some redundant calls in the gui) it
# worked again. EDIT: Might have been caused by my broken/bloated ssd. I'll
# keep it in some places, since it did make the tests more reliable after all.
- for _ in range(10):
+ for _ in range(iterations):
gtk_iteration()
time.sleep(0.002)
- @classmethod
- def tearDownClass(cls):
- UserInterface.start_processes = cls.original_start_processes
-
def activate_recording_toggle(self):
logger.info("Activating the recording toggle")
- self.set_focus(self.toggle)
- self.toggle.set_active(True)
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
def disable_recording_toggle(self):
logger.info("Deactivating the recording toggle")
- self.set_focus(None)
+ self.recording_toggle.set_active(False)
+ gtk_iteration()
# should happen automatically:
- self.assertFalse(self.toggle.get_active())
+ self.assertFalse(self.recording_toggle.get_active())
def set_focus(self, widget):
logger.info("Focusing %s", widget)
@@ -356,7 +367,7 @@ class GuiTestBase(unittest.TestCase):
self.throttle()
- def get_selection_labels(self):
+ def get_selection_labels(self) -> List[SelectionLabel]:
return self.selection_label_listbox.get_children()
def get_status_text(self):
@@ -364,7 +375,7 @@ class GuiTestBase(unittest.TestCase):
return status_bar.get_message_area().get_children()[0].get_label()
def get_unfiltered_symbol_input_text(self):
- buffer = self.editor.get_code_editor().get_buffer()
+ buffer = self.code_editor.get_buffer()
return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)
def select_mapping(self, i: int):
@@ -373,7 +384,7 @@ class GuiTestBase(unittest.TestCase):
Parameters
----------
i
- if -1, will select the "empty row",
+ if -1, will select the last row,
0 will select the uppermost row.
1 will select the second row, and so on
"""
@@ -381,130 +392,19 @@ class GuiTestBase(unittest.TestCase):
self.selection_label_listbox.select_row(selection_label)
logger.info(
'Selecting mapping %s "%s"',
- selection_label.get_combination(),
- selection_label.get_label(),
+ selection_label.combination,
+ selection_label.name,
)
+ gtk_iteration()
return selection_label
- def add_mapping_via_ui(self, key, symbol, expect_success=True, target=None):
- """Modify the one empty mapping that always exists.
-
- Utility function for other tests.
-
- Parameters
- ----------
- key : EventCombination or None
- expect_success : boolean
- If the key can be stored in the selection label. False if this change
- is going to cause a duplicate.
- target : str
- the target selection
- """
- logger.info(
- 'Adding mapping %s, "%s", expecting to %s',
- key,
- symbol,
- "work" if expect_success else "fail",
- )
-
- self.assertIsNone(reader.get_unreleased_keys())
-
- changed = active_preset.has_unsaved_changes()
-
- # wait for the window to create a new empty selection_label if needed
- time.sleep(0.1)
- gtk_iteration()
-
- # the empty selection_label is expected to be the last one
- selection_label = self.select_mapping(-1)
- self.assertIsNone(selection_label.get_combination())
- self.assertFalse(self.editor._input_has_arrived)
-
- if self.toggle.get_active():
- self.assertEqual(self.toggle.get_label(), "Press Key")
- else:
- self.assertEqual(self.toggle.get_label(), "Change Key")
-
- # the recording toggle connects to focus events
- self.set_focus(self.toggle)
- self.toggle.set_active(True)
- self.assertIsNone(selection_label.get_combination())
- self.assertEqual(self.toggle.get_label(), "Press Key")
-
- if key:
- # modifies the keycode in the selection_label not by writing into the input,
- # but by sending an event. press down all the keys of a combination
- for sub_key in key:
- send_event_to_reader(new_event(*sub_key.event_tuple))
- # this will be consumed all at once, since no gtk_iteration
- # is done
-
- # make the window consume the keycode
- self.sleep(len(key))
-
- # holding down
- self.assertIsNotNone(reader.get_unreleased_keys())
- self.assertGreater(len(reader.get_unreleased_keys()), 0)
- self.assertTrue(self.editor._input_has_arrived)
- self.assertTrue(self.toggle.get_active())
-
- # release all the keys
- for sub_key in key:
- send_event_to_reader(new_event(*sub_key.type_and_code, 0))
-
- # wait for the window to consume the keycode
- self.sleep(len(key))
-
- # released
- self.assertIsNone(reader.get_unreleased_keys())
- self.assertFalse(self.editor._input_has_arrived)
-
- if expect_success:
- self.assertEqual(self.editor.get_combination(), key)
- # the previously new entry, which has been edited now, is still the
- # selected one
- self.assertEqual(self.editor.active_selection_label, selection_label)
- self.assertEqual(
- self.editor.active_selection_label.get_label(),
- key.beautify(),
- )
- self.assertFalse(self.toggle.get_active())
- self.assertEqual(len(reader._unreleased), 0)
-
- if not expect_success:
- self.assertIsNone(selection_label.get_combination())
- self.assertEqual(self.editor.get_symbol_input_text(), "")
- self.assertFalse(self.editor._input_has_arrived)
- # it won't switch the focus to the symbol input
- self.assertTrue(self.toggle.get_active())
- self.assertEqual(active_preset.has_unsaved_changes(), changed)
- return selection_label
-
- if key is None:
- self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
- self.assertEqual(self.editor.get_symbol_input_text(), "")
-
- # set the target selection
- if target:
- self.editor.set_target_selection(target)
- self.assertEqual(self.editor.get_target_selection(), target)
- else:
- self.assertEqual(self.editor.get_target_selection(), "keyboard")
-
- # set the symbol to make the new selection_label complete
- self.editor.set_symbol_input_text(symbol)
- self.assertEqual(self.editor.get_symbol_input_text(), symbol)
-
- # unfocus them to trigger some final logic
- self.set_focus(None)
- correct_case = system_mapping.correct_case(symbol)
- self.assertEqual(self.editor.get_symbol_input_text(), correct_case)
- self.assertFalse(active_preset.has_unsaved_changes())
-
- self.set_focus(self.editor.get_code_editor())
- self.set_focus(None)
-
- return selection_label
+ def add_mapping(self, mapping: Mapping = None):
+ self.controller.create_mapping()
+ self.controller.load_mapping(EventCombination.empty_combination())
+ gtk_iteration()
+ if mapping:
+ self.controller.update_mapping(**mapping.dict(exclude_defaults=True))
+ gtk_iteration()
def sleep(self, num_events):
for _ in range(num_events * 2):
@@ -515,20 +415,6 @@ class GuiTestBase(unittest.TestCase):
gtk_iteration()
- def set_combination(self, combination: EventCombination) -> None:
- """Partial implementation of editor.consume_newest_keycode
- simplifies setting combination without going through the add mapping via ui function
- """
- previous_key = self.editor.get_combination()
- # keycode didn't change, do nothing
- if combination == previous_key:
- return
-
- self.editor.set_combination(combination)
- self.editor.active_mapping.event_combination = combination
- if previous_key is None and combination is not None:
- active_preset.add(self.editor.active_mapping)
-
class TestGui(GuiTestBase):
"""For tests that use the window.
@@ -540,203 +426,108 @@ class TestGui(GuiTestBase):
self.assertIsNotNone(self.user_interface)
self.assertTrue(self.user_interface.window.get_visible())
- def test_gui_clean(self):
- # check that the test is correctly set up so that the user interface is clean
+ def assert_gui_clean(self):
selection_labels = self.selection_label_listbox.get_children()
- self.assertEqual(len(selection_labels), 1)
- self.assertEqual(self.editor.active_selection_label, selection_labels[0])
- self.assertEqual(
- self.selection_label_listbox.get_selected_row(),
- selection_labels[0],
- )
- self.assertEqual(len(active_preset), 0)
- self.assertEqual(selection_labels[0].get_label(), "new entry")
- self.assertEqual(self.editor.get_symbol_input_text(), "")
- preset_selection = self.user_interface.get("preset_selection")
- self.assertEqual(preset_selection.get_active_id(), "new preset")
- self.assertEqual(len(active_preset), 0)
- self.assertEqual(self.editor.get_recording_toggle().get_label(), "Change Key")
+ self.assertEqual(len(selection_labels), 0)
+ self.assertEqual(len(self.data_manager.active_preset), 0)
+ self.assertEqual(self.preset_selection.get_active_id(), "new preset")
+ self.assertEqual(self.recording_toggle.get_label(), "Record Input")
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
- def test_ctrl_q(self):
- closed = False
-
- def on_close():
- nonlocal closed
- closed = True
-
- with patch.object(self.user_interface, "on_close", on_close):
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Control_L)
- )
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_a)
- )
- self.user_interface.on_key_release(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Control_L)
- )
- self.user_interface.on_key_release(
- self.user_interface, GtkKeyEvent(Gdk.KEY_a)
- )
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_b)
- )
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_q)
- )
- self.user_interface.on_key_release(
- self.user_interface, GtkKeyEvent(Gdk.KEY_q)
- )
- self.user_interface.on_key_release(
- self.user_interface, GtkKeyEvent(Gdk.KEY_b)
- )
- self.assertFalse(closed)
-
- # while keys are being recorded no shortcut should work
- self.toggle.set_active(True)
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Control_L)
- )
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_q)
- )
- self.assertFalse(closed)
-
- self.toggle.set_active(False)
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Control_L)
- )
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_q)
- )
- self.assertTrue(closed)
-
- self.user_interface.on_key_release(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Control_L)
- )
- self.user_interface.on_key_release(
- self.user_interface, GtkKeyEvent(Gdk.KEY_q)
- )
-
- def test_ctrl_r(self):
- with patch.object(reader, "refresh_groups") as reader_get_devices_patch:
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Control_L)
- )
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_r)
- )
- reader_get_devices_patch.assert_called_once()
-
- def test_ctrl_del(self):
- with patch.object(self.user_interface.dbus, "stop_injecting") as stop_injecting:
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Control_L)
- )
- self.user_interface.on_key_press(
- self.user_interface, GtkKeyEvent(Gdk.KEY_Delete)
- )
- stop_injecting.assert_called_once()
-
- def test_show_device_mapping_status(self):
- # this function may not return True, otherwise the timeout
- # runs forever
- self.assertFalse(self.user_interface.show_device_mapping_status())
-
- def test_autoload(self):
- self.assertFalse(
- global_config.is_autoloaded(
- self.user_interface.group.key, self.user_interface.preset_name
- )
- )
-
- with spy(self.user_interface.dbus, "set_config_dir") as set_config_dir:
- self.user_interface.on_autoload_switch(None, False)
+ def test_initial_state(self):
+ self.assertEqual(self.data_manager.active_group.key, "Foo Device")
+ self.assertEqual(self.device_selection.get_active_id(), "Foo Device")
+ self.assertEqual(self.data_manager.active_preset.name, "preset3")
+ self.assertEqual(self.preset_selection.get_active_id(), "preset3")
+ self.assertFalse(self.data_manager.get_autoload())
+ self.assertFalse(self.autoload_toggle.get_active())
+ self.assertEqual(
+ self.selection_label_listbox.get_selected_row().combination, ((1, 5, 1),)
+ )
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination, ((1, 5, 1),)
+ )
+ self.assertEqual(self.selection_label_listbox.get_selected_row().name, "4")
+ self.assertIsNone(self.data_manager.active_mapping.name)
+ self.assertTrue(self.data_manager.active_mapping.is_valid())
+ self.assertTrue(self.data_manager.active_preset.is_valid())
+ # todo
+
+ def test_set_autoload_refreshes_service_config(self):
+ self.assertFalse(self.data_manager.get_autoload())
+ with spy(self.daemon, "set_config_dir") as set_config_dir:
+ self.autoload_toggle.set_active(True)
+ gtk_iteration()
set_config_dir.assert_called_once()
+ self.assertTrue(self.data_manager.get_autoload())
- self.assertFalse(
- global_config.is_autoloaded(
- self.user_interface.group.key, self.user_interface.preset_name
- )
- )
+ def test_autoload_sets_correctly(self):
+ self.assertFalse(self.data_manager.get_autoload())
+ self.assertFalse(self.autoload_toggle.get_active())
- self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device 2"))
+ self.autoload_toggle.set_active(True)
gtk_iteration()
- self.assertFalse(self.user_interface.get("preset_autoload_switch").get_active())
+ self.assertTrue(self.data_manager.get_autoload())
+ self.assertTrue(self.autoload_toggle.get_active())
- # select a preset for the first device
- self.user_interface.get("preset_autoload_switch").set_active(True)
+ self.autoload_toggle.set_active(False)
gtk_iteration()
- self.assertTrue(self.user_interface.get("preset_autoload_switch").get_active())
- self.assertEqual(self.user_interface.group.key, "Foo Device 2")
- self.assertEqual(self.user_interface.group.name, "Foo Device")
- self.assertTrue(
- global_config.is_autoloaded(self.user_interface.group.key, "new preset")
- )
- self.assertFalse(global_config.is_autoloaded("Bar Device", "new preset"))
- self.assertListEqual(
- list(global_config.iterate_autoload_presets()),
- [("Foo Device 2", "new preset")],
- )
+ self.assertFalse(self.data_manager.get_autoload())
+ self.assertFalse(self.autoload_toggle.get_active())
+
+ def test_autoload_is_set_when_changing_preset(self):
+ self.assertFalse(self.data_manager.get_autoload())
+ self.assertFalse(self.autoload_toggle.get_active())
- # create a new preset, the switch should be correctly off and the
- # global_config not changed.
- self.user_interface.on_create_preset_clicked()
+ self.device_selection.set_active_id("Foo Device 2")
+ self.preset_selection.set_active_id("preset2")
gtk_iteration()
- self.assertEqual(self.user_interface.preset_name, "new preset 2")
- self.assertFalse(self.user_interface.get("preset_autoload_switch").get_active())
- self.assertTrue(global_config.is_autoloaded("Foo Device 2", "new preset"))
- self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset"))
- self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset 2"))
- self.assertFalse(global_config.is_autoloaded("Foo Device 2", "new preset 2"))
+ self.assertTrue(self.data_manager.get_autoload())
+ self.assertTrue(self.autoload_toggle.get_active())
- # select a preset for the second device
- self.user_interface.on_select_device(FakeDeviceDropdown("Bar Device"))
- self.user_interface.get("preset_autoload_switch").set_active(True)
+ def test_only_one_autoload_per_group(self):
+ self.assertFalse(self.data_manager.get_autoload())
+ self.assertFalse(self.autoload_toggle.get_active())
+
+ self.device_selection.set_active_id("Foo Device 2")
+ self.preset_selection.set_active_id("preset2")
gtk_iteration()
- self.assertTrue(global_config.is_autoloaded("Foo Device 2", "new preset"))
- self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset"))
- self.assertTrue(global_config.is_autoloaded("Bar Device", "new preset"))
- self.assertListEqual(
- list(global_config.iterate_autoload_presets()),
- [("Foo Device 2", "new preset"), ("Bar Device", "new preset")],
- )
+ self.assertTrue(self.data_manager.get_autoload())
+ self.assertTrue(self.autoload_toggle.get_active())
- # disable autoloading for the second device
- self.user_interface.get("preset_autoload_switch").set_active(False)
+ self.preset_selection.set_active_id("preset3")
gtk_iteration()
- self.assertTrue(global_config.is_autoloaded("Foo Device 2", "new preset"))
- self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset"))
- self.assertFalse(global_config.is_autoloaded("Bar Device", "new preset"))
- self.assertListEqual(
- list(global_config.iterate_autoload_presets()),
- [("Foo Device 2", "new preset")],
- )
+ self.autoload_toggle.set_active(True)
+ gtk_iteration()
+ self.preset_selection.set_active_id("preset2")
+ gtk_iteration()
+ self.assertFalse(self.data_manager.get_autoload())
+ self.assertFalse(self.autoload_toggle.get_active())
- def test_select_device(self):
+ def test_each_device_can_have_autoload(self):
+ self.autoload_toggle.set_active(True)
+ gtk_iteration()
+ self.assertTrue(self.data_manager.get_autoload())
+ self.assertTrue(self.autoload_toggle.get_active())
+
+ self.device_selection.set_active_id("Foo Device 2")
+ gtk_iteration()
+ self.autoload_toggle.set_active(True)
+ gtk_iteration()
+ self.assertTrue(self.data_manager.get_autoload())
+ self.assertTrue(self.autoload_toggle.get_active())
+
+ self.device_selection.set_active_id("Foo Device")
+ gtk_iteration()
+ self.assertTrue(self.data_manager.get_autoload())
+ self.assertTrue(self.autoload_toggle.get_active())
+
+ def test_select_device_without_preset(self):
# creates a new empty preset when no preset exists for the device
- self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device"))
- m1 = UIMapping(
- event_combination="1,50,1",
- output_symbol="q",
- target_uinput="keyboard",
- )
- m2 = UIMapping(
- event_combination="1,51,1",
- output_symbol="u",
- target_uinput="keyboard",
- )
- m3 = UIMapping(
- event_combination="1,52,1",
- output_symbol="x",
- target_uinput="keyboard",
- )
- active_preset.add(m1)
- active_preset.add(m2)
- active_preset.add(m3)
- self.assertEqual(len(active_preset), 3)
- self.user_interface.on_select_device(FakeDeviceDropdown("Bar Device"))
- self.assertEqual(len(active_preset), 0)
+ self.device_selection.set_active_id("Bar Device")
+ self.assertEqual(self.preset_selection.get_active_id(), "new preset")
+ self.assertEqual(len(self.data_manager.active_preset), 0)
+
# it creates the file for that right away. It may have been possible
# to write it such that it doesn't (its empty anyway), but it does,
# so use that to test it in more detail.
@@ -745,479 +536,600 @@ class TestGui(GuiTestBase):
with open(path, "r") as file:
self.assertEqual(file.read(), "")
- def test_permission_error_on_create_preset_clicked(self):
- def save(_=None):
- raise PermissionError
+ def test_recording_toggle_labels(self):
+ self.assertEqual(self.recording_toggle.get_label(), "Record Input")
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
+ self.assertEqual(self.recording_toggle.get_label(), "Recording ...")
+ self.recording_toggle.set_active(False)
+ gtk_iteration()
+ self.assertEqual(self.recording_toggle.get_label(), "Record Input")
- with patch.object(active_preset, "save", save):
- self.user_interface.on_create_preset_clicked()
- status = self.get_status_text()
- self.assertIn("Permission denied", status)
+ def test_recording_label_updates_on_recording_finished(self):
+ self.assertEqual(self.recording_toggle.get_label(), "Record Input")
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
+ self.assertEqual(self.recording_toggle.get_label(), "Recording ...")
+ self.message_broker.signal(MessageType.recording_finished)
+ gtk_iteration()
+ self.assertEqual(self.recording_toggle.get_label(), "Record Input")
+ self.assertFalse(self.recording_toggle.get_active())
- def test_show_injection_result_failure(self):
- def get_state(_=None):
- return FAILED
+ def test_events_from_helper_arrive(self):
+ # load a device with more capabilities
+ self.controller.load_group("Foo Device 2")
+ gtk_iteration()
+ mock1 = MagicMock()
+ mock2 = MagicMock()
+ self.message_broker.subscribe(MessageType.combination_recorded, mock1)
+ self.message_broker.subscribe(MessageType.recording_finished, mock2)
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
- with patch.object(self.user_interface.dbus, "get_state", get_state):
- self.user_interface.show_injection_result()
- text = self.get_status_text()
- self.assertIn("Failed", text)
+ push_events(
+ "Foo Device 2",
+ [InputEvent.from_string("1,30,1"), InputEvent.from_string("1,31,1")],
+ )
+ self.throttle(20)
+ mock1.assert_has_calls(
+ (
+ call(CombinationRecorded(EventCombination.from_string("1,30,1"))),
+ call(
+ CombinationRecorded(EventCombination.from_string("1,30,1+1,31,1"))
+ ),
+ ),
+ any_order=False,
+ )
+ self.assertEqual(mock1.call_count, 2)
+ mock2.assert_not_called()
+
+ push_events("Foo Device 2", [InputEvent.from_string("1,31,0")])
+ self.throttle(20)
+ self.assertEqual(mock1.call_count, 2)
+ mock2.assert_not_called()
+
+ push_events("Foo Device 2", [InputEvent.from_string("1,30,0")])
+ self.throttle(20)
+ self.assertEqual(mock1.call_count, 2)
+ mock2.assert_called_once()
+
+ self.assertFalse(self.recording_toggle.get_active())
+
+ def test_cannot_create_duplicate_event_combination(self):
+ # load a device with more capabilities
+ self.controller.load_group("Foo Device 2")
+ gtk_iteration()
- def test_editor_keycode_to_string(self):
- # not an integration test, but I have all the selection_label tests here already
- self.assertEqual(
- EventCombination((EV_KEY, evdev.ecodes.KEY_A, 1)).beautify(),
- "a",
+ # update the combination of the active mapping
+ self.controller.start_key_recording()
+ push_events(
+ "Foo Device 2",
+ [InputEvent.from_string("1,30,1"), InputEvent.from_string("1,30,0")],
)
+ self.throttle(20)
+
self.assertEqual(
- EventCombination([EV_KEY, evdev.ecodes.KEY_A, 1]).beautify(),
- "a",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,30,1"),
)
+
+ # create a new mapping
+ self.controller.create_mapping()
+ gtk_iteration()
self.assertEqual(
- EventCombination((EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)).beautify(),
- "DPad Up",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.empty_combination(),
)
- self.assertEqual(
- EventCombination((EV_KEY, evdev.ecodes.BTN_A, 1)).beautify(),
- "Button A",
+
+ # try to recorde the same combination
+ self.controller.start_key_recording()
+ push_events(
+ "Foo Device 2",
+ [InputEvent.from_string("1,30,1"), InputEvent.from_string("1,30,0")],
)
- self.assertEqual(EventCombination((EV_KEY, 1234, 1)).beautify(), "1234")
+ self.throttle(20)
+ # should still be the empty mapping
self.assertEqual(
- EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, -1]).beautify(),
- "DPad Left",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.empty_combination(),
)
+
+ # try to recorde a different combination
+ self.controller.start_key_recording()
+ push_events("Foo Device 2", [InputEvent.from_string("1,30,1")])
+ self.throttle(20)
+ # nothing changed yet, as we got the duplicate combination
self.assertEqual(
- EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0Y, -1]).beautify(),
- "DPad Up",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.empty_combination(),
)
+ push_events("Foo Device 2", [InputEvent.from_string("1,31,1")])
+ self.throttle(20)
+ # now the combination is different
self.assertEqual(
- EventCombination([EV_KEY, evdev.ecodes.BTN_A, 1]).beautify(),
- "Button A",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,30,1+1,31,1"),
)
- self.assertEqual(EventCombination([EV_KEY, 1234, 1]).beautify(), "1234")
+
+ # let's make the combination even longer
+ push_events("Foo Device 2", [InputEvent.from_string("1,32,1")])
+ self.throttle(20)
self.assertEqual(
- EventCombination([EV_ABS, evdev.ecodes.ABS_X, 1]).beautify(),
- "Joystick Right",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,30,1+1,31,1+1,32,1"),
+ )
+
+ # make sure we stop recording by releasing all keys
+ push_events(
+ "Foo Device 2",
+ [
+ InputEvent.from_string("1,31,0"),
+ InputEvent.from_string("1,30,0"),
+ InputEvent.from_string("1,32,0"),
+ ],
)
+ self.throttle(20)
+
+ # sending a combination update now should not do anything
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,35,1"))
+ )
+ gtk_iteration()
self.assertEqual(
- EventCombination([EV_ABS, evdev.ecodes.ABS_RY, 1]).beautify(),
- "Joystick 2 Down",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,30,1+1,31,1+1,32,1"),
)
+
+ def test_create_simple_mapping(self):
+ # 1. create a mapping
+ self.create_mapping_btn.clicked()
+ gtk_iteration()
+
self.assertEqual(
- EventCombination([EV_REL, evdev.ecodes.REL_HWHEEL, 1]).beautify(),
- "Wheel Right",
+ self.selection_label_listbox.get_selected_row().combination,
+ EventCombination.empty_combination(),
)
self.assertEqual(
- EventCombination([EV_REL, evdev.ecodes.REL_WHEEL, -1]).beautify(),
- "Wheel Down",
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.empty_combination(),
)
-
- # combinations
self.assertEqual(
- EventCombination(
- (EV_KEY, evdev.ecodes.BTN_A, 1),
- (EV_KEY, evdev.ecodes.BTN_B, 1),
- (EV_KEY, evdev.ecodes.BTN_C, 1),
- ).beautify(),
- "Button A + Button B + Button C",
+ self.selection_label_listbox.get_selected_row().name, "Empty Mapping"
)
+ self.assertIsNone(self.data_manager.active_mapping.name)
- def test_is_waiting_for_input(self):
- self.activate_recording_toggle()
- self.assertTrue(self.editor.is_waiting_for_input())
-
- self.disable_recording_toggle()
- self.assertFalse(self.editor.is_waiting_for_input())
-
- def test_editor_simple(self):
- self.assertEqual(self.toggle.get_label(), "Change Key")
-
- self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
-
- selection_label = self.selection_label_listbox.get_children()[0]
- self.activate_recording_toggle()
- self.assertTrue(self.editor.is_waiting_for_input())
- self.assertEqual(self.toggle.get_label(), "Press Key")
-
- self.user_interface.consume_newest_keycode()
- # nothing happens
- self.assertIsNone(selection_label.get_combination())
- self.assertEqual(len(active_preset), 0)
- self.assertEqual(self.toggle.get_label(), "Press Key")
-
- send_event_to_reader(InputEvent.from_tuple((EV_KEY, 30, 1)))
- self.user_interface.consume_newest_keycode()
- # no symbol configured yet, so the active_preset remains empty
- self.assertEqual(len(active_preset), 0)
- self.assertEqual(len(selection_label.get_combination()), 1)
- self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1))
- # this is KEY_A in linux/input-event-codes.h,
- # but KEY_ is removed from the text for display purposes
- self.assertEqual(selection_label.get_label(), "a")
-
- # providing the same key again doesn't do any harm
- # (Maybe this could happen for gamepads or something, idk)
- send_event_to_reader(InputEvent.from_tuple((EV_KEY, 30, 1)))
- self.user_interface.consume_newest_keycode()
- self.assertEqual(len(active_preset), 0) # not released yet
- self.assertEqual(len(selection_label.get_combination()), 1)
- self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1))
+ # there are now 2 mappings
+ self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
+ self.assertEqual(len(self.data_manager.active_preset), 2)
- time.sleep(0.11)
- # new empty entry was added
+ # 2. recorde a combination for that mapping
+ self.recording_toggle.set_active(True)
gtk_iteration()
+ push_events("Foo Device", [InputEvent.from_string("1,30,1")])
+ self.throttle(20)
+ push_events("Foo Device", [InputEvent.from_string("1,30,0")])
+ self.throttle(20)
+
+ # check the event_combination
self.assertEqual(
- len(self.selection_label_listbox.get_children()),
- 2,
+ self.selection_label_listbox.get_selected_row().combination,
+ EventCombination.from_string("1,30,1"),
)
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,30,1"),
+ )
+ self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a")
+ self.assertIsNone(self.data_manager.active_mapping.name)
- self.disable_recording_toggle()
- self.set_focus(self.editor.get_code_editor())
- self.assertFalse(self.editor.is_waiting_for_input())
-
- self.editor.set_symbol_input_text("Shift_L")
-
- self.set_focus(None)
- self.assertFalse(self.editor.is_waiting_for_input())
+ # 3. set the output symbol
+ self.code_editor.get_buffer().set_text("Shift_L")
+ gtk_iteration()
- num_mappings = len(active_preset)
- self.assertEqual(num_mappings, 1)
+ # the mapping and preset should be valid by now
+ self.assertTrue(self.data_manager.active_mapping.is_valid())
+ self.assertTrue(self.data_manager.active_preset.is_valid())
- time.sleep(0.1)
- gtk_iteration()
self.assertEqual(
- len(self.selection_label_listbox.get_children()),
- 2,
+ self.data_manager.active_mapping,
+ Mapping(
+ event_combination="1,30,1",
+ output_symbol="Shift_L",
+ target_uinput="keyboard",
+ ),
+ )
+ self.assertEqual(self.target_selection.get_active_id(), "keyboard")
+ buffer = self.code_editor.get_buffer()
+ self.assertEqual(
+ buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True),
+ "Shift_L",
)
-
self.assertEqual(
- active_preset.get_mapping(EventCombination([EV_KEY, 30, 1])),
- ("Shift_L", "keyboard"),
+ self.selection_label_listbox.get_selected_row().combination,
+ EventCombination.from_string("1,30,1"),
)
- self.assertEqual(self.editor.get_target_selection(), "keyboard")
- self.assertEqual(self.editor.get_symbol_input_text(), "Shift_L")
- self.assertEqual(len(selection_label.get_combination()), 1)
- self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1))
- self.editor.set_target_selection("mouse")
- time.sleep(0.1)
+ # 4. update target to mouse
+ self.target_selection.set_active_id("mouse")
gtk_iteration()
self.assertEqual(
- len(self.selection_label_listbox.get_children()),
- 2,
+ self.data_manager.active_mapping,
+ Mapping(
+ event_combination="1,30,1",
+ output_symbol="Shift_L",
+ target_uinput="mouse",
+ ),
)
- self.assertEqual(
- active_preset.get_mapping(EventCombination([EV_KEY, 30, 1])),
- ("Shift_L", "mouse"),
- )
- self.assertEqual(self.editor.get_target_selection(), "mouse")
- self.assertEqual(self.editor.get_symbol_input_text(), "Shift_L")
- self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1))
-
- def test_editor_not_focused(self):
- # focus anything that is not the selection_label,
- # no keycode should be inserted into it
- self.set_focus(self.user_interface.get("preset_name_input"))
- send_event_to_reader(new_event(1, 61, 1))
- self.user_interface.consume_newest_keycode()
-
- selection_labels = self.get_selection_labels()
- self.assertEqual(len(selection_labels), 1)
- selection_label = selection_labels[0]
-
- # the empty selection_label has this combination not set
- self.assertIsNone(selection_label.get_combination())
-
- # focus the text input instead
- self.set_focus(self.editor.get_code_editor())
- send_event_to_reader(new_event(1, 61, 1))
- self.user_interface.consume_newest_keycode()
-
- # still nothing set
- self.assertIsNone(selection_label.get_combination())
def test_show_status(self):
- self.user_interface.show_status(0, "a" * 100)
+ self.message_broker.send(StatusData(0, "a" * 100))
+ gtk_iteration()
text = self.get_status_text()
self.assertIn("...", text)
- self.user_interface.show_status(0, "b")
+ self.message_broker.send(StatusData(0, "b"))
+ gtk_iteration()
text = self.get_status_text()
self.assertNotIn("...", text)
- def test_clears_unreleased_on_focus_change(self):
- ev_1 = EventCombination([EV_KEY, 41, 1])
-
- # focus
- self.set_focus(self.toggle)
- send_event_to_reader(new_event(*ev_1[0].event_tuple))
- reader.read()
- self.assertEqual(reader.get_unreleased_keys(), ev_1)
-
- # unfocus
- # doesn't call reader.clear. Otherwise the super key cannot be mapped,
- # because the start menu that opens up would unfocus the user interface
- self.set_focus(None)
- self.assertEqual(reader.get_unreleased_keys(), ev_1)
-
- # focus the toggle after selecting a different selection_label.
- # It resets the reader
- self.editor.add_empty()
- self.select_mapping(-1)
- self.set_focus(self.toggle)
- self.toggle.set_active(True)
-
- self.assertEqual(reader.get_unreleased_keys(), None)
-
- def test_editor(self):
- """Comprehensive test for the editor."""
- system_mapping.clear()
- system_mapping._set("Foo_BAR", 41)
- system_mapping._set("B", 42)
- system_mapping._set("c", 43)
- system_mapping._set("d", 44)
-
- # how many selection_labels there should be in the end
- num_selection_labels_target = 3
-
- ev_1 = EventCombination([EV_KEY, 10, 1])
- ev_2 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, -1])
-
- """edit"""
-
- # add two selection_labels by modifiying the one empty selection_label that
- # exists. Insert lowercase, it should be corrected to uppercase as stored
- # in system_mapping
- self.add_mapping_via_ui(ev_1, "foo_bar", target="mouse")
- self.add_mapping_via_ui(ev_2, "k(b).k(c)")
-
- # one empty selection_label added automatically again
- time.sleep(0.1)
+ def test_hat_switch(self):
+ # load a device with more capabilities
+ self.controller.load_group("Foo Device 2")
gtk_iteration()
- self.assertEqual(len(self.get_selection_labels()), num_selection_labels_target)
-
- self.assertEqual(active_preset.get_mapping(ev_1), ("Foo_BAR", "mouse"))
- self.assertEqual(active_preset.get_mapping(ev_2), ("k(b).k(c)", "keyboard"))
- """edit first selection_label"""
-
- self.select_mapping(0)
- self.assertEqual(self.editor.get_combination(), ev_1)
- self.set_focus(self.editor.get_code_editor())
- self.editor.set_symbol_input_text("c")
- self.set_focus(None)
-
- # after unfocusing, it stores the mapping. So loading it again will retain
- # the mapping that was used
- preset_name = self.user_interface.preset_name
- preset_path = self.user_interface.group.get_preset_path(preset_name)
- active_preset.load(preset_path)
-
- self.assertEqual(active_preset.get_mapping(ev_1), ("c", "mouse"))
- self.assertEqual(active_preset.get_mapping(ev_2), ("k(b).k(c)", "keyboard"))
+ # it should be possible to add all of them
+ ev_1 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1))
+ ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1))
+ ev_3 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, -1))
+ ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, 1))
- """add duplicate"""
+ def add_mapping(event, symbol):
+ self.controller.create_mapping()
+ gtk_iteration()
+ self.controller.start_key_recording()
+ push_events("Foo Device 2", [event, event.modify(value=0)])
+ self.throttle(20)
+ gtk_iteration()
+ self.code_editor.get_buffer().set_text(symbol)
+ gtk_iteration()
- # try to add a duplicate keycode, it should be ignored
- self.add_mapping_via_ui(ev_2, "d", expect_success=False)
- self.assertEqual(active_preset.get_mapping(ev_2), ("k(b).k(c)", "keyboard"))
- # and the number of selection_labels shouldn't change
- self.assertEqual(len(self.get_selection_labels()), num_selection_labels_target)
+ add_mapping(ev_1, "a")
+ add_mapping(ev_2, "b")
+ add_mapping(ev_3, "c")
+ add_mapping(ev_4, "d")
- def test_hat0x(self):
- # it should be possible to add all of them
- ev_1 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, -1])
- ev_2 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, 1])
- ev_3 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0Y, -1])
- ev_4 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0Y, 1])
-
- self.add_mapping_via_ui(ev_1, "a")
- self.add_mapping_via_ui(ev_2, "b")
- self.add_mapping_via_ui(ev_3, "c")
- self.add_mapping_via_ui(ev_4, "d")
-
- self.assertEqual(active_preset.get_mapping(ev_1), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(ev_2), ("b", "keyboard"))
- self.assertEqual(active_preset.get_mapping(ev_3), ("c", "keyboard"))
- self.assertEqual(active_preset.get_mapping(ev_4), ("d", "keyboard"))
-
- # and trying to add them as duplicate selection_labels will be ignored for each
- # of them
- self.add_mapping_via_ui(ev_1, "e", expect_success=False)
- self.add_mapping_via_ui(ev_2, "f", expect_success=False)
- self.add_mapping_via_ui(ev_3, "g", expect_success=False)
- self.add_mapping_via_ui(ev_4, "h", expect_success=False)
-
- self.assertEqual(active_preset.get_mapping(ev_1), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(ev_2), ("b", "keyboard"))
- self.assertEqual(active_preset.get_mapping(ev_3), ("c", "keyboard"))
- self.assertEqual(active_preset.get_mapping(ev_4), ("d", "keyboard"))
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(
+ EventCombination(ev_1)
+ ).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(
+ EventCombination(ev_2)
+ ).output_symbol,
+ "b",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(
+ EventCombination(ev_3)
+ ).output_symbol,
+ "c",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(
+ EventCombination(ev_4)
+ ).output_symbol,
+ "d",
+ )
def test_combination(self):
+ # load a device with more capabilities
+ self.controller.load_group("Foo Device 2")
+ gtk_iteration()
+
# it should be possible to write a combination combination
ev_1 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_A, 1))
ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1))
ev_3 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_C, 1))
ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1))
- combination_1 = EventCombination(ev_1, ev_2, ev_3)
- combination_2 = EventCombination(ev_2, ev_1, ev_3)
+ combination_1 = EventCombination((ev_1, ev_2, ev_3))
+ combination_2 = EventCombination((ev_2, ev_1, ev_3))
# same as 1, but different D-Pad direction
- combination_3 = EventCombination(ev_1, ev_4, ev_3)
- combination_4 = EventCombination(ev_4, ev_1, ev_3)
+ combination_3 = EventCombination((ev_1, ev_4, ev_3))
+ combination_4 = EventCombination((ev_4, ev_1, ev_3))
# same as 1, but the last combination is different
- combination_5 = EventCombination(ev_1, ev_3, ev_2)
- combination_6 = EventCombination(ev_3, ev_1, ev_2)
+ combination_5 = EventCombination((ev_1, ev_3, ev_2))
+ combination_6 = EventCombination((ev_3, ev_1, ev_2))
+
+ def add_mapping(combi: EventCombination, symbol):
+ self.controller.create_mapping()
+ gtk_iteration()
+ self.controller.start_key_recording()
+ push_events("Foo Device 2", [event for event in combi])
+ push_events("Foo Device 2", [event.modify(value=0) for event in combi])
+ self.throttle(20)
+ gtk_iteration()
+ self.code_editor.get_buffer().set_text(symbol)
+ gtk_iteration()
- self.add_mapping_via_ui(combination_1, "a")
- self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard"))
- self.assertIsNone(active_preset.get_mapping(combination_3))
- self.assertIsNone(active_preset.get_mapping(combination_4))
- self.assertIsNone(active_preset.get_mapping(combination_5))
- self.assertIsNone(active_preset.get_mapping(combination_6))
+ add_mapping(combination_1, "a")
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
+ "a",
+ )
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
# it won't write the same combination again, even if the
# first two events are in a different order
- self.add_mapping_via_ui(combination_2, "b", expect_success=False)
- self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard"))
- self.assertIsNone(active_preset.get_mapping(combination_3))
- self.assertIsNone(active_preset.get_mapping(combination_4))
- self.assertIsNone(active_preset.get_mapping(combination_5))
- self.assertIsNone(active_preset.get_mapping(combination_6))
-
- self.add_mapping_via_ui(combination_3, "c")
- self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_3), ("c", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_4), ("c", "keyboard"))
- self.assertIsNone(active_preset.get_mapping(combination_5))
- self.assertIsNone(active_preset.get_mapping(combination_6))
+ add_mapping(combination_2, "b")
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
+ "a",
+ )
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
+
+ add_mapping(combination_3, "c")
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
+ "c",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
+ "c",
+ )
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
# same as with combination_2, the existing combination_3 blocks
# combination_4 because they have the same keys and end in the
# same key.
- self.add_mapping_via_ui(combination_4, "d", expect_success=False)
- self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_3), ("c", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_4), ("c", "keyboard"))
- self.assertIsNone(active_preset.get_mapping(combination_5))
- self.assertIsNone(active_preset.get_mapping(combination_6))
-
- self.add_mapping_via_ui(combination_5, "e")
- self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_3), ("c", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_4), ("c", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_5), ("e", "keyboard"))
- self.assertEqual(active_preset.get_mapping(combination_6), ("e", "keyboard"))
+ add_mapping(combination_4, "d")
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
+ "c",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
+ "c",
+ )
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
+ self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
+
+ add_mapping(combination_5, "e")
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
+ "a",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
+ "c",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
+ "c",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_5).output_symbol,
+ "e",
+ )
+ self.assertEqual(
+ self.data_manager.active_preset.get_mapping(combination_6).output_symbol,
+ "e",
+ )
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
- self.assertFalse(error_icon.get_visible())
- self.assertFalse(warning_icon.get_visible())
+ self.assertFalse(error_icon.get_visible())
+ self.assertFalse(warning_icon.get_visible())
+
+ def test_only_one_empty_mapping_possible(self):
+ self.assertEqual(
+ self.selection_label_listbox.get_selected_row().combination,
+ EventCombination.from_string("1,5,1"),
+ )
+ self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
+ self.assertEqual(len(self.data_manager.active_preset), 1)
+
+ self.create_mapping_btn.clicked()
+ gtk_iteration()
+ self.assertEqual(
+ self.selection_label_listbox.get_selected_row().combination,
+ EventCombination.empty_combination(),
+ )
+ self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
+ self.assertEqual(len(self.data_manager.active_preset), 2)
+
+ self.create_mapping_btn.clicked()
+ gtk_iteration()
+ self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
+ self.assertEqual(len(self.data_manager.active_preset), 2)
+
+ def test_selection_labels_sort_alphabetically(self):
+ self.controller.load_preset("preset1")
+ # contains two mappings (1,1,1 -> b) and (1,2,1 -> a)
+ gtk_iteration()
+ # we expect (1,2,1 -> a) to be selected because "1" < "Escape"
+ self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
+ self.assertIs(
+ self.selection_label_listbox.get_row_at_index(0),
+ self.selection_label_listbox.get_selected_row(),
+ )
+
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
+ self.message_broker.send(
+ CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1)))
+ )
+ gtk_iteration()
+ self.message_broker.signal(MessageType.recording_finished)
+ gtk_iteration()
+ # the combination and the order changed "Escape" < "q"
+ self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
+ self.assertIs(
+ self.selection_label_listbox.get_row_at_index(1),
+ self.selection_label_listbox.get_selected_row(),
+ )
+
+ def test_selection_labels_sort_empty_mapping_to_the_bottom(self):
+ # make sure we have a mapping which would sort to the bottom only
+ # considering alphanumeric sorting: "q" > "Empty Mapping"
+ self.controller.load_preset("preset1")
+ gtk_iteration()
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
+ self.message_broker.send(
+ CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1)))
+ )
+ gtk_iteration()
+ self.message_broker.signal(MessageType.recording_finished)
+ gtk_iteration()
+
+ self.controller.create_mapping()
+ gtk_iteration()
+ row: SelectionLabel = self.selection_label_listbox.get_selected_row()
+ self.assertEqual(row.combination, EventCombination.empty_combination())
+ self.assertEqual(row.label.get_text(), "Empty Mapping")
+ self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
+
+ def test_select_mapping(self):
+ self.controller.load_preset("preset1")
+ # contains two mappings (1,1,1 -> b) and (1,2,1 -> a)
+ gtk_iteration()
+ # we expect (1,2,1 -> a) to be selected because "1" < "Escape"
+ self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
+
+ # select the second entry in the listbox
+ row = self.selection_label_listbox.get_row_at_index(1)
+ self.selection_label_listbox.select_row(row)
+ gtk_iteration()
+ self.assertEqual(self.data_manager.active_mapping.output_symbol, "b")
+
+ def test_selection_label_uses_name_if_available(self):
+ self.controller.load_preset("preset1")
+ gtk_iteration()
+ row: SelectionLabel = self.selection_label_listbox.get_selected_row()
+ self.assertEqual(row.label.get_text(), "1")
+ self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
+
+ self.controller.update_mapping(name="foo")
+ gtk_iteration()
+ self.assertEqual(row.label.get_text(), "foo")
+ self.assertIs(row, self.selection_label_listbox.get_row_at_index(1))
+
+ # Empty Mapping still sorts to the bottom
+ self.controller.create_mapping()
+ gtk_iteration()
+ row = self.selection_label_listbox.get_selected_row()
+ self.assertEqual(row.combination, EventCombination.empty_combination())
+ self.assertEqual(row.label.get_text(), "Empty Mapping")
+ self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
+
+ def test_fake_empty_mapping_does_not_sort_to_bottom(self):
+ """If someone chooses to name a mapping "Empty Mapping"
+ it is not sorted to the bottom"""
+ self.controller.load_preset("preset1")
+ gtk_iteration()
+
+ self.controller.update_mapping(name="Empty Mapping")
+ self.throttle() # sorting seems to take a bit
+
+ # "Empty Mapping" < "Escape" so we still expect this to be the first row
+ row = self.selection_label_listbox.get_selected_row()
+ self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
+
+ # now create a real empty mapping
+ self.controller.create_mapping()
+ self.throttle()
- def test_remove_selection_label(self):
- """Comprehensive test for selection_labels 2."""
-
- def remove(
- selection_label,
- code,
- symbol,
- num_selection_labels_after,
- target="keyboard",
- ):
- """Remove a selection_label by clicking the delete button.
-
- Parameters
- ----------
- selection_label : SelectionLabel
- code : int or None
- keycode of the mapping that is associated with this selection_label
- symbol : string
- ouptut of the mapping that is associated with this selection_label
- num_selection_labels_after : int
- after deleting, how many selection_labels are expected to still be there
- target :
- selected target in target_selector
- """
- self.selection_label_listbox.select_row(selection_label)
-
- if code is not None and symbol is not None:
- self.assertEqual(
- active_preset.get_mapping(EventCombination([EV_KEY, code, 1])),
- (symbol, target),
- )
-
- if symbol is not None:
- self.assertEqual(self.editor.get_symbol_input_text(), symbol)
-
- self.assertEqual(self.editor.get_target_selection(), target)
-
- if code is None:
- self.assertIsNone(selection_label.get_combination())
- else:
- self.assertEqual(
- selection_label.get_combination(),
- EventCombination([EV_KEY, code, 1]),
- )
-
- with PatchedConfirmDelete(self.user_interface):
- self.editor._on_delete_button_clicked()
-
- time.sleep(0.2)
- gtk_iteration()
+ # for some reason we no longer can use assertIs maybe a gtk bug?
+ # self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
- # if a reference to the selection_label is held somewhere and it is
- # accidentally used again, make sure to not provide any outdated
- # information that is supposed to be deleted
- self.assertIsNone(selection_label.get_combination())
- if code is not None:
- self.assertIsNone(
- active_preset.get_mapping(EventCombination([EV_KEY, code, 1])),
- )
-
- self.assertEqual(
- len(self.get_selection_labels()),
- num_selection_labels_after,
- )
-
- # sleeps are added to be able to visually follow and debug the test. Add two
- # selection_labels by modifiying the one empty selection_label that exists
- selection_label_1 = self.add_mapping_via_ui(
- EventCombination([EV_KEY, 10, 1]),
- "a",
- )
- selection_label_2 = self.add_mapping_via_ui(
- EventCombination([EV_KEY, 11, 1]),
- "b",
+ # we expect the fake empty mapping in row 0 and the real one in row 2
+ self.selection_label_listbox.select_row(
+ self.selection_label_listbox.get_row_at_index(0)
)
-
- # no empty selection_label added because one is unfinished
- time.sleep(0.2)
gtk_iteration()
- self.assertEqual(len(self.get_selection_labels()), 3)
+ self.assertEqual(self.data_manager.active_mapping.name, "Empty Mapping")
+ self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
+ self.selection_label_listbox.select_row(
+ self.selection_label_listbox.get_row_at_index(2)
+ )
+ self.assertIsNone(self.data_manager.active_mapping.name)
self.assertEqual(
- active_preset.get_mapping(EventCombination([EV_KEY, 11, 1])),
- ("b", "keyboard"),
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.empty_combination(),
)
- remove(selection_label_1, 10, "a", 2)
- remove(selection_label_2, 11, "b", 1)
+ def test_remove_mapping(self):
+ self.controller.load_preset("preset1")
+ gtk_iteration()
+ self.assertEqual(len(self.data_manager.active_preset), 2)
+ self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
+
+ with PatchedConfirmDelete(self.user_interface):
+ self.delete_mapping_btn.clicked()
+ gtk_iteration()
- # there is no empty selection_label at the moment, so after removing that one,
- # which is the only selection_label, one empty selection_label will be there.
- # So the number of selection_labels won't change.
- remove(self.selection_label_listbox.get_children()[-1], None, None, 1)
+ self.assertEqual(len(self.data_manager.active_preset), 1)
+ self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
def test_problematic_combination(self):
- combination = EventCombination((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1))
- self.add_mapping_via_ui(combination, "b")
+ # load a device with more capabilities
+ self.controller.load_group("Foo Device 2")
+ gtk_iteration()
+
+ def add_mapping(combi: EventCombination, symbol):
+ self.controller.create_mapping()
+ gtk_iteration()
+ self.controller.start_key_recording()
+ push_events("Foo Device 2", [event for event in combi])
+ push_events("Foo Device 2", [event.modify(value=0) for event in combi])
+ self.throttle(20)
+ gtk_iteration()
+ self.code_editor.get_buffer().set_text(symbol)
+ gtk_iteration()
+
+ combination = EventCombination(((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)))
+ add_mapping(combination, "b")
text = self.get_status_text()
self.assertIn("shift", text)
@@ -1228,421 +1140,204 @@ class TestGui(GuiTestBase):
self.assertTrue(warning_icon.get_visible())
def test_rename_and_save(self):
- self.assertEqual(self.user_interface.group.name, "Foo Device")
- self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset"))
+ # only a basic test, TestController and TestDataManager go more in detail
+ self.rename_input.set_text("foo")
+ self.rename_btn.clicked()
+ gtk_iteration()
- m1 = get_ui_mapping()
- active_preset.add(m1)
- self.assertEqual(self.user_interface.preset_name, "new preset")
- self.user_interface.save_preset()
- self.assertEqual(
- active_preset.get_mapping(EventCombination([99, 99, 99])),
- m1,
- )
- global_config.set_autoload_preset("Foo Device", "new preset")
- self.assertTrue(global_config.is_autoloaded("Foo Device", "new preset"))
-
- m2 = get_ui_mapping()
- m2.output_symbol = "b"
- active_preset.get_mapping(EventCombination([99, 99, 99])).output_symbol = "b"
- self.user_interface.get("preset_name_input").set_text("asdf")
- self.user_interface.save_preset()
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(self.user_interface.preset_name, "asdf")
- preset_path = f"{CONFIG_PATH}/presets/Foo Device/asdf.json"
+ preset_path = f"{CONFIG_PATH}/presets/Foo Device/foo.json"
self.assertTrue(os.path.exists(preset_path))
- self.assertEqual(
- active_preset.get_mapping(EventCombination([99, 99, 99])),
- m2,
- )
-
- # after renaming the preset it is still set to autoload
- self.assertTrue(global_config.is_autoloaded("Foo Device", "asdf"))
- # ALSO IN THE ACTUAL CONFIG FILE!
- global_config.load_config()
- self.assertTrue(global_config.is_autoloaded("Foo Device", "asdf"))
-
error_icon = self.user_interface.get("error_status_icon")
self.assertFalse(error_icon.get_visible())
- # otherwise save won't do anything
- m2.output_symbol = "c"
- active_preset.get_mapping(EventCombination([99, 99, 99])).output_symbol = "c"
- self.assertTrue(active_preset.has_unsaved_changes())
-
def save():
raise PermissionError
- with patch.object(active_preset, "save", save):
- self.user_interface.save_preset()
- status = self.get_status_text()
- self.assertIn("Permission denied", status)
+ with patch.object(self.data_manager.active_preset, "save", save):
+ self.code_editor.get_buffer().set_text("f")
+ gtk_iteration()
+ status = self.get_status_text()
+ self.assertIn("Permission denied", status)
with PatchedConfirmDelete(self.user_interface):
- self.user_interface.on_delete_preset_clicked(None)
- self.assertFalse(os.path.exists(preset_path))
-
- def test_rename_create_switch(self):
- # after renaming a preset and saving it, new presets
- # start with "new preset" again
- m1 = get_ui_mapping()
- active_preset.add(m1)
- self.user_interface.get("preset_name_input").set_text("asdf")
- self.user_interface.save_preset()
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(len(active_preset), 1)
- self.assertEqual(self.user_interface.preset_name, "asdf")
-
- self.user_interface.on_create_preset_clicked()
- self.assertEqual(self.user_interface.preset_name, "new preset")
- self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
- self.assertEqual(len(active_preset), 0)
- self.user_interface.save_preset()
-
- # symbol and code in the gui won't be carried over after selecting a preset
- combination = EventCombination([EV_KEY, 15, 1])
- self.set_combination(combination)
- self.editor.set_symbol_input_text("b")
-
- # selecting the first preset again loads the saved mapping, and saves
- # the current changes in the gui
- self.user_interface.on_select_preset(FakePresetDropdown("asdf"))
- self.assertEqual(
- active_preset.get_mapping(EventCombination([99, 99, 99])),
- m1,
- )
- self.assertEqual(len(active_preset), 1)
- self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
- global_config.set_autoload_preset("Foo Device", "new preset")
-
- # renaming a preset to an existing name appends a number
- self.user_interface.on_select_preset(FakePresetDropdown("new preset"))
- self.user_interface.get("preset_name_input").set_text("asdf")
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(self.user_interface.preset_name, "asdf 2")
- # and that added number is correctly used in the autoload
- # configuration as well
- self.assertTrue(global_config.is_autoloaded("Foo Device", "asdf 2"))
- m2 = get_ui_mapping()
- m2.event_combination = "1,15,1"
- m2.output_symbol = "b"
- self.assertEqual(
- active_preset.get_mapping(EventCombination([EV_KEY, 15, 1])).dict(),
- m2.dict(),
- )
- self.assertEqual(len(active_preset), 1)
- self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
-
- self.assertEqual(self.user_interface.get("preset_name_input").get_text(), "")
-
- # renaming the current preset to itself doesn't append a number and
- # it doesn't do anything on the file system
- def _raise(*_):
- # should not get called
- raise AssertionError
-
- with patch.object(os, "rename", _raise):
- self.user_interface.get("preset_name_input").set_text("asdf 2")
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(self.user_interface.preset_name, "asdf 2")
-
- self.user_interface.get("preset_name_input").set_text("")
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(self.user_interface.preset_name, "asdf 2")
+ self.delete_preset_btn.clicked()
+ gtk_iteration()
+ self.assertFalse(os.path.exists(preset_path))
def test_check_for_unknown_symbols(self):
status = self.user_interface.get("status_bar")
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
- active_preset.change(EventCombination([EV_KEY, 71, 1]), "keyboard", "qux", None)
- active_preset.change(EventCombination([EV_KEY, 72, 1]), "keyboard", "foo", None)
- self.user_interface.save_preset()
+ self.controller.load_preset("preset1")
+ self.throttle()
+ self.controller.load_mapping(EventCombination.from_string("1,1,1"))
+ gtk_iteration()
+ self.controller.update_mapping(output_symbol="foo")
+ gtk_iteration()
+ self.controller.load_mapping(EventCombination.from_string("1,2,1"))
+ gtk_iteration()
+ self.controller.update_mapping(output_symbol="qux")
+ gtk_iteration()
+
tooltip = status.get_tooltip_text().lower()
self.assertIn("qux", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
# it will still save it though
- with open(get_preset_path("Foo Device", "new preset")) as f:
+ with open(get_preset_path("Foo Device", "preset1")) as f:
content = f.read()
self.assertIn("qux", content)
self.assertIn("foo", content)
- active_preset.change(EventCombination([EV_KEY, 71, 1]), "keyboard", "a", None)
- self.user_interface.save_preset()
+ self.controller.update_mapping(output_symbol="a")
+ gtk_iteration()
tooltip = status.get_tooltip_text().lower()
self.assertIn("foo", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
- active_preset.change(EventCombination([EV_KEY, 72, 1]), "keyboard", "b", None)
- self.user_interface.save_preset()
+ self.controller.load_mapping(EventCombination.from_string("1,1,1"))
+ gtk_iteration()
+ self.controller.update_mapping(output_symbol="b")
+ gtk_iteration()
tooltip = status.get_tooltip_text()
self.assertIsNone(tooltip)
self.assertFalse(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
def test_check_macro_syntax(self):
- status = self.user_interface.get("status_bar")
+ status = self.status_bar
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
- active_preset.change(
- EventCombination([EV_KEY, 9, 1]),
- "keyboard",
- "k(1))",
- None,
- )
- self.user_interface.save_preset()
+ self.code_editor.get_buffer().set_text("k(1))")
+ gtk_iteration()
tooltip = status.get_tooltip_text().lower()
self.assertIn("brackets", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
- active_preset.change(EventCombination([EV_KEY, 9, 1]), "keyboard", "k(1)", None)
- self.user_interface.save_preset()
+ self.code_editor.get_buffer().set_text("k(1)")
+ gtk_iteration()
tooltip = (status.get_tooltip_text() or "").lower()
self.assertNotIn("brackets", tooltip)
self.assertFalse(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
self.assertEqual(
- active_preset.get_mapping(EventCombination([EV_KEY, 9, 1])),
- ("k(1)", "keyboard"),
+ self.data_manager.active_mapping.output_symbol,
+ "k(1)",
)
- def test_debounce_check_on_typing(self):
+ def test_check_on_typing(self):
status = self.user_interface.get("status_bar")
- status.set_tooltip_text(None)
error_icon = self.user_interface.get("error_status_icon")
warning_icon = self.user_interface.get("warning_status_icon")
- self.add_mapping_via_ui(EventCombination([EV_KEY, 10, 1]), "")
- gtk_iteration()
tooltip = status.get_tooltip_text()
# nothing wrong yet
self.assertIsNone(tooltip)
# now change the mapping by typing into the field
- buffer = self.editor.get_text_input().get_buffer()
+ buffer = self.code_editor.get_buffer()
buffer.set_text("sdfgkj()")
- self.throttle()
- # debouncing, still nothing shown
- tooltip = status.get_tooltip_text()
- self.assertIsNone(tooltip)
-
- # after 510 ms the debouncing should have been triggered a syntax check
- time.sleep(0.51)
gtk_iteration()
+ # the mapping is immediately validated
tooltip = status.get_tooltip_text()
self.assertIn("Unknown function sdfgkj", tooltip)
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
- self.assertEqual(self.editor.get_symbol_input_text(), "sdfgkj()")
-
- def test_select_device_and_preset(self):
- foo_device_path = f"{CONFIG_PATH}/presets/Foo Device"
- key_10 = EventCombination([EV_KEY, 10, 1])
- key_11 = EventCombination([EV_KEY, 11, 1])
-
- # created on start because the first device is selected and some empty
- # preset prepared.
- self.assertTrue(os.path.exists(f"{foo_device_path}/new preset.json"))
- self.assertEqual(self.user_interface.group.name, "Foo Device")
- self.assertEqual(self.user_interface.preset_name, "new preset")
- # change it to check if the gui loads presets correctly later
- self.editor.set_combination(key_10)
- self.editor.set_symbol_input_text("a")
+ self.assertEqual(self.data_manager.active_mapping.output_symbol, "sdfgkj()")
- # create another one
- self.user_interface.on_create_preset_clicked()
+ def test_select_device(self):
+ # simple test to make sure we can switch between devices
+ # more detailed tests in TestController and TestDataManager
+ self.device_selection.set_active_id("Bar Device")
gtk_iteration()
- self.assertTrue(os.path.exists(f"{foo_device_path}/new preset.json"))
- self.assertTrue(os.path.exists(f"{foo_device_path}/new preset 2.json"))
- self.assertEqual(self.user_interface.preset_name, "new preset 2")
- self.assertEqual(len(active_preset), 0)
- # this should not be loaded when "new preset" is selected, because it belongs
- # to "new preset 2":
- self.editor.set_combination(key_11)
- self.editor.set_symbol_input_text("a")
- # select the first one again
- self.user_interface.on_select_preset(FakePresetDropdown("new preset"))
+ entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
+ self.assertEqual(entries, {"new preset"})
+
+ self.device_selection.set_active_id("Foo Device")
gtk_iteration()
- self.assertEqual(self.user_interface.preset_name, "new preset")
- self.assertEqual(len(active_preset), 1)
- self.assertEqual(active_preset.get_mapping(key_10), ("a", "keyboard"))
+ entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
+ self.assertEqual(entries, {"preset1", "preset2", "preset3"})
- self.assertListEqual(
- sorted(os.listdir(f"{foo_device_path}")),
- sorted(["new preset.json", "new preset 2.json"]),
+ # make sure a preset and mapping was loaded
+ self.assertIsNotNone(self.data_manager.active_preset)
+ self.assertEqual(
+ self.data_manager.active_preset.name, self.preset_selection.get_active_id()
+ )
+ self.assertIsNotNone(self.data_manager.active_mapping)
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ self.selection_label_listbox.get_selected_row().combination,
)
- """now try to change the name"""
-
- self.user_interface.get("preset_name_input").set_text("abc 123")
+ def test_select_preset(self):
+ # simple test to make sure we can switch between presets
+ # more detailed tests in TestController and TestDataManager
+ self.device_selection.set_active_id("Foo Device 2")
+ gtk_iteration()
+ self.preset_selection.set_active_id("preset1")
gtk_iteration()
- self.assertEqual(self.user_interface.preset_name, "new preset")
- self.assertFalse(os.path.exists(f"{foo_device_path}/abc 123.json"))
-
- # putting new information into the editor does not lead to some weird
- # problems. when doing the rename everything will be saved and then moved
- # to the new path
- self.editor.set_combination(EventCombination([EV_KEY, 10, 1]))
- self.editor.set_symbol_input_text("1")
- self.assertEqual(self.user_interface.preset_name, "new preset")
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(self.user_interface.preset_name, "abc 123")
+ mappings = {
+ row.combination for row in self.selection_label_listbox.get_children()
+ }
+ self.assertEqual(
+ mappings,
+ {
+ EventCombination.from_string("1,1,1"),
+ EventCombination.from_string("1,2,1"),
+ },
+ )
+ self.assertFalse(self.autoload_toggle.get_active())
+ self.preset_selection.set_active_id("preset2")
gtk_iteration()
- self.assertEqual(self.user_interface.preset_name, "abc 123")
- self.assertTrue(os.path.exists(f"{foo_device_path}/abc 123.json"))
- self.assertListEqual(
- sorted(os.listdir(os.path.join(CONFIG_PATH, "presets"))),
- sorted(["Foo Device"]),
- )
- self.assertListEqual(
- sorted(os.listdir(f"{foo_device_path}")),
- sorted(["abc 123.json", "new preset 2.json"]),
+
+ mappings = {
+ row.combination for row in self.selection_label_listbox.get_children()
+ }
+ self.assertEqual(
+ mappings,
+ {
+ EventCombination.from_string("1,3,1"),
+ EventCombination.from_string("1,4,1"),
+ },
)
+ self.assertTrue(self.autoload_toggle.get_active())
def test_copy_preset(self):
- selection_labels = self.selection_label_listbox
- self.add_mapping_via_ui(EventCombination([EV_KEY, 81, 1]), "a")
- time.sleep(0.1)
- gtk_iteration()
- self.user_interface.save_preset()
- # 2 selection_labels: the changed selection_label and an empty selection_label
- self.assertEqual(len(selection_labels.get_children()), 2)
-
- # should be cleared when creating a new preset
- active_preset.set("a.b", 3)
- self.assertEqual(active_preset.get("a.b"), 3)
-
- self.user_interface.on_create_preset_clicked()
-
- # the preset should be empty, only one empty selection_label present
- self.assertEqual(len(selection_labels.get_children()), 1)
- self.assertIsNone(active_preset.get("a.b"))
-
- # add one new selection_label again and a setting
- self.add_mapping_via_ui(EventCombination([EV_KEY, 81, 1]), "b")
- time.sleep(0.1)
- gtk_iteration()
- self.user_interface.save_preset()
- self.assertEqual(len(selection_labels.get_children()), 2)
- active_preset.set(["foo", "bar"], 2)
-
- # this time it should be copied
- self.user_interface.on_copy_preset_clicked()
- self.assertEqual(self.user_interface.preset_name, "new preset 2 copy")
- self.assertEqual(len(selection_labels.get_children()), 2)
- self.assertEqual(self.editor.get_symbol_input_text(), "b")
- self.assertEqual(active_preset.get(["foo", "bar"]), 2)
-
- # make another copy
- self.user_interface.on_copy_preset_clicked()
- self.assertEqual(self.user_interface.preset_name, "new preset 2 copy 2")
- self.assertEqual(len(selection_labels.get_children()), 2)
- self.assertEqual(self.editor.get_symbol_input_text(), "b")
- self.assertEqual(len(active_preset), 1)
- self.assertEqual(active_preset.get("foo.bar"), 2)
-
- def test_gamepad_config(self):
- # set some stuff in the beginning, otherwise gtk fails to
- # do handler_unblock_by_func, which makes no sense at all.
- # but it ONLY fails on right_joystick_purpose for some reason,
- # unblocking the left one works just fine. I should open a bug report
- # on gtk or something probably.
- self.user_interface.get("left_joystick_purpose").set_active_id(BUTTONS)
- self.user_interface.get("right_joystick_purpose").set_active_id(BUTTONS)
- self.user_interface.get("joystick_mouse_speed").set_value(1)
- active_preset.set_has_unsaved_changes(False)
-
- # select a device that is not a gamepad
- self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device"))
- self.assertFalse(self.user_interface.get("gamepad_config").is_visible())
- self.assertFalse(active_preset.has_unsaved_changes())
-
- # select a gamepad
- self.user_interface.on_select_device(FakeDeviceDropdown("gamepad"))
- self.assertTrue(self.user_interface.get("gamepad_config").is_visible())
- self.assertFalse(active_preset.has_unsaved_changes())
-
- # set stuff
- gtk_iteration()
- self.user_interface.get("left_joystick_purpose").set_active_id(WHEEL)
- self.user_interface.get("right_joystick_purpose").set_active_id(WHEEL)
- joystick_mouse_speed = 5
- self.user_interface.get("joystick_mouse_speed").set_value(joystick_mouse_speed)
-
- # it should be stored in active_preset, which overwrites the
- # global_config
- global_config.set("gamepad.joystick.left_purpose", MOUSE)
- global_config.set("gamepad.joystick.right_purpose", MOUSE)
- global_config.set("gamepad.joystick.pointer_speed", 50)
- self.assertTrue(active_preset.has_unsaved_changes())
- left_purpose = active_preset.get("gamepad.joystick.left_purpose")
- right_purpose = active_preset.get("gamepad.joystick.right_purpose")
- pointer_speed = active_preset.get("gamepad.joystick.pointer_speed")
- self.assertEqual(left_purpose, WHEEL)
- self.assertEqual(right_purpose, WHEEL)
- self.assertEqual(pointer_speed, 2**joystick_mouse_speed)
-
- # select a device that is not a gamepad again
- self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device"))
- self.assertFalse(self.user_interface.get("gamepad_config").is_visible())
- self.assertFalse(active_preset.has_unsaved_changes())
-
- def test_wont_start(self):
- error_icon = self.user_interface.get("error_status_icon")
- preset_name = "foo preset"
- group_name = "Bar Device"
- self.user_interface.preset_name = preset_name
- self.user_interface.group = groups.find(name=group_name)
+ # simple tests to ensure it works
+ # more detailed tests in TestController and TestDataManager
- # empty
+ # check the initial state
+ entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
+ self.assertEqual(entries, {"preset1", "preset2", "preset3"})
+ self.assertEqual(self.preset_selection.get_active_id(), "preset3")
- active_preset.empty()
- self.user_interface.save_preset()
- self.user_interface.on_apply_preset_clicked(None)
- text = self.get_status_text()
- self.assertIn("add keys", text)
- self.assertTrue(error_icon.get_visible())
- self.assertNotEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING
- )
+ self.copy_preset_btn.clicked()
+ gtk_iteration()
+ entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
+ self.assertEqual(entries, {"preset1", "preset2", "preset3", "preset3 copy"})
+ self.assertEqual(self.preset_selection.get_active_id(), "preset3 copy")
- # not empty, but keys are held down
+ self.copy_preset_btn.clicked()
+ gtk_iteration()
- active_preset.change(EventCombination([EV_KEY, KEY_A, 1]), "keyboard", "a")
- self.user_interface.save_preset()
- send_event_to_reader(new_event(EV_KEY, KEY_A, 1))
- reader.read()
- self.assertEqual(len(reader._unreleased), 1)
- self.assertFalse(self.user_interface.unreleased_warn)
- self.user_interface.on_apply_preset_clicked(None)
- text = self.get_status_text()
- self.assertIn("release", text)
- self.assertTrue(error_icon.get_visible())
- self.assertNotEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING
- )
- self.assertTrue(self.user_interface.unreleased_warn)
- self.assertEqual(
- self.user_interface.get("apply_system_layout").get_opacity(), 0.4
- )
+ entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
self.assertEqual(
- self.user_interface.get("key_recording_toggle").get_opacity(), 1
+ entries, {"preset1", "preset2", "preset3", "preset3 copy", "preset3 copy 2"}
)
- # device grabbing fails
-
+ def test_wont_start(self):
def wait():
"""Wait for the injector process to finish doing stuff."""
for _ in range(10):
@@ -1651,11 +1346,28 @@ class TestGui(GuiTestBase):
if "Starting" not in self.get_status_text():
return
+ error_icon = self.user_interface.get("error_status_icon")
+ self.controller.load_group("Bar Device")
+
+ # empty
+ self.start_injector_btn.clicked()
+ gtk_iteration()
+ wait()
+ text = self.get_status_text()
+ self.assertIn("add keys", text)
+ self.assertTrue(error_icon.get_visible())
+ self.assertNotEqual(self.daemon.get_state("Bar Device"), RUNNING)
+
+ # device grabbing fails
+ self.controller.load_group("Foo Device 2")
+ gtk_iteration()
+
for i in range(2):
# just pressing apply again will overwrite the previous error
self.grab_fails = True
- self.user_interface.on_apply_preset_clicked(None)
- self.assertFalse(self.user_interface.unreleased_warn)
+ self.start_injector_btn.clicked()
+ gtk_iteration()
+
text = self.get_status_text()
# it takes a little bit of time
self.assertIn("Starting injection", text)
@@ -1664,21 +1376,13 @@ class TestGui(GuiTestBase):
text = self.get_status_text()
self.assertIn("not grabbed", text)
self.assertTrue(error_icon.get_visible())
- self.assertNotEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key),
- RUNNING,
- )
-
- # for the second try, release the key. that should also work
- send_event_to_reader(new_event(EV_KEY, KEY_A, 0))
- reader.read()
- self.assertEqual(len(reader._unreleased), 0)
+ self.assertNotEqual(self.daemon.get_state("Foo Device 2"), RUNNING)
# this time work properly
self.grab_fails = False
- active_preset.save(get_preset_path(group_name, preset_name))
- self.user_interface.on_apply_preset_clicked(None)
+ self.start_injector_btn.clicked()
+ gtk_iteration()
text = self.get_status_text()
self.assertIn("Starting injection", text)
self.assertFalse(error_icon.get_visible())
@@ -1688,27 +1392,19 @@ class TestGui(GuiTestBase):
text = self.get_status_text()
self.assertNotIn("CTRL + DEL", text) # only shown if btn_left mapped
self.assertFalse(error_icon.get_visible())
- self.assertEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING
- )
-
- self.assertEqual(
- self.user_interface.get("apply_system_layout").get_opacity(), 1
- )
- self.assertEqual(
- self.user_interface.get("key_recording_toggle").get_opacity(), 0.4
- )
+ self.assertEqual(self.daemon.get_state("Foo Device 2"), RUNNING)
- # because this test managed to reproduce some minor bug:
- # The mapping is supposed to be in active_preset._mapping, not in _config.
- # For reasons I don't remember.
- self.assertNotIn("mapping", active_preset._config)
+ def test_start_with_btn_left(self):
+ self.controller.load_group("Foo Device 2")
+ gtk_iteration()
- def test_wont_start_2(self):
- preset_name = "foo preset"
- group_name = "Bar Device"
- self.user_interface.preset_name = preset_name
- self.user_interface.group = groups.find(name=group_name)
+ self.controller.create_mapping()
+ gtk_iteration()
+ self.controller.update_mapping(
+ event_combination=EventCombination(InputEvent.btn_left()),
+ output_symbol="a",
+ )
+ gtk_iteration()
def wait():
"""Wait for the injector process to finish doing stuff."""
@@ -1718,62 +1414,37 @@ class TestGui(GuiTestBase):
if "Starting" not in self.get_status_text():
return
- # btn_left mapped
- active_preset.change(EventCombination(InputEvent.btn_left()), "keyboard", "a")
- self.user_interface.save_preset()
-
- # and combination held down
- send_event_to_reader(new_event(EV_KEY, KEY_A, 1))
- reader.read()
- self.assertEqual(len(reader._unreleased), 1)
- self.assertFalse(self.user_interface.unreleased_warn)
-
# first apply, shows btn_left warning
- self.user_interface.on_apply_preset_clicked(None)
+ self.start_injector_btn.clicked()
+ gtk_iteration()
text = self.get_status_text()
self.assertIn("click", text)
- self.assertEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), UNKNOWN
- )
-
- # second apply, shows unreleased warning
- self.user_interface.on_apply_preset_clicked(None)
- text = self.get_status_text()
- self.assertIn("release", text)
- self.assertEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), UNKNOWN
- )
+ self.assertEqual(self.daemon.get_state("Foo Device 2"), UNKNOWN)
- # third apply, overwrites both warnings
- self.user_interface.on_apply_preset_clicked(None)
+ # second apply, overwrites
+ self.start_injector_btn.clicked()
+ gtk_iteration()
wait()
- self.assertEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING
- )
+ self.assertEqual(self.daemon.get_state("Foo Device 2"), RUNNING)
text = self.get_status_text()
# because btn_left is mapped, shows help on how to stop
# injecting via the keyboard
self.assertIn("CTRL + DEL", text)
- def test_can_modify_mapping(self):
- preset_name = "foo preset"
- group_name = "Bar Device"
- self.user_interface.preset_name = preset_name
- self.user_interface.group = groups.find(name=group_name)
+ def test_cannot_record_keys(self):
+ self.controller.load_group("Foo Device 2")
+ self.assertNotEqual(self.data_manager.get_state(), RUNNING)
+ self.assertNotIn("Stop Injection", self.get_status_text())
- self.assertNotEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING
- )
- self.user_interface.can_modify_preset()
- text = self.get_status_text()
- self.assertNotIn("Stop Injection", text)
- active_preset.path = get_preset_path(group_name, preset_name)
- active_preset.add(
- get_ui_mapping(EventCombination([EV_KEY, KEY_A, 1]), "keyboard", "b"),
- )
- active_preset.save()
- self.user_interface.on_apply_preset_clicked(None)
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
+ self.assertTrue(self.recording_toggle.get_active())
+ self.controller.stop_key_recording()
+ gtk_iteration()
+ self.assertFalse(self.recording_toggle.get_active())
+ self.start_injector_btn.clicked()
+ gtk_iteration()
# wait for the injector to start
for _ in range(10):
time.sleep(0.1)
@@ -1781,376 +1452,314 @@ class TestGui(GuiTestBase):
if "Starting" not in self.get_status_text():
break
- self.assertEqual(
- self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING
- )
-
- # the preset cannot be changed anymore
- self.assertFalse(self.user_interface.can_modify_preset())
+ self.assertEqual(self.data_manager.get_state(), RUNNING)
# the toggle button should reset itself shortly
- self.user_interface.editor.get_recording_toggle().set_active(True)
+ self.recording_toggle.set_active(True)
+ gtk_iteration()
+ self.assertFalse(self.recording_toggle.get_active())
+ text = self.get_status_text()
+ self.assertIn("Stop Injection", text)
+
+ def test_start_injecting(self):
+ self.controller.load_group("Foo Device 2")
+
+ with spy(self.daemon, "set_config_dir") as spy1:
+ with spy(self.daemon, "start_injecting") as spy2:
+ self.start_injector_btn.clicked()
+ gtk_iteration()
+ # correctly uses group.key, not group.name
+ spy2.assert_called_once_with("Foo Device 2", "preset3")
+
+ spy1.assert_called_once_with(get_config_path())
+
for _ in range(10):
time.sleep(0.1)
gtk_iteration()
- if not self.user_interface.editor.get_recording_toggle().get_active():
+ if self.data_manager.get_state() == RUNNING:
break
- self.assertFalse(self.user_interface.editor.get_recording_toggle().get_active())
- text = self.get_status_text()
- self.assertIn("Stop Injection", text)
-
- def test_start_injecting(self):
- keycode_from = 9
- keycode_to = 200
+ # fail here so we don't block forever
+ self.assertEqual(self.data_manager.get_state(), RUNNING)
- self.add_mapping_via_ui(EventCombination([EV_KEY, keycode_from, 1]), "a")
- system_mapping.clear()
- system_mapping._set("a", keycode_to)
+ # this is a stupid workaround for the bad test fixtures
+ # by switching the group we make sure that the helper no longer listens for
+ # events on "Foo Device 2" otherwise we would have two processes
+ # (helper and injector) reading the same pipe which can block this test
+ # indefinitely
+ self.controller.load_group("Foo Device")
+ gtk_iteration()
push_events(
"Foo Device 2",
[
- new_event(evdev.events.EV_KEY, keycode_from, 1),
- new_event(evdev.events.EV_KEY, keycode_from, 0),
+ new_event(evdev.events.EV_KEY, 5, 1),
+ new_event(evdev.events.EV_KEY, 5, 0),
],
)
- # injecting for group.key will look at paths containing group.name
- active_preset.save(get_preset_path("Foo Device", "foo preset"))
-
- # use only the manipulated system_mapping
- if os.path.exists(os.path.join(tmp, XMODMAP_FILENAME)):
- os.remove(os.path.join(tmp, XMODMAP_FILENAME))
-
- # select the second Foo device
- self.user_interface.group = groups.find(key="Foo Device 2")
-
- with spy(self.user_interface.dbus, "set_config_dir") as spy1:
- self.user_interface.preset_name = "foo preset"
-
- with spy(self.user_interface.dbus, "start_injecting") as spy2:
- self.user_interface.on_apply_preset_clicked(None)
- # correctly uses group.key, not group.name
- spy2.assert_called_once_with("Foo Device 2", "foo preset")
-
- spy1.assert_called_once_with(get_config_path())
-
- # the integration tests will cause the injection to be started as
- # processes, as intended. Luckily, recv will block until the events
- # are handled and pushed.
-
- # Note, that appending events to pending_events won't work anymore
- # from here on because the injector processes memory cannot be
- # modified from here.
-
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, evdev.events.EV_KEY)
- self.assertEqual(event.code, keycode_to)
+ self.assertEqual(event.code, KEY_A)
self.assertEqual(event.value, 1)
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, evdev.events.EV_KEY)
- self.assertEqual(event.code, keycode_to)
+ self.assertEqual(event.code, KEY_A)
self.assertEqual(event.value, 0)
# the input-remapper device will not be shown
- groups.refresh()
- self.user_interface.populate_devices()
- for entry in self.user_interface.device_store:
+ self.controller.refresh_groups()
+ gtk_iteration()
+
+ for entry in self.device_selection.get_child().get_model():
# whichever attribute contains "input-remapper"
self.assertNotIn("input-remapper", "".join(entry))
- def test_gamepad_purpose_mouse_and_button(self):
- self.user_interface.on_select_device(FakeDeviceDropdown("gamepad"))
- self.user_interface.get("right_joystick_purpose").set_active_id(MOUSE)
- self.user_interface.get("left_joystick_purpose").set_active_id(BUTTONS)
- self.user_interface.get("joystick_mouse_speed").set_value(6)
+ def test_stop_injecting(self):
+ self.controller.load_group("Foo Device 2")
+ self.start_injector_btn.clicked()
gtk_iteration()
- speed = active_preset.get("gamepad.joystick.pointer_speed")
- active_preset.set("gamepad.joystick.non_linearity", 1)
- self.assertEqual(speed, 2**6)
-
- # don't consume the events in the reader, they are used to test
- # the injection
- reader.terminate()
- time.sleep(0.1)
-
- push_events(
- "gamepad",
- [new_event(EV_ABS, ABS_RX, MIN_ABS), new_event(EV_ABS, ABS_X, MAX_ABS)]
- * 100,
- )
- active_preset.change(EventCombination([EV_ABS, ABS_X, 1]), "keyboard", "a")
- self.user_interface.save_preset()
+ for _ in range(10):
+ time.sleep(0.1)
+ gtk_iteration()
+ if self.data_manager.get_state() == RUNNING:
+ break
+ # fail here so we don't block forever
+ self.assertEqual(self.data_manager.get_state(), RUNNING)
+ # stupid fixture workaround
+ self.controller.load_group("Foo Device")
gtk_iteration()
- self.user_interface.on_apply_preset_clicked(None)
- time.sleep(0.3)
-
- history = []
- while uinput_write_history_pipe[0].poll():
- history.append(uinput_write_history_pipe[0].recv().t)
-
- count_mouse = history.count((EV_REL, REL_X, -speed))
- count_button = history.count((EV_KEY, KEY_A, 1))
- self.assertGreater(count_mouse, 1)
- self.assertEqual(count_button, 1)
- self.assertEqual(count_button + count_mouse, len(history))
-
- self.assertIn("gamepad", self.user_interface.dbus.injectors)
-
- def test_stop_injecting(self):
- keycode_from = 16
- keycode_to = 90
-
- self.add_mapping_via_ui(EventCombination([EV_KEY, keycode_from, 1]), "t")
- system_mapping.clear()
- system_mapping._set("t", keycode_to)
-
- # not all of those events should be processed, since that takes some
- # time due to time.sleep in the fakes and the injection is stopped.
- push_events("Bar Device", [new_event(1, keycode_from, 1)] * 100)
-
- active_preset.save(get_preset_path("Bar Device", "foo preset"))
-
- self.user_interface.group = groups.find(name="Bar Device")
- self.user_interface.preset_name = "foo preset"
- self.user_interface.on_apply_preset_clicked(None)
-
pipe = uinput_write_history_pipe[0]
- # block until the first event is available, indicating that
- # the injector is ready
- write_history = [pipe.recv()]
+ self.assertFalse(pipe.poll())
- # stop
- self.user_interface.on_stop_injecting_clicked(None)
+ push_events(
+ "Foo Device 2",
+ [
+ new_event(evdev.events.EV_KEY, 5, 1),
+ new_event(evdev.events.EV_KEY, 5, 0),
+ ],
+ )
- # try to receive a few of the events
time.sleep(0.2)
+ self.assertTrue(pipe.poll())
while pipe.poll():
- write_history.append(pipe.recv())
+ pipe.recv()
+
+ self.controller.load_group("Foo Device 2")
+ self.controller.stop_injecting()
+ gtk_iteration()
- len_before = len(write_history)
- self.assertLess(len(write_history), 50)
+ for _ in range(10):
+ time.sleep(0.1)
+ gtk_iteration()
+ if self.data_manager.get_state() == STOPPED:
+ break
+ self.assertEqual(self.data_manager.get_state(), STOPPED)
- # since the injector should not be running anymore, no more events
- # should be received after waiting even more time
+ push_events(
+ "Foo Device 2",
+ [
+ new_event(evdev.events.EV_KEY, 5, 1),
+ new_event(evdev.events.EV_KEY, 5, 0),
+ ],
+ )
time.sleep(0.2)
- while pipe.poll():
- write_history.append(pipe.recv())
- self.assertEqual(len(write_history), len_before)
+ self.assertFalse(pipe.poll())
def test_delete_preset(self):
- self.editor.set_combination(EventCombination([EV_KEY, 71, 1]))
- self.editor.set_symbol_input_text("a")
- self.user_interface.get("preset_name_input").set_text("asdf")
- self.user_interface.on_rename_button_clicked(None)
- gtk_iteration()
- self.assertEqual(self.user_interface.preset_name, "asdf")
- self.assertEqual(len(active_preset), 1)
- self.user_interface.save_preset()
- self.assertTrue(os.path.exists(get_preset_path("Foo Device", "asdf")))
+ # as per test_initial_state we already have preset3 loaded
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3")))
with PatchedConfirmDelete(self.user_interface, Gtk.ResponseType.CANCEL):
- self.user_interface.on_delete_preset_clicked(None)
- self.assertTrue(os.path.exists(get_preset_path("Foo Device", "asdf")))
- self.assertEqual(self.user_interface.preset_name, "asdf")
- self.assertEqual(self.user_interface.group.name, "Foo Device")
-
- with PatchedConfirmDelete(self.user_interface):
- self.user_interface.on_delete_preset_clicked(None)
- self.assertFalse(os.path.exists(get_preset_path("Foo Device", "asdf")))
- self.assertEqual(self.user_interface.preset_name, "new preset")
- self.assertEqual(self.user_interface.group.name, "Foo Device")
-
- def test_populate_devices(self):
- preset_selection = self.user_interface.get("preset_selection")
-
- # create two presets
- self.user_interface.get("preset_name_input").set_text("preset 1")
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(preset_selection.get_active_id(), "preset 1")
-
- # to make sure the next preset has a slightly higher timestamp
- time.sleep(0.1)
- self.user_interface.on_create_preset_clicked()
- self.user_interface.get("preset_name_input").set_text("preset 2")
- self.user_interface.on_rename_button_clicked(None)
- self.assertEqual(preset_selection.get_active_id(), "preset 2")
+ self.delete_preset_btn.clicked()
+ gtk_iteration()
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3")))
+ self.assertEqual(self.data_manager.active_preset.name, "preset3")
+ self.assertEqual(self.data_manager.active_group.name, "Foo Device")
+
+ with PatchedConfirmDelete(self.user_interface):
+ self.delete_preset_btn.clicked()
+ gtk_iteration()
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset3")))
+ self.assertEqual(self.data_manager.active_preset.name, "preset2")
+ self.assertEqual(self.data_manager.active_group.name, "Foo Device")
+
+ def test_refresh_groups(self):
+ # sanity check: preset3 should be the newest
+ self.assertEqual(self.preset_selection.get_active_id(), "preset3")
# select the older one
- preset_selection.set_active_id("preset 1")
- self.assertEqual(self.user_interface.preset_name, "preset 1")
+ self.preset_selection.set_active_id("preset1")
+ gtk_iteration()
+ self.assertEqual(self.data_manager.active_preset.name, "preset1")
# add a device that doesn't exist to the dropdown
unknown_key = "key-1234"
- self.user_interface.device_store.insert(0, [unknown_key, None, "foo"])
+ self.device_selection.get_child().get_model().insert(
+ 0, [unknown_key, None, "foo"]
+ )
- self.user_interface.populate_devices()
+ self.controller.refresh_groups()
+ gtk_iteration()
+ self.throttle(100)
# the newest preset should be selected
- self.assertEqual(self.user_interface.preset_name, "preset 2")
+ self.assertEqual(self.controller.get_a_preset(), "preset3")
+ self.assertEqual(self.data_manager.active_preset.name, "preset3")
# the list contains correct entries
# and the non-existing entry should be removed
- entries = [tuple(entry) for entry in self.user_interface.device_store]
- keys = [entry[0] for entry in self.user_interface.device_store]
+ entries = [
+ tuple(entry) for entry in self.device_selection.get_child().get_model()
+ ]
+ keys = [entry[0] for entry in self.device_selection.get_child().get_model()]
self.assertNotIn(unknown_key, keys)
+
self.assertIn("Foo Device", keys)
self.assertIn(("Foo Device", "input-keyboard", "Foo Device"), entries)
- self.assertIn(("Foo Device 2", "input-mouse", "Foo Device 2"), entries)
+ self.assertIn(("Foo Device 2", "input-gaming", "Foo Device 2"), entries)
self.assertIn(("Bar Device", "input-keyboard", "Bar Device"), entries)
self.assertIn(("gamepad", "input-gaming", "gamepad"), entries)
# it won't crash due to "list index out of range"
# when `types` is an empty list. Won't show an icon
- groups.find(key="Foo Device 2").types = []
- self.user_interface.populate_devices()
+ self.data_manager._reader.groups.find(key="Foo Device 2").types = []
+ self.data_manager._reader.send_groups()
+ gtk_iteration()
self.assertIn(
("Foo Device 2", None, "Foo Device 2"),
- [tuple(entry) for entry in self.user_interface.device_store],
+ [tuple(entry) for entry in self.device_selection.get_child().get_model()],
)
def test_shared_presets(self):
# devices with the same name (but different key because the key is
# unique) share the same presets.
# Those devices would usually be of the same model of keyboard for example
+ # Todo: move this to unit tests, there is no point in having the ui around
+ self.controller.load_group("Foo Device")
+ presets1 = self.data_manager.get_preset_names()
+ self.controller.load_group("Foo Device 2")
+ gtk_iteration()
+ presets2 = self.data_manager.get_preset_names()
+ self.controller.load_group("Bar Device")
+ gtk_iteration()
+ presets3 = self.data_manager.get_preset_names()
- # 1. create a preset
- self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device 2"))
- self.user_interface.on_create_preset_clicked()
- self.add_mapping_via_ui(EventCombination([3, 2, 1]), "qux")
- self.user_interface.get("preset_name_input").set_text("asdf")
- self.user_interface.on_rename_button_clicked(None)
- self.user_interface.save_preset()
- self.assertIn("asdf.json", os.listdir(get_preset_path("Foo Device")))
-
- # 2. switch to the different device, there should be no preset named asdf
- self.user_interface.on_select_device(FakeDeviceDropdown("Bar Device"))
- self.assertEqual(self.user_interface.preset_name, "new preset")
- self.assertNotIn("asdf.json", os.listdir(get_preset_path("Bar Device")))
- self.assertEqual(self.editor.get_symbol_input_text(), "")
-
- # 3. switch to the device with the same name as the first one
- self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device"))
- # the newest preset is asdf, it should be automatically selected
- self.assertEqual(self.user_interface.preset_name, "asdf")
- self.assertEqual(self.editor.get_symbol_input_text(), "qux")
+ self.assertEqual(presets1, presets2)
+ self.assertNotEqual(presets1, presets3)
def test_delete_last_preset(self):
with PatchedConfirmDelete(self.user_interface):
- # add some rows
- for code in range(3):
- self.add_mapping_via_ui(EventCombination([1, code, 1]), "qux")
-
- self.user_interface.on_delete_preset_clicked()
- # the ui should be clear now
- self.test_gui_clean()
- device_path = f"{CONFIG_PATH}/presets/{self.user_interface.group.key}"
+ # as per test_initial_state we already have preset3 loaded
+
+ self.delete_preset_btn.clicked()
+ gtk_iteration()
+ # the next newest preset should be loaded
+ self.assertEqual(self.data_manager.active_preset.name, "preset2")
+ self.delete_preset_btn.clicked()
+ gtk_iteration()
+ self.delete_preset_btn.clicked()
+ # the ui should be clean
+ self.assert_gui_clean()
+ device_path = f"{CONFIG_PATH}/presets/{self.data_manager.active_group.name}"
self.assertTrue(os.path.exists(f"{device_path}/new preset.json"))
- self.user_interface.on_delete_preset_clicked()
+ self.delete_preset_btn.clicked()
+ gtk_iteration()
# deleting an empty preset als doesn't do weird stuff
- self.test_gui_clean()
- device_path = f"{CONFIG_PATH}/presets/{self.user_interface.group.key}"
+ self.assert_gui_clean()
+ device_path = f"{CONFIG_PATH}/presets/{self.data_manager.active_group.name}"
self.assertTrue(os.path.exists(f"{device_path}/new preset.json"))
def test_enable_disable_symbol_input(self):
+ # load a group without any presets
+ self.controller.load_group("Bar Device")
+
# should be disabled by default since no key is recorded yet
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
- self.assertFalse(self.editor.get_code_editor().get_sensitive())
+ self.assertFalse(self.code_editor.get_sensitive())
- self.editor.enable_symbol_input()
- self.assertEqual(self.get_unfiltered_symbol_input_text(), "")
- self.assertTrue(self.editor.get_text_input().get_sensitive())
-
- # disable it
- self.editor.disable_symbol_input()
- self.assertFalse(self.editor.get_text_input().get_sensitive())
+ # create a mapping
+ self.controller.create_mapping()
+ gtk_iteration()
- # try to enable it by providing a key via set_combination
- self.editor.set_combination(EventCombination((1, 201, 1)))
- self.assertEqual(self.get_unfiltered_symbol_input_text(), "")
- self.assertTrue(self.editor.get_text_input().get_sensitive())
+ # should still be disabled
+ self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
+ self.assertFalse(self.code_editor.get_sensitive())
- # disable it again
- self.editor.set_combination(None)
- self.assertFalse(self.editor.get_text_input().get_sensitive())
+ # enable it by sending a combination
+ self.controller.start_key_recording()
+ gtk_iteration()
+ push_events(
+ "Bar Device",
+ [
+ InputEvent.from_string("1,30,1"),
+ InputEvent.from_string("1,30,0"),
+ ],
+ )
+ self.throttle(50) # give time for the input to arrive
- # try to enable it via the reader
- self.activate_recording_toggle()
- send_event_to_reader(InputEvent.from_tuple((EV_KEY, 101, 1)))
- self.user_interface.consume_newest_keycode()
self.assertEqual(self.get_unfiltered_symbol_input_text(), "")
- self.assertTrue(self.editor.get_code_editor().get_sensitive())
-
- # it wouldn't clear user input, if for whatever reason (a bug?) there is user
- # input in there when enable_symbol_input is called.
- self.editor.set_symbol_input_text("foo")
- self.editor.enable_symbol_input()
- self.assertEqual(self.get_unfiltered_symbol_input_text(), "foo")
-
- def test_whitespace_symbol(self):
- # test how the editor behaves when the text of a mapping is a whitespace.
- # Caused an "Expected `symbol` not to be empty" error in the past, because
- # the symbol was not stripped of whitespaces and logic was performed that
- # resulted in a call to actually changing the mapping.
- self.add_mapping_via_ui(EventCombination([1, 201, 1]), "a")
- self.add_mapping_via_ui(EventCombination([1, 202, 1]), "b")
+ self.assertTrue(self.code_editor.get_sensitive())
- self.select_mapping(1)
- self.assertEqual(self.editor.get_symbol_input_text(), "b")
- self.editor.set_symbol_input_text(" ")
+ # disable it by deleting the mapping
+ with PatchedConfirmDelete(self.user_interface):
+ self.delete_mapping_btn.clicked()
+ gtk_iteration()
- self.select_mapping(0)
- self.assertEqual(self.editor.get_symbol_input_text(), "a")
+ self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
+ self.assertFalse(self.code_editor.get_sensitive())
class TestAutocompletion(GuiTestBase):
def press_key(self, keyval):
event = Gdk.EventKey()
event.keyval = keyval
- self.editor.autocompletion.navigate(None, event)
+ self.user_interface.autocompletion.navigate(None, event)
def test_autocomplete_key(self):
- self.add_mapping_via_ui(EventCombination([1, 99, 1]), "")
- source_view = self.editor.get_code_editor()
- self.set_focus(source_view)
+ self.controller.update_mapping(output_symbol="")
+ gtk_iteration()
+
+ self.set_focus(self.code_editor)
complete_key_name = "Test_Foo_Bar"
system_mapping.clear()
system_mapping._set(complete_key_name, 1)
+ system_mapping._set("KEY_A", 30) # we need this for the UIMapping to work
# it can autocomplete a combination inbetween other things
incomplete = "qux_1\n + + qux_2"
- Gtk.TextView.do_insert_at_cursor(source_view, incomplete)
+ Gtk.TextView.do_insert_at_cursor(self.code_editor, incomplete)
Gtk.TextView.do_move_cursor(
- source_view,
+ self.code_editor,
Gtk.MovementStep.VISUAL_POSITIONS,
-8,
False,
)
- Gtk.TextView.do_insert_at_cursor(source_view, "foo")
- time.sleep(0.11)
- gtk_iteration()
+ Gtk.TextView.do_insert_at_cursor(self.code_editor, "foo")
+ self.throttle(100)
- autocompletion = self.editor.autocompletion
+ autocompletion = self.user_interface.autocompletion
self.assertTrue(autocompletion.visible)
self.press_key(Gdk.KEY_Down)
self.press_key(Gdk.KEY_Return)
+ gtk_iteration()
# the first suggestion should have been selected
- modified_symbol = self.editor.get_symbol_input_text().strip()
+ modified_symbol = self.data_manager.active_mapping.output_symbol
self.assertEqual(modified_symbol, f"qux_1\n + {complete_key_name} + qux_2")
# try again, but a whitespace completes the word and so no autocompletion
# should be shown
- Gtk.TextView.do_insert_at_cursor(source_view, " + foo ")
+ Gtk.TextView.do_insert_at_cursor(self.code_editor, " + foo ")
time.sleep(0.11)
gtk_iteration()
@@ -2158,8 +1767,10 @@ class TestAutocompletion(GuiTestBase):
self.assertFalse(autocompletion.visible)
def test_autocomplete_function(self):
- self.add_mapping_via_ui(EventCombination([1, 99, 1]), "")
- source_view = self.editor.get_code_editor()
+ self.controller.update_mapping(output_symbol="")
+ gtk_iteration()
+
+ source_view = self.code_editor
self.set_focus(source_view)
incomplete = "key(KEY_A).\nepea"
@@ -2168,19 +1779,21 @@ class TestAutocompletion(GuiTestBase):
time.sleep(0.11)
gtk_iteration()
- autocompletion = self.editor.autocompletion
+ autocompletion = self.user_interface.autocompletion
self.assertTrue(autocompletion.visible)
self.press_key(Gdk.KEY_Down)
self.press_key(Gdk.KEY_Return)
# the first suggestion should have been selected
- modified_symbol = self.editor.get_symbol_input_text().strip()
+ modified_symbol = self.data_manager.active_mapping.output_symbol
self.assertEqual(modified_symbol, "key(KEY_A).\nrepeat")
def test_close_autocompletion(self):
- self.add_mapping_via_ui(EventCombination([1, 99, 1]), "")
- source_view = self.editor.get_code_editor()
+ self.controller.update_mapping(output_symbol="")
+ gtk_iteration()
+
+ source_view = self.code_editor
self.set_focus(source_view)
Gtk.TextView.do_insert_at_cursor(source_view, "KEY_")
@@ -2188,7 +1801,7 @@ class TestAutocompletion(GuiTestBase):
time.sleep(0.11)
gtk_iteration()
- autocompletion = self.editor.autocompletion
+ autocompletion = self.user_interface.autocompletion
self.assertTrue(autocompletion.visible)
self.press_key(Gdk.KEY_Down)
@@ -2196,17 +1809,18 @@ class TestAutocompletion(GuiTestBase):
self.assertFalse(autocompletion.visible)
- symbol = self.editor.get_symbol_input_text().strip()
+ symbol = self.data_manager.active_mapping.output_symbol
self.assertEqual(symbol, "KEY_")
def test_writing_still_works(self):
- self.add_mapping_via_ui(EventCombination([1, 99, 1]), "")
- source_view = self.editor.get_code_editor()
+ self.controller.update_mapping(output_symbol="")
+ gtk_iteration()
+ source_view = self.code_editor
self.set_focus(source_view)
Gtk.TextView.do_insert_at_cursor(source_view, "KEY_")
- autocompletion = self.editor.autocompletion
+ autocompletion = self.user_interface.autocompletion
time.sleep(0.11)
gtk_iteration()
@@ -2229,13 +1843,14 @@ class TestAutocompletion(GuiTestBase):
self.assertFalse(autocompletion.visible)
def test_cycling(self):
- self.add_mapping_via_ui(EventCombination([1, 99, 1]), "")
- source_view = self.editor.get_code_editor()
+ self.controller.update_mapping(output_symbol="")
+ gtk_iteration()
+ source_view = self.code_editor
self.set_focus(source_view)
Gtk.TextView.do_insert_at_cursor(source_view, "KEY_")
- autocompletion = self.editor.autocompletion
+ autocompletion = self.user_interface.autocompletion
time.sleep(0.11)
gtk_iteration()
diff --git a/tests/integration/test_user_interface.py b/tests/integration/test_user_interface.py
new file mode 100644
index 00000000..10e39889
--- /dev/null
+++ b/tests/integration/test_user_interface.py
@@ -0,0 +1,107 @@
+import unittest
+from unittest.mock import MagicMock, patch
+from evdev.ecodes import EV_KEY, KEY_A
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+gi.require_version("GLib", "2.0")
+gi.require_version("GtkSource", "4")
+from gi.repository import Gtk, GtkSource, Gdk, GObject, GLib
+
+from inputremapper.gui.utils import gtk_iteration
+from tests.test import quick_cleanup
+from inputremapper.gui.message_broker import MessageBroker, MessageType
+from inputremapper.gui.user_interface import UserInterface
+from inputremapper.configs.mapping import MappingData
+from inputremapper.event_combination import EventCombination
+
+
+class TestUserInterface(unittest.TestCase):
+ def setUp(self) -> None:
+ self.message_broker = MessageBroker()
+ self.controller_mock = MagicMock()
+ self.user_interface = UserInterface(self.message_broker, self.controller_mock)
+
+ def tearDown(self) -> None:
+ super().tearDown()
+ self.message_broker.signal(MessageType.terminate)
+ GLib.timeout_add(0, self.user_interface.window.destroy)
+ GLib.timeout_add(0, Gtk.main_quit)
+ Gtk.main()
+ quick_cleanup()
+
+ def test_shortcut(self):
+ mock = MagicMock()
+ self.user_interface.shortcuts[Gdk.KEY_x] = mock
+
+ event = Gdk.Event()
+ event.key.keyval = Gdk.KEY_x
+ event.key.state = Gdk.ModifierType.SHIFT_MASK
+ self.user_interface.window.emit("key-press-event", event)
+ gtk_iteration()
+ mock.assert_not_called()
+
+ event.key.state = Gdk.ModifierType.CONTROL_MASK
+ self.user_interface.window.emit("key-press-event", event)
+ gtk_iteration()
+ mock.assert_called_once()
+
+ mock.reset_mock()
+ event.key.keyval = Gdk.KEY_y
+ self.user_interface.window.emit("key-press-event", event)
+ gtk_iteration()
+ mock.assert_not_called()
+
+ def test_connected_shortcuts(self):
+ should_be_connected = {Gdk.KEY_q, Gdk.KEY_r, Gdk.KEY_Delete}
+ connected = set(self.user_interface.shortcuts.keys())
+ self.assertEqual(connected, should_be_connected)
+
+ self.assertIs(
+ self.user_interface.shortcuts[Gdk.KEY_q], self.controller_mock.close
+ )
+ self.assertIs(
+ self.user_interface.shortcuts[Gdk.KEY_r],
+ self.controller_mock.refresh_groups,
+ )
+ self.assertIs(
+ self.user_interface.shortcuts[Gdk.KEY_Delete],
+ self.controller_mock.stop_injecting,
+ )
+
+ def test_connect_disconnect_shortcuts(self):
+ mock = MagicMock()
+ self.user_interface.shortcuts[Gdk.KEY_x] = mock
+
+ event = Gdk.Event()
+ event.key.keyval = Gdk.KEY_x
+ event.key.state = Gdk.ModifierType.CONTROL_MASK
+ self.user_interface.disconnect_shortcuts()
+ self.user_interface.window.emit("key-press-event", event)
+ gtk_iteration()
+ mock.assert_not_called()
+
+ self.user_interface.connect_shortcuts()
+ gtk_iteration()
+ self.user_interface.window.emit("key-press-event", event)
+ gtk_iteration()
+ mock.assert_called_once()
+
+ def test_combination_label_shows_combination(self):
+ self.message_broker.send(
+ MappingData(
+ event_combination=EventCombination((EV_KEY, KEY_A, 1)), name="foo"
+ )
+ )
+ gtk_iteration()
+ label: Gtk.Label = self.user_interface.get("combination-label")
+ self.assertEqual(label.get_text(), "a")
+ self.assertEqual(label.get_opacity(), 1)
+
+ def test_combination_label_shows_text_when_empty_mapping(self):
+ self.message_broker.send(MappingData())
+ gtk_iteration()
+ label: Gtk.Label = self.user_interface.get("combination-label")
+ self.assertEqual(label.get_text(), "no input configured")
+ self.assertEqual(label.get_opacity(), 0.4)
diff --git a/tests/test.py b/tests/test.py
index d014477a..1df300f9 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -25,9 +25,28 @@ This module needs to be imported first in test files.
"""
import argparse
+import json
import os
import sys
import tempfile
+import traceback
+import warnings
+from multiprocessing.connection import Connection
+from typing import Dict, Tuple
+import tracemalloc
+
+tracemalloc.start()
+
+try:
+ sys.modules.get("tests.test").main
+ raise AssertionError(
+ "test.py was already imported. "
+ "Always use 'from tests.test import ...' "
+ "not 'from test import ...' to import this"
+ )
+ # have fun debugging infinitely blocking tests without this
+except AttributeError:
+ pass
def get_project_root():
@@ -132,7 +151,7 @@ tmp = temporary_directory.name
uinput_write_history = []
# for tests that makes the injector create its processes
uinput_write_history_pipe = multiprocessing.Pipe()
-pending_events = {}
+pending_events: Dict[str, Tuple[Connection, Connection]] = {}
def read_write_history_pipe():
@@ -166,7 +185,10 @@ fixtures = {
# see if the groups correct attribute is used in functions and paths.
"/dev/input/event11": {
"capabilities": {
- evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_LEFT],
+ evdev.ecodes.EV_KEY: [
+ evdev.ecodes.BTN_LEFT,
+ evdev.ecodes.BTN_TOOL_DOUBLETAP,
+ ],
evdev.ecodes.EV_REL: [
evdev.ecodes.REL_X,
evdev.ecodes.REL_Y,
@@ -200,6 +222,26 @@ fixtures = {
"name": "Foo Device qux",
"group_key": "Foo Device 2",
},
+ "/dev/input/event15": {
+ "capabilities": {
+ evdev.ecodes.EV_SYN: [],
+ evdev.ecodes.EV_ABS: [
+ evdev.ecodes.ABS_X,
+ evdev.ecodes.ABS_Y,
+ evdev.ecodes.ABS_RX,
+ evdev.ecodes.ABS_RY,
+ evdev.ecodes.ABS_Z,
+ evdev.ecodes.ABS_RZ,
+ evdev.ecodes.ABS_HAT0X,
+ evdev.ecodes.ABS_HAT0Y,
+ ],
+ evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_A],
+ },
+ "phys": f"{phys_foo}/input4",
+ "info": info_foo,
+ "name": "Foo Device bar",
+ "group_key": "Foo Device 2",
+ },
# Bar Device
"/dev/input/event20": {
"capabilities": {evdev.ecodes.EV_KEY: keyboard_keys},
@@ -256,6 +298,7 @@ def setup_pipe(group_key):
which in turn will be sent to the reader
"""
if pending_events.get(group_key) is None:
+ logger.info("creating Pipe for %s", group_key)
pending_events[group_key] = multiprocessing.Pipe()
@@ -284,6 +327,7 @@ def push_event(group_key, event):
event : InputEvent
"""
setup_pipe(group_key)
+ logger.info("Simulating %s for %s", event, group_key)
pending_events[group_key][0].send(event)
@@ -355,19 +399,21 @@ class InputDevice:
logger.info("ungrab %s %s", self.name, self.path)
async def async_read_loop(self):
- if pending_events.get(self.group_key) is None:
- self.log("no events to read", self.group_key)
- return
-
- # consume all of them
- while pending_events[self.group_key][1].poll():
- result = pending_events[self.group_key][1].recv()
- self.log(result, "async_read_loop")
- yield result
- await asyncio.sleep(0.01)
+ logger.info("starting read loop for %s", self.path)
+ new_frame = asyncio.Event()
+ asyncio.get_running_loop().add_reader(self.fd, new_frame.set)
+ while True:
+ await new_frame.wait()
+ new_frame.clear()
+ if not pending_events[self.group_key][1].poll():
+ # todo: why? why do we need this?
+ # sometimes this happens, as if a other process calls recv on
+ # the pipe
+ continue
- # doesn't loop endlessly in order to run tests for the injector in
- # the main process
+ event = pending_events[self.group_key][1].recv()
+ logger.info("got %s at %s", event, self.path)
+ yield event
def read(self):
# the patched fake InputDevice objects read anything pending from
@@ -396,13 +442,12 @@ class InputDevice:
def read_one(self):
"""Read one event or none if nothing available."""
- if pending_events.get(self.group_key) is None:
+ if not pending_events.get(self.group_key):
return None
- if len(pending_events[self.group_key]) == 0:
+ if not pending_events[self.group_key][1].poll():
return None
- time.sleep(EVENT_READ_TIMEOUT)
try:
event = pending_events[self.group_key][1].recv()
except (UnpicklingError, EOFError):
@@ -541,6 +586,19 @@ def clear_write_history():
uinput_write_history_pipe[0].recv()
+def warn_with_traceback(message, category, filename, lineno, file=None, line=None):
+
+ log = file if hasattr(file, "write") else sys.stderr
+ traceback.print_stack(file=log)
+ log.write(warnings.formatwarning(message, category, filename, lineno, line))
+
+
+def patch_warnings():
+ # show traceback
+ warnings.showwarning = warn_with_traceback
+ warnings.simplefilter("always")
+
+
# quickly fake some stuff before any other file gets a chance to import
# the original versions
patch_paths()
@@ -548,23 +606,26 @@ patch_evdev()
patch_events()
patch_os_system()
patch_check_output()
+# patch_warnings()
from inputremapper.logger import update_verbosity
update_verbosity(True)
+from inputremapper.daemon import DaemonProxy
from inputremapper.input_event import InputEvent as InternalInputEvent
-from inputremapper.injection.injector import Injector
+from inputremapper.injection.injector import Injector, RUNNING, STOPPED
+from inputremapper.injection.macros.macro import macro_variables
+from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.configs.global_config import global_config
from inputremapper.configs.mapping import Mapping, UIMapping
-from inputremapper.gui.reader import reader
-from inputremapper.groups import groups
+from inputremapper.groups import groups, _Groups
from inputremapper.configs.system_mapping import system_mapping
-from inputremapper.gui.active_preset import active_preset
-from inputremapper.configs.paths import get_config_path
-from inputremapper.injection.macros.macro import macro_variables
+from inputremapper.gui.message_broker import MessageBroker
+from inputremapper.gui.reader import Reader
+from inputremapper.configs.paths import get_config_path, get_preset_path
+from inputremapper.configs.preset import Preset
-# from inputremapper.injection.mapping_handlers.keycode_mapper import active_macros, unreleased
from inputremapper.injection.global_uinputs import global_uinputs
# no need for a high number in tests
@@ -602,16 +663,6 @@ def get_ui_mapping(
)
-def send_event_to_reader(event):
- """Act like the helper and send input events to the reader."""
- reader._results._unread.append(
- {
- "type": "event",
- "message": (event.sec, event.usec, event.type, event.code, event.value),
- }
- )
-
-
def quick_cleanup(log=True):
"""Reset the applications state."""
if log:
@@ -628,11 +679,6 @@ def quick_cleanup(log=True):
pending_events[device] = None
setup_pipe(device)
- try:
- reader.terminate()
- except (BrokenPipeError, OSError):
- pass
-
try:
if asyncio.get_event_loop().is_running():
for task in asyncio.all_tasks():
@@ -662,7 +708,6 @@ def quick_cleanup(log=True):
global_config._save_config()
system_mapping.populate()
- active_preset.empty()
clear_write_history()
@@ -685,8 +730,6 @@ def quick_cleanup(log=True):
if device not in environ_copy:
del os.environ[device]
- reader.clear()
-
for _, pipe in pending_events.values():
assert not pipe.poll()
@@ -725,6 +768,75 @@ def spy(obj, name):
return patch.object(obj, name, wraps=obj.__getattribute__(name))
+class FakeDaemonProxy:
+ def __init__(self):
+ self.calls = {
+ "stop_injecting": [],
+ "get_state": [],
+ "start_injecting": [],
+ "stop_all": 0,
+ "set_config_dir": [],
+ "autoload": 0,
+ "autoload_single": [],
+ "hello": [],
+ }
+
+ def stop_injecting(self, group_key: str) -> None:
+ self.calls["stop_injecting"].append(group_key)
+
+ def get_state(self, group_key: str) -> int:
+ self.calls["get_state"].append(group_key)
+ return STOPPED
+
+ def start_injecting(self, group_key: str, preset: str) -> bool:
+ self.calls["start_injecting"].append((group_key, preset))
+ return True
+
+ def stop_all(self) -> None:
+ self.calls["stop_all"] += 1
+
+ def set_config_dir(self, config_dir: str) -> None:
+ self.calls["set_config_dir"].append(config_dir)
+
+ def autoload(self) -> None:
+ self.calls["autoload"] += 1
+
+ def autoload_single(self, group_key: str) -> None:
+ self.calls["autoload_single"].append(group_key)
+
+ def hello(self, out: str) -> str:
+ self.calls["hello"].append(out)
+ return out
+
+
+def prepare_presets():
+ """prepare a few presets for use in tests
+ "Foo Device 2/preset3" is the newest and "Foo Device 2/preset2" is set to autoload
+ """
+ preset1 = Preset(get_preset_path("Foo Device", "preset1"))
+ preset1.add(get_key_mapping(combination="1,1,1", output_symbol="b"))
+ preset1.add(get_key_mapping(combination="1,2,1"))
+ preset1.save()
+
+ time.sleep(0.05)
+ preset2 = Preset(get_preset_path("Foo Device", "preset2"))
+ preset2.add(get_key_mapping(combination="1,3,1"))
+ preset2.add(get_key_mapping(combination="1,4,1"))
+ preset2.save()
+
+ time.sleep(0.05) # make sure the timestamp of preset 3 is the newest
+ preset3 = Preset(get_preset_path("Foo Device", "preset3"))
+ preset3.add(get_key_mapping(combination="1,5,1"))
+ preset3.save()
+
+ with open(get_config_path("config.json"), "w") as file:
+ json.dump({"autoload": {"Foo Device 2": "preset2"}}, file, indent=4)
+
+ global_config.load_config()
+
+ return preset1, preset2, preset3
+
+
cleanup()
diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py
index 2eee09c9..1c496d92 100644
--- a/tests/unit/test_context.py
+++ b/tests/unit/test_context.py
@@ -85,9 +85,9 @@ class TestContext(unittest.TestCase):
(1, 33): 1,
(1, 34): 1,
}
- self.assertEqual(set(callbacks.keys()), set(context.callbacks.keys()))
+ self.assertEqual(set(callbacks.keys()), set(context.notify_callbacks.keys()))
for key, val in callbacks.items():
- self.assertEqual(val, len(context.callbacks[key]))
+ self.assertEqual(val, len(context.notify_callbacks[key]))
self.assertEqual(
7, len(context._handlers)
diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py
index fd0edb0c..bebd2253 100644
--- a/tests/unit/test_control.py
+++ b/tests/unit/test_control.py
@@ -32,7 +32,6 @@ import collections
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
-from inputremapper.gui.active_preset import active_preset
from inputremapper.configs.global_config import global_config
from inputremapper.daemon import Daemon
from inputremapper.configs.preset import Preset
@@ -42,7 +41,6 @@ from inputremapper.groups import groups
def import_control():
"""Import the core function of the input-remapper-control command."""
- active_preset.empty()
bin_path = os.path.join(os.getcwd(), "bin", "input-remapper-control")
diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py
new file mode 100644
index 00000000..825520cd
--- /dev/null
+++ b/tests/unit/test_controller.py
@@ -0,0 +1,1189 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# input-remapper - GUI for device specific keyboard mappings
+# Copyright (C) 2022 sezanzeb
+#
+# 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 .
+import builtins
+import json
+import os.path
+import time
+import unittest
+from dataclasses import dataclass
+from unittest.mock import patch, MagicMock, call
+from typing import Tuple, List, Any
+
+import gi
+
+from inputremapper.configs.system_mapping import system_mapping
+from inputremapper.injection.injector import (
+ RUNNING,
+ FAILED,
+ NO_GRAB,
+ UPGRADE_EVDEV,
+ UNKNOWN,
+ STOPPED,
+)
+from inputremapper.input_event import InputEvent
+
+gi.require_version("Gtk", "3.0")
+gi.require_version("GLib", "2.0")
+gi.require_version("GtkSource", "4")
+from gi.repository import Gtk
+
+# from inputremapper.gui.helper import is_helper_running
+from inputremapper.event_combination import EventCombination
+from inputremapper.groups import _Groups
+from inputremapper.gui.message_broker import (
+ MessageBroker,
+ MessageType,
+ Signal,
+ UInputsData,
+ GroupsData,
+ GroupData,
+ PresetData,
+ StatusData,
+ CombinationUpdate,
+ CombinationRecorded,
+ UserConfirmRequest,
+)
+from inputremapper.gui.reader import Reader
+from inputremapper.gui.utils import CTX_ERROR, CTX_APPLY, gtk_iteration
+from inputremapper.gui.gettext import _
+from inputremapper.injection.global_uinputs import GlobalUInputs
+from inputremapper.configs.mapping import Mapping, UIMapping, MappingData
+from tests.test import (
+ quick_cleanup,
+ get_key_mapping,
+ FakeDaemonProxy,
+ fixtures,
+ prepare_presets,
+ spy,
+)
+from inputremapper.configs.global_config import global_config, GlobalConfig
+from inputremapper.gui.controller import Controller, MAPPING_DEFAULTS
+from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
+from inputremapper.configs.paths import get_preset_path, get_config_path
+from inputremapper.configs.preset import Preset
+
+
+class TestController(unittest.TestCase):
+ def setUp(self) -> None:
+ super(TestController, self).setUp()
+ self.message_broker = MessageBroker()
+ uinputs = GlobalUInputs()
+ uinputs.prepare_all()
+ self.data_manager = DataManager(
+ self.message_broker,
+ GlobalConfig(),
+ Reader(self.message_broker, _Groups()),
+ FakeDaemonProxy(),
+ uinputs,
+ system_mapping,
+ )
+ self.user_interface = MagicMock()
+ self.controller = Controller(self.message_broker, self.data_manager)
+ self.controller.set_gui(self.user_interface)
+
+ def tearDown(self) -> None:
+ quick_cleanup()
+
+ def test_should_get_newest_group(self):
+ """get_a_group should the newest group"""
+ with patch.object(
+ self.data_manager, "get_newest_group_key", MagicMock(return_value="foo")
+ ):
+ self.assertEqual(self.controller.get_a_group(), "foo")
+
+ def test_should_get_any_group(self):
+ """get_a_group should return a valid group"""
+ with patch.object(
+ self.data_manager,
+ "get_newest_group_key",
+ MagicMock(side_effect=FileNotFoundError),
+ ):
+ fixture_keys = [
+ fixture.get("group_key") or fixture.get("name")
+ for fixture in fixtures.values()
+ ]
+ self.assertIn(self.controller.get_a_group(), fixture_keys)
+
+ def test_should_get_newest_preset(self):
+ """get_a_group should the newest group"""
+ with patch.object(
+ self.data_manager, "get_newest_preset_name", MagicMock(return_value="bar")
+ ):
+ self.data_manager.load_group("Foo Device")
+ self.assertEqual(self.controller.get_a_preset(), "bar")
+
+ def test_should_get_any_preset(self):
+ """get_a_preset should return a new preset if none exist"""
+ self.data_manager.load_group("Foo Device")
+ self.assertEqual(
+ self.controller.get_a_preset(), "new preset"
+ ) # the default name
+
+ def test_on_init_should_provide_uinputs(self):
+ calls = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.uinputs, f)
+ self.message_broker.signal(MessageType.init)
+ self.assertEqual(
+ ["keyboard", "gamepad", "mouse", "keyboard + mouse"],
+ list(calls[-1].uinputs.keys()),
+ )
+
+ def test_on_init_should_provide_groups(self):
+ calls: List[GroupsData] = []
+
+ def f(groups):
+ calls.append(groups)
+
+ self.message_broker.subscribe(MessageType.groups, f)
+ self.message_broker.signal(MessageType.init)
+ self.assertEqual(
+ ["Foo Device", "Foo Device 2", "Bar Device", "gamepad"],
+ list(calls[-1].groups.keys()),
+ )
+
+ def test_on_init_should_provide_a_group(self):
+ calls: List[GroupData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.group, f)
+ self.message_broker.signal(MessageType.init)
+ self.assertGreaterEqual(len(calls), 1)
+
+ def test_on_init_should_provide_a_preset(self):
+ calls: List[PresetData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.preset, f)
+ self.message_broker.signal(MessageType.init)
+ self.assertGreaterEqual(len(calls), 1)
+
+ def test_on_init_should_provide_a_mapping(self):
+ """only if there is one"""
+ prepare_presets()
+ calls: List[MappingData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+ self.message_broker.signal(MessageType.init)
+ self.assertTrue(calls[-1].is_valid())
+
+ def test_on_init_should_provide_a_default_mapping(self):
+ """if there is no real preset available"""
+ calls: List[MappingData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+ self.message_broker.signal(MessageType.init)
+ for m in calls:
+ self.assertEqual(m, UIMapping(**MAPPING_DEFAULTS))
+
+ def test_on_init_should_provide_status_if_helper_is_not_running(self):
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+ with patch("inputremapper.gui.controller.is_helper_running", lambda: False):
+ self.message_broker.signal(MessageType.init)
+ self.assertIn(StatusData(CTX_ERROR, _("The helper did not start")), calls)
+
+ def test_on_init_should_not_provide_status_if_helper_is_running(self):
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+ with patch("inputremapper.gui.controller.is_helper_running", lambda: True):
+ self.message_broker.signal(MessageType.init)
+
+ self.assertNotIn(StatusData(CTX_ERROR, _("The helper did not start")), calls)
+
+ def test_on_load_group_should_provide_preset(self):
+ with patch.object(self.data_manager, "load_preset") as mock:
+ self.controller.load_group("Foo Device")
+ mock.assert_called_once()
+
+ def test_on_load_group_should_provide_mapping(self):
+ """if there is one"""
+ prepare_presets()
+ calls: List[MappingData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+ self.controller.load_group(group_key="Foo Device 2")
+ self.assertTrue(calls[-1].is_valid())
+
+ def test_on_load_group_should_provide_default_mapping(self):
+ """if there is none"""
+ calls: List[MappingData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+
+ self.controller.load_group(group_key="Foo Device")
+ for m in calls:
+ self.assertEqual(m, UIMapping(**MAPPING_DEFAULTS))
+
+ def test_on_load_preset_should_provide_mapping(self):
+ """if there is one"""
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ calls: List[MappingData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+ self.controller.load_preset(name="preset2")
+ self.assertTrue(calls[-1].is_valid())
+
+ def test_on_load_preset_should_provide_default_mapping(self):
+ """if there is none"""
+ Preset(get_preset_path("Foo Device", "bar")).save()
+ self.data_manager.load_group("Foo Device 2")
+ calls: List[MappingData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+ self.controller.load_preset(name="bar")
+ for m in calls:
+ self.assertEqual(m, UIMapping(**MAPPING_DEFAULTS))
+
+ def test_on_delete_preset_asks_for_confirmation(self):
+ prepare_presets()
+ self.message_broker.signal(MessageType.init)
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.user_confirm_request, mock)
+ self.controller.delete_preset()
+ mock.assert_called_once()
+
+ def test_deletes_preset_when_confirmed(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.message_broker.subscribe(
+ MessageType.user_confirm_request, lambda msg: msg.respond(True)
+ )
+ self.controller.delete_preset()
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2")))
+
+ def test_does_not_delete_preset_when_not_confirmed(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.user_interface.confirm_delete.configure_mock(
+ return_value=Gtk.ResponseType.CANCEL
+ )
+ self.controller.delete_preset()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ def test_copy_preset(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset()
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy")))
+
+ def test_copy_preset_should_add_number(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy"
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy 2"
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2")))
+
+ def test_copy_preset_should_increment_existing_number(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy"
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy 2"
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy 3"
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 3")))
+
+ def test_copy_preset_should_not_append_copy_twice(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy"
+ self.controller.copy_preset() # creates "preset2 copy 2" not "preset2 copy copy"
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2")))
+
+ def test_copy_preset_should_not_append_copy_to_copy_with_number(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy"
+ self.data_manager.load_preset("preset2")
+ self.controller.copy_preset() # creates "preset2 copy 2"
+ self.controller.copy_preset() # creates "preset2 copy 3" not "preset2 copy 2 copy"
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 3")))
+
+ def test_rename_preset(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "foo")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.rename_preset(new_name="foo")
+
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "foo")))
+
+ def test_rename_preset_should_pick_available_name(self):
+ prepare_presets()
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3")))
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset3 2")))
+
+ self.data_manager.load_group("Foo Device")
+ self.data_manager.load_preset("preset2")
+ self.controller.rename_preset(new_name="preset3")
+
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3")))
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3 2")))
+
+ def test_rename_preset_should_not_rename_to_empty_name(self):
+ prepare_presets()
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device")
+ self.data_manager.load_preset("preset2")
+ self.controller.rename_preset(new_name="")
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+
+ def test_rename_preset_should_not_update_same_name(self):
+ """when the new name is the same as the current name"""
+ prepare_presets()
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.rename_preset(new_name="preset2")
+
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2")))
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2 2")))
+
+ def test_on_add_preset_uses_default_name(self):
+ self.assertFalse(
+ os.path.exists(get_preset_path("Foo Device", DEFAULT_PRESET_NAME))
+ )
+
+ self.data_manager.load_group("Foo Device 2")
+
+ self.controller.add_preset()
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "new preset")))
+
+ def test_on_add_preset_uses_provided_name(self):
+ self.assertFalse(os.path.exists(get_preset_path("Foo Device", "foo")))
+
+ self.data_manager.load_group("Foo Device 2")
+
+ self.controller.add_preset(name="foo")
+ self.assertTrue(os.path.exists(get_preset_path("Foo Device", "foo")))
+
+ def test_on_add_preset_shows_permission_error_status(self):
+ self.data_manager.load_group("Foo Device 2")
+
+ msg = None
+
+ def f(data):
+ nonlocal msg
+ msg = data
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+ mock = MagicMock(side_effect=PermissionError)
+ with patch("inputremapper.configs.preset.Preset.save", mock):
+ self.controller.add_preset("foo")
+
+ mock.assert_called()
+ self.assertIsNotNone(msg)
+ self.assertIn("Permission denied", msg.msg)
+
+ def test_on_update_mapping(self):
+ """update_mapping should call data_manager.update_mapping
+ this ensures mapping_changed is emitted
+ """
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(combination=EventCombination("1,4,1"))
+
+ with patch.object(self.data_manager, "update_mapping") as mock:
+ self.controller.update_mapping(
+ name="foo",
+ output_symbol="f",
+ release_timeout=0.3,
+ )
+ mock.assert_called_once()
+
+ def test_create_mapping_will_load_the_created_mapping(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+
+ calls: List[MappingData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+ self.controller.create_mapping()
+
+ self.assertEqual(calls[-1], UIMapping(**MAPPING_DEFAULTS))
+
+ def test_create_mapping_should_not_create_multiple_empty_mappings(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.controller.create_mapping() # create a first empty mapping
+
+ calls = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.mapping, f)
+ self.message_broker.subscribe(MessageType.preset, f)
+
+ self.controller.create_mapping() # try to create a second one
+ self.assertEqual(len(calls), 0)
+
+ def test_delete_mapping_asks_for_confirmation(self):
+ prepare_presets()
+ self.message_broker.signal(MessageType.init)
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.user_confirm_request, mock)
+ self.controller.delete_mapping()
+ mock.assert_called_once()
+
+ def test_deletes_mapping_when_confirmed(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.message_broker.subscribe(
+ MessageType.user_confirm_request, lambda msg: msg.respond(True)
+ )
+ self.controller.delete_mapping()
+ self.controller.save()
+
+ preset = Preset(get_preset_path("Foo Device", "preset2"))
+ preset.load()
+ self.assertIsNone(preset.get_mapping(EventCombination("1,3,1")))
+
+ def test_does_not_delete_mapping_when_not_confirmed(self):
+ prepare_presets()
+ self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2")))
+
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.user_interface.confirm_delete.configure_mock(
+ return_value=Gtk.ResponseType.CANCEL
+ )
+
+ self.controller.delete_mapping()
+ self.controller.save()
+
+ preset = Preset(get_preset_path("Foo Device", "preset2"))
+ preset.load()
+ self.assertIsNotNone(preset.get_mapping(EventCombination("1,3,1")))
+
+ def test_should_update_combination(self):
+ """when combination is free"""
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
+
+ calls: List[CombinationUpdate] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.combination_update, f)
+ self.controller.update_combination(EventCombination.from_string("1,10,1"))
+ self.assertEqual(
+ calls[0],
+ CombinationUpdate(
+ EventCombination.from_string("1,3,1"),
+ EventCombination.from_string("1,10,1"),
+ ),
+ )
+
+ def test_should_not_update_combination(self):
+ """when combination is already used"""
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
+
+ calls: List[CombinationUpdate] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.combination_update, f)
+ self.controller.update_combination(EventCombination.from_string("1,4,1"))
+ self.assertEqual(len(calls), 0)
+
+ def test_key_recording_disables_gui_shortcuts(self):
+ self.message_broker.signal(MessageType.init)
+ self.user_interface.disconnect_shortcuts.assert_not_called()
+ self.controller.start_key_recording()
+ self.user_interface.disconnect_shortcuts.assert_called_once()
+
+ def test_key_recording_enables_gui_shortcuts_when_finished(self):
+ self.message_broker.signal(MessageType.init)
+ self.controller.start_key_recording()
+
+ self.user_interface.connect_shortcuts.assert_not_called()
+ self.message_broker.signal(MessageType.recording_finished)
+ self.user_interface.connect_shortcuts.assert_called_once()
+
+ def test_key_recording_enables_gui_shortcuts_when_stopped(self):
+ self.message_broker.signal(MessageType.init)
+ self.controller.start_key_recording()
+
+ self.user_interface.connect_shortcuts.assert_not_called()
+ self.controller.stop_key_recording()
+ self.user_interface.connect_shortcuts.assert_called_once()
+
+ def test_key_recording_updates_mapping_combination(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
+
+ calls: List[CombinationUpdate] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.combination_update, f)
+
+ self.controller.start_key_recording()
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,10,1"))
+ )
+ self.assertEqual(
+ calls[0],
+ CombinationUpdate(
+ EventCombination.from_string("1,3,1"),
+ EventCombination.from_string("1,10,1"),
+ ),
+ )
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,10,1+1,3,1"))
+ )
+ self.assertEqual(
+ calls[1],
+ CombinationUpdate(
+ EventCombination.from_string("1,10,1"),
+ EventCombination.from_string("1,10,1+1,3,1"),
+ ),
+ )
+
+ def test_no_key_recording_when_not_started(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
+
+ calls: List[CombinationUpdate] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.combination_update, f)
+
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,10,1"))
+ )
+ self.assertEqual(len(calls), 0)
+
+ def test_key_recording_stops_when_finished(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
+
+ calls: List[CombinationUpdate] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.combination_update, f)
+
+ self.controller.start_key_recording()
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,10,1"))
+ )
+ self.message_broker.signal(MessageType.recording_finished)
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,10,1+1,3,1"))
+ )
+
+ self.assertEqual(len(calls), 1) # only the first was processed
+
+ def test_key_recording_stops_when_stopped(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
+
+ calls: List[CombinationUpdate] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.combination_update, f)
+
+ self.controller.start_key_recording()
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,10,1"))
+ )
+ self.controller.stop_key_recording()
+ self.message_broker.send(
+ CombinationRecorded(EventCombination.from_string("1,10,1+1,3,1"))
+ )
+
+ self.assertEqual(len(calls), 1) # only the first was processed
+
+ def test_start_injecting_shows_status_when_preset_empty(self):
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.create_preset("foo")
+ self.data_manager.load_preset("foo")
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+
+ def f2():
+ raise AssertionError("Injection started unexpectedly")
+
+ self.data_manager.start_injecting = f2
+ self.controller.start_injecting()
+
+ self.assertEqual(
+ calls[-1], StatusData(CTX_ERROR, _("You need to add keys and save first"))
+ )
+
+ def test_start_injecting_warns_about_btn_left(self):
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.create_preset("foo")
+ self.data_manager.load_preset("foo")
+ self.data_manager.create_mapping()
+ self.data_manager.update_mapping(
+ event_combination=EventCombination(InputEvent.btn_left()),
+ target_uinput="keyboard",
+ output_symbol="a",
+ )
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+
+ def f2():
+ raise AssertionError("Injection started unexpectedly")
+
+ self.data_manager.start_injecting = f2
+ self.controller.start_injecting()
+
+ self.assertEqual(calls[-1].ctx_id, CTX_ERROR)
+ self.assertIn("BTN_LEFT", calls[-1].tooltip)
+
+ def test_start_injecting_starts_with_btn_left_on_second_try(self):
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.create_preset("foo")
+ self.data_manager.load_preset("foo")
+ self.data_manager.create_mapping()
+ self.data_manager.update_mapping(
+ event_combination=EventCombination(InputEvent.btn_left()),
+ target_uinput="keyboard",
+ output_symbol="a",
+ )
+
+ with patch.object(self.data_manager, "start_injecting") as mock:
+ self.controller.start_injecting()
+ mock.assert_not_called()
+ self.controller.start_injecting()
+ mock.assert_called_once()
+
+ def test_start_injecting_starts_with_btn_left_when_mapped_to_other_button(self):
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.create_preset("foo")
+ self.data_manager.load_preset("foo")
+ self.data_manager.create_mapping()
+ self.data_manager.update_mapping(
+ event_combination=EventCombination(InputEvent.btn_left()),
+ target_uinput="keyboard",
+ output_symbol="a",
+ )
+ self.data_manager.create_mapping()
+ self.data_manager.load_mapping(EventCombination.empty_combination())
+ self.data_manager.update_mapping(
+ event_combination=EventCombination.from_string("1,5,1"),
+ target_uinput="mouse",
+ output_symbol="BTN_LEFT",
+ )
+
+ mock = MagicMock(return_value=True)
+ self.data_manager.start_injecting = mock
+ self.controller.start_injecting()
+ mock.assert_called()
+
+ def test_start_injecting_shows_status(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+ mock = MagicMock(return_value=True)
+ self.data_manager.start_injecting = mock
+ self.controller.start_injecting()
+
+ mock.assert_called()
+ self.assertEqual(calls[0], StatusData(CTX_APPLY, _("Starting injection...")))
+
+ def test_start_injecting_shows_failure_status(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+ mock = MagicMock(return_value=False)
+ self.data_manager.start_injecting = mock
+ self.controller.start_injecting()
+
+ mock.assert_called()
+ self.assertEqual(
+ calls[-1],
+ StatusData(
+ CTX_APPLY,
+ _("Failed to apply preset %s") % self.data_manager.active_preset.name,
+ ),
+ )
+
+ def test_start_injecting_adds_listener_to_update_injector_status(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+
+ with patch.object(self.message_broker, "subscribe") as mock:
+ self.controller.start_injecting()
+ mock.assert_called_once_with(
+ MessageType.injector_state, self.controller.show_injector_result
+ )
+
+ def test_stop_injecting_shows_status(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+ mock = MagicMock(return_value=STOPPED)
+ self.data_manager.get_state = mock
+ self.controller.stop_injecting()
+ gtk_iteration(50)
+
+ mock.assert_called()
+ self.assertEqual(
+ calls[-1], StatusData(CTX_APPLY, _("Applied the system default"))
+ )
+
+ def test_show_injection_result(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+
+ mock = MagicMock(return_value=RUNNING)
+ self.data_manager.get_state = mock
+ calls: List[StatusData] = []
+
+ def f(data):
+ calls.append(data)
+
+ self.message_broker.subscribe(MessageType.status_msg, f)
+
+ self.controller.start_injecting()
+ gtk_iteration(50)
+ self.assertEqual(calls[-1].msg, _("Applied preset %s") % "preset2")
+
+ mock.return_value = FAILED
+ self.controller.start_injecting()
+ gtk_iteration(50)
+ self.assertEqual(calls[-1].msg, _("Failed to apply preset %s") % "preset2")
+
+ mock.return_value = NO_GRAB
+ self.controller.start_injecting()
+ gtk_iteration(50)
+ self.assertEqual(calls[-1].msg, "The device was not grabbed")
+
+ mock.return_value = UPGRADE_EVDEV
+ self.controller.start_injecting()
+ gtk_iteration(50)
+ self.assertEqual(calls[-1].msg, "Upgrade python-evdev")
+
+ def test_close(self):
+ mock_save = MagicMock()
+ listener = MagicMock()
+ self.message_broker.subscribe(MessageType.terminate, listener)
+ self.data_manager.save = mock_save
+
+ self.controller.close()
+ mock_save.assert_called()
+ listener.assert_called()
+
+ def test_set_autoload_refreshes_service_config(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+
+ with patch.object(self.data_manager, "refresh_service_config_path") as mock:
+ self.controller.set_autoload(True)
+ mock.assert_called()
+
+ def test_move_event_up(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(
+ event_combination=EventCombination.from_string("1,1,1+1,2,1+1,3,1")
+ )
+
+ self.controller.move_event_in_combination(InputEvent.from_string("1,2,1"), "up")
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,2,1+1,1,1+1,3,1"),
+ )
+ # now nothing changes
+ self.controller.move_event_in_combination(InputEvent.from_string("1,2,1"), "up")
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,2,1+1,1,1+1,3,1"),
+ )
+
+ def test_move_event_down(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(
+ event_combination=EventCombination.from_string("1,1,1+1,2,1+1,3,1")
+ )
+
+ self.controller.move_event_in_combination(
+ InputEvent.from_string("1,2,1"), "down"
+ )
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,1,1+1,3,1+1,2,1"),
+ )
+ # now nothing changes
+ self.controller.move_event_in_combination(
+ InputEvent.from_string("1,2,1"), "down"
+ )
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,1,1+1,3,1+1,2,1"),
+ )
+
+ def test_move_event_in_combination_of_len_1(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.controller.move_event_in_combination(
+ InputEvent.from_string("1,3,1"), "down"
+ )
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,3,1"),
+ )
+
+ def test_move_event_loads_it_again(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(
+ event_combination=EventCombination.from_string("1,1,1+1,2,1+1,3,1")
+ )
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.controller.move_event_in_combination(
+ InputEvent.from_string("1,2,1"), "down"
+ )
+ mock.assert_called_once_with(InputEvent.from_string("1,2,1"))
+
+ def test_update_event(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.load_event(InputEvent.from_string("1,3,1"))
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.controller.update_event(InputEvent.from_string("1,10,1"))
+ mock.assert_called_once_with(InputEvent.from_string("1,10,1"))
+
+ def test_update_event_reloads_mapping_and_event_when_update_fails(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.load_event(InputEvent.from_string("1,3,1"))
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.message_broker.subscribe(MessageType.mapping, mock)
+ calls = [
+ call(self.data_manager.active_mapping.get_bus_message()),
+ call(InputEvent.from_string("1,3,1")),
+ ]
+ self.controller.update_event(InputEvent.from_string("1,4,1")) # already exists
+ mock.assert_has_calls(calls, any_order=False)
+
+ def test_remove_event_does_nothing_when_mapping_not_loaded(self):
+ with spy(self.data_manager, "update_mapping") as mock:
+ self.controller.remove_event()
+ mock.assert_not_called()
+
+ def test_remove_event_removes_active_event(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="1,3,1+1,4,1")
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,3,1+1,4,1"),
+ )
+ self.data_manager.load_event(InputEvent.from_string("1,4,1"))
+
+ self.controller.remove_event()
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,3,1"),
+ )
+
+ def test_remove_event_loads_a_event(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="1,3,1+1,4,1")
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("1,3,1+1,4,1"),
+ )
+ self.data_manager.load_event(InputEvent.from_string("1,4,1"))
+
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.controller.remove_event()
+ mock.assert_called_once_with(InputEvent.from_string("1,3,1"))
+
+ def test_remove_event_reloads_mapping_and_event_when_update_fails(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="1,3,1+1,4,1")
+ self.data_manager.load_event(InputEvent.from_string("1,3,1"))
+
+ # removing "1,3,1" will throw a key error because a mapping with combination
+ # "1,4,1" already exists in preset
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.message_broker.subscribe(MessageType.mapping, mock)
+ calls = [
+ call(self.data_manager.active_mapping.get_bus_message()),
+ call(InputEvent.from_string("1,3,1")),
+ ]
+ self.controller.remove_event()
+ mock.assert_has_calls(calls, any_order=False)
+
+ def test_set_event_as_analog_sets_input_to_analog(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="3,0,10")
+ self.data_manager.load_event(InputEvent.from_string("3,0,10"))
+
+ self.controller.set_event_as_analog(True)
+ self.assertEqual(
+ self.data_manager.active_mapping.event_combination,
+ EventCombination.from_string("3,0,0"),
+ )
+
+ def test_set_event_as_analog_adds_rel_threshold(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="2,0,0")
+ self.data_manager.load_event(InputEvent.from_string("2,0,0"))
+
+ self.controller.set_event_as_analog(False)
+ combinations = [EventCombination("2,0,1"), EventCombination("2,0,-1")]
+ self.assertIn(self.data_manager.active_mapping.event_combination, combinations)
+
+ def test_set_event_as_analog_adds_abs_threshold(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="3,0,0")
+ self.data_manager.load_event(InputEvent.from_string("3,0,0"))
+
+ self.controller.set_event_as_analog(False)
+ combinations = [EventCombination("3,0,10"), EventCombination("3,0,-10")]
+ self.assertIn(self.data_manager.active_mapping.event_combination, combinations)
+
+ def test_set_event_as_analog_reloads_mapping_and_event_when_key_event(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.load_event(InputEvent.from_string("1,3,1"))
+
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.message_broker.subscribe(MessageType.mapping, mock)
+ calls = [
+ call(self.data_manager.active_mapping.get_bus_message()),
+ call(InputEvent.from_string("1,3,1")),
+ ]
+ self.controller.set_event_as_analog(True)
+ mock.assert_has_calls(calls, any_order=False)
+
+ def test_set_event_as_analog_reloads_when_setting_to_analog_fails(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="3,0,10")
+ self.data_manager.load_event(InputEvent.from_string("3,0,10"))
+
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.message_broker.subscribe(MessageType.mapping, mock)
+ calls = [
+ call(self.data_manager.active_mapping.get_bus_message()),
+ call(InputEvent.from_string("3,0,10")),
+ ]
+ with patch.object(self.data_manager, "update_mapping", side_effect=KeyError):
+ self.controller.set_event_as_analog(True)
+ mock.assert_has_calls(calls, any_order=False)
+
+ def test_set_event_as_analog_reloads_when_setting_to_key_fails(self):
+ prepare_presets()
+ self.data_manager.load_group("Foo Device 2")
+ self.data_manager.load_preset("preset2")
+ self.data_manager.load_mapping(EventCombination("1,3,1"))
+ self.data_manager.update_mapping(event_combination="3,0,0")
+ self.data_manager.load_event(InputEvent.from_string("3,0,0"))
+
+ mock = MagicMock()
+ self.message_broker.subscribe(MessageType.selected_event, mock)
+ self.message_broker.subscribe(MessageType.mapping, mock)
+ calls = [
+ call(self.data_manager.active_mapping.get_bus_message()),
+ call(InputEvent.from_string("3,0,0")),
+ ]
+ with patch.object(self.data_manager, "update_mapping", side_effect=KeyError):
+ self.controller.set_event_as_analog(False)
+ mock.assert_has_calls(calls, any_order=False)
diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py
index 3521d1fd..92b92faf 100644
--- a/tests/unit/test_daemon.py
+++ b/tests/unit/test_daemon.py
@@ -160,9 +160,9 @@ class TestDaemon(unittest.TestCase):
self.assertEqual(event.value, 1)
self.daemon.stop_injecting(group.key)
+ time.sleep(0.2)
self.assertEqual(self.daemon.get_state(group.key), STOPPED)
- time.sleep(0.1)
try:
self.assertFalse(uinput_write_history_pipe[0].poll())
except AssertionError:
@@ -171,13 +171,13 @@ class TestDaemon(unittest.TestCase):
raise
"""Injection 2"""
+ self.daemon.start_injecting(group.key, preset_name)
+ time.sleep(0.1)
# -1234 will be classified as -1 by the injector
push_events(group.key, [new_event(*ev_2, -1234)])
-
- self.daemon.start_injecting(group.key, preset_name)
-
time.sleep(0.1)
+
self.assertTrue(uinput_write_history_pipe[0].poll())
# the written key is a key-down event, not the original
@@ -255,6 +255,7 @@ class TestDaemon(unittest.TestCase):
self.assertEqual(event.t, (EV_KEY, KEY_A, 1))
self.daemon.stop_injecting(group_key)
+ time.sleep(0.2)
self.assertEqual(self.daemon.get_state(group_key), STOPPED)
def test_refresh_for_unknown_key(self):
@@ -354,6 +355,7 @@ class TestDaemon(unittest.TestCase):
self.assertNotIn(group.key, daemon.autoload_history._autoload_history)
self.assertTrue(daemon.autoload_history.may_autoload(group.key, preset_name))
self.assertIn(group.key, daemon.injectors)
+ time.sleep(0.2)
self.assertEqual(previous_injector.get_state(), STOPPED)
# a different injetor is now running
self.assertNotEqual(previous_injector, daemon.injectors[group.key])
@@ -377,6 +379,7 @@ class TestDaemon(unittest.TestCase):
# stop
daemon.stop_injecting(group.key)
+ time.sleep(0.2)
self.assertNotIn(group.key, daemon.autoload_history._autoload_history)
self.assertEqual(daemon.injectors[group.key].get_state(), STOPPED)
self.assertTrue(daemon.autoload_history.may_autoload(group.key, preset_name))
@@ -409,7 +412,7 @@ class TestDaemon(unittest.TestCase):
injector = daemon.injectors[group.key]
self.assertEqual(len_before + 1, len_after)
- # calling duplicate _autoload does nothing
+ # calling duplicate get_autoload does nothing
self.daemon._autoload(group.key)
self.assertEqual(
daemon.autoload_history._autoload_history[group.key][1], preset_name
diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py
new file mode 100644
index 00000000..5476e541
--- /dev/null
+++ b/tests/unit/test_data_manager.py
@@ -0,0 +1,893 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# input-remapper - GUI for device specific keyboard mappings
+# Copyright (C) 2022 sezanzeb
+#
+# 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 .
+import json
+import os
+import time
+import unittest
+from itertools import permutations
+from typing import List, Dict, Any
+from unittest.mock import MagicMock, call
+
+from inputremapper.configs.global_config import global_config
+from inputremapper.configs.mapping import UIMapping, MappingData
+from inputremapper.configs.system_mapping import system_mapping
+from inputremapper.event_combination import EventCombination
+from inputremapper.exceptions import DataManagementError
+from inputremapper.groups import _Groups
+from inputremapper.gui.message_broker import (
+ MessageBroker,
+ MessageType,
+ GroupData,
+ PresetData,
+ CombinationUpdate,
+)
+from inputremapper.gui.reader import Reader
+from inputremapper.injection.global_uinputs import GlobalUInputs
+from inputremapper.input_event import InputEvent
+from tests.test import get_key_mapping, quick_cleanup, FakeDaemonProxy, prepare_presets
+
+from inputremapper.configs.paths import get_preset_path, get_config_path
+from inputremapper.configs.preset import Preset
+from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
+
+
+class Listener:
+ def __init__(self):
+ self.calls: List = []
+
+ def __call__(self, data):
+ self.calls.append(data)
+
+
+class TestDataManager(unittest.TestCase):
+ def setUp(self) -> None:
+ self.message_broker = MessageBroker()
+ self.reader = Reader(self.message_broker, _Groups())
+ self.uinputs = GlobalUInputs()
+ self.uinputs.prepare_all()
+ self.data_manager = DataManager(
+ self.message_broker,
+ global_config,
+ self.reader,
+ FakeDaemonProxy(),
+ self.uinputs,
+ system_mapping,
+ )
+
+ def tearDown(self) -> None:
+ quick_cleanup()
+
+ 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 should
+ raise 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(get_preset_path("Foo Device", "preset1"))
+ expected_preset.load()
+ expected_mappings = [
+ (mapping.name, mapping.event_combination) for mapping in 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 an 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=EventCombination("1,1,1"))
+
+ mapping: MappingData = listener.calls[0]
+ control_preset = Preset(get_preset_path("Foo Device", "preset1"))
+ control_preset.load()
+ self.assertEqual(
+ control_preset.get_mapping(EventCombination("1,1,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(EventCombination("1,1,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_load_mapping(self):
+ """should be able to load a mapping"""
+ preset, _, _ = prepare_presets()
+ expected_mapping = preset.get_mapping(EventCombination("1,1,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=EventCombination("1,1,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=EventCombination("1,1,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=EventCombination("1,1,1"),
+ )
+ self.data_manager.load_group("Foo Device")
+ self.assertRaises(
+ DataManagementError,
+ self.data_manager.load_mapping,
+ combination=EventCombination("1,1,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(EventCombination("1,1,1"))
+ self.data_manager.load_event(InputEvent.from_string("1,1,1"))
+ mock.assert_called_once_with(InputEvent.from_string("1,1,1"))
+ self.assertEqual(
+ self.data_manager.active_event, InputEvent.from_string("1,1,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_event(InputEvent.from_string("1,1,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(EventCombination("1,1,1"))
+ with self.assertRaises(ValueError):
+ self.data_manager.load_event(InputEvent.from_string("1,5,1"))
+
+ 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(EventCombination("1,1,1"))
+ self.data_manager.load_event(InputEvent.from_string("1,1,1"))
+ self.data_manager.update_event(InputEvent.from_string("1,5,1"))
+ self.assertEqual(
+ self.data_manager.active_event, InputEvent.from_string("1,5,1")
+ )
+
+ 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(EventCombination("1,1,1"))
+ self.data_manager.load_event(InputEvent.from_string("1,1,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_event(InputEvent.from_string("1,5,1"))
+ expected = [
+ call(
+ CombinationUpdate(EventCombination("1,1,1"), EventCombination("1,5,1"))
+ ),
+ call(self.data_manager.active_mapping.get_bus_message()),
+ call(InputEvent.from_string("1,5,1")),
+ ]
+ 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(EventCombination("1,1,1"))
+ self.data_manager.load_event(InputEvent.from_string("1,1,1"))
+ with self.assertRaises(KeyError):
+ self.data_manager.update_event(InputEvent.from_string("1,2,1"))
+
+ 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(EventCombination("1,1,1"))
+ with self.assertRaises(DataManagementError):
+ self.data_manager.update_event(InputEvent.from_string("1,2,1"))
+
+ 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=EventCombination("1,4,1"))
+
+ 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=EventCombination("1,4,1"))
+
+ self.data_manager.update_mapping(
+ name="foo",
+ output_symbol="f",
+ release_timeout=0.3,
+ )
+ self.data_manager.save()
+
+ preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
+ preset.load()
+ mapping = preset.get_mapping(EventCombination("1,4,1"))
+ self.assertEqual(mapping.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=EventCombination("1,4,1"))
+
+ self.data_manager.update_mapping(
+ output_symbol="bar", # not a macro and not a valid symbol
+ )
+ self.data_manager.save()
+
+ preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
+ preset.load()
+ mapping = preset.get_mapping(EventCombination("1,4,1"))
+ 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=EventCombination("1,4,1"))
+ 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(
+ event_combination=EventCombination.from_string("1,5,1+1,6,1")
+ )
+ self.assertEqual(listener.calls[0].message_type, MessageType.combination_update)
+ self.assertEqual(
+ listener.calls[0].old_combination,
+ EventCombination.from_string("1,4,1"),
+ )
+ self.assertEqual(
+ listener.calls[0].new_combination,
+ EventCombination.from_string("1,5,1+1,6,1"),
+ )
+ self.assertEqual(listener.calls[1].message_type, MessageType.mapping)
+ self.assertEqual(
+ listener.calls[1].event_combination,
+ EventCombination.from_string("1,5,1+1,6,1"),
+ )
+
+ 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=EventCombination("1,4,1"))
+
+ self.assertRaises(
+ KeyError,
+ self.data_manager.update_mapping,
+ event_combination=EventCombination("1,3,1"),
+ )
+
+ 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=EventCombination.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(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=EventCombination("1,3,1"))
+
+ 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(EventCombination("1,3,1"))
+ mappings = listener.calls[0].mappings
+ preset_name = listener.calls[0].name
+ expected_preset = Preset(get_preset_path("Foo Device", "preset2"))
+ expected_preset.load()
+ expected_mappings = [
+ (mapping.name, mapping.event_combination) for mapping in expected_preset
+ ]
+
+ self.assertEqual(preset_name, "preset2")
+ for mapping in expected_mappings:
+ self.assertIn(mapping, mappings)
+
+ self.assertNotIn(
+ (deleted_mapping.name, deleted_mapping.event_combination), 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(get_preset_path("Foo Device", "preset 1")).save()
+ time.sleep(0.01)
+ Preset(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(get_preset_path("Foo Device", "preset 1")).save()
+ time.sleep(0.01)
+ Preset(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(get_preset_path("Foo Device", "preset 1")).save()
+ time.sleep(0.01)
+ Preset(get_preset_path("Bar Device", "preset 2")).save()
+
+ # not a preset, ignore
+ time.sleep(0.01)
+ path = os.path.join(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(get_preset_path("Bar Device", "preset 1")).save()
+ time.sleep(0.01)
+ Preset(get_preset_path("Bar Device", "preset 2")).save()
+ time.sleep(0.01)
+ Preset(get_preset_path("Bar Device", "preset 3")).save()
+
+ # not a preset, ignore
+ time.sleep(0.01)
+ path = os.path.join(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_unknon_groups(self):
+ Preset(get_preset_path("Bar Device", "preset 1")).save()
+ time.sleep(0.01)
+ Preset(get_preset_path("unknown_group", "preset 2")).save() # not a known group
+
+ 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(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 a active group"""
+ self.assertRaises(DataManagementError, self.data_manager.get_newest_preset_name)
+
+ def test_newest_preset_only_searches_active_group(self):
+ Preset(get_preset_path("Foo Device", "preset 1")).save()
+ time.sleep(0.01)
+ Preset(get_preset_path("Foo Device", "preset 3")).save()
+ time.sleep(0.01)
+ Preset(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(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(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_available_preset_name_increments_default(self):
+ Preset(get_preset_path("Foo Device", DEFAULT_PRESET_NAME)).save()
+ Preset(get_preset_path("Foo Device", f"{DEFAULT_PRESET_NAME} 2")).save()
+ Preset(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(get_preset_path("Foo Device", "foo")).save()
+ Preset(get_preset_path("Foo Device", "foo 1")).save()
+ Preset(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_send_groups(self):
+ listener = Listener()
+ self.message_broker.subscribe(MessageType.groups, listener)
+
+ self.data_manager.send_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"],
+ },
+ )
+
+ 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.send_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)
diff --git a/tests/unit/test_event_combination.py b/tests/unit/test_event_combination.py
index dadf4d80..8c6a1876 100644
--- a/tests/unit/test_event_combination.py
+++ b/tests/unit/test_event_combination.py
@@ -21,7 +21,24 @@
import unittest
-from evdev.ecodes import KEY_LEFTSHIFT, KEY_RIGHTALT, KEY_LEFTCTRL
+from evdev.ecodes import (
+ EV_KEY,
+ EV_ABS,
+ EV_REL,
+ BTN_C,
+ BTN_B,
+ BTN_A,
+ REL_WHEEL,
+ REL_HWHEEL,
+ ABS_RY,
+ ABS_X,
+ ABS_HAT0Y,
+ ABS_HAT0X,
+ KEY_A,
+ KEY_LEFTSHIFT,
+ KEY_RIGHTALT,
+ KEY_LEFTCTRL,
+)
from inputremapper.event_combination import EventCombination
from inputremapper.input_event import InputEvent
@@ -120,6 +137,66 @@ class TestKey(unittest.TestCase):
self.assertEqual(c1.json_str(), "1,2,3")
self.assertEqual(c2.json_str(), "1,2,3+4,5,6")
+ def test_beautify(self):
+ # not an integration test, but I have all the selection_label tests here already
+ self.assertEqual(
+ EventCombination((EV_KEY, KEY_A, 1)).beautify(),
+ "a",
+ )
+ self.assertEqual(
+ EventCombination([EV_KEY, KEY_A, 1]).beautify(),
+ "a",
+ )
+ self.assertEqual(
+ EventCombination((EV_ABS, ABS_HAT0Y, -1)).beautify(),
+ "DPad-Y Up",
+ )
+ self.assertEqual(
+ EventCombination((EV_KEY, BTN_A, 1)).beautify(),
+ "Button A",
+ )
+ self.assertEqual(EventCombination((EV_KEY, 1234, 1)).beautify(), "unknown")
+ self.assertEqual(
+ EventCombination([EV_ABS, ABS_HAT0X, -1]).beautify(),
+ "DPad-X Left",
+ )
+ self.assertEqual(
+ EventCombination([EV_ABS, ABS_HAT0Y, -1]).beautify(),
+ "DPad-Y Up",
+ )
+ self.assertEqual(
+ EventCombination([EV_KEY, BTN_A, 1]).beautify(),
+ "Button A",
+ )
+ self.assertEqual(
+ EventCombination([EV_ABS, ABS_X, 1]).beautify(),
+ "Joystick-X Right",
+ )
+ self.assertEqual(
+ EventCombination([EV_ABS, ABS_RY, 1]).beautify(),
+ "Joystick-RY Down",
+ )
+ self.assertEqual(
+ EventCombination([EV_REL, REL_HWHEEL, 1]).beautify(),
+ "Wheel Right",
+ )
+ self.assertEqual(
+ EventCombination([EV_REL, REL_WHEEL, -1]).beautify(),
+ "Wheel Down",
+ )
+
+ # combinations
+ self.assertEqual(
+ EventCombination(
+ (
+ (EV_KEY, BTN_A, 1),
+ (EV_KEY, BTN_B, 1),
+ (EV_KEY, BTN_C, 1),
+ ),
+ ).beautify(),
+ "Button A + Button B + Button C",
+ )
+
if __name__ == "__main__":
unittest.main()
diff --git a/inputremapper/gui/editor/__init__.py b/tests/unit/test_event_pipeline/__init__.py
similarity index 100%
rename from inputremapper/gui/editor/__init__.py
rename to tests/unit/test_event_pipeline/__init__.py
diff --git a/tests/unit/test_axis_transformation.py b/tests/unit/test_event_pipeline/test_axis_transformation.py
similarity index 92%
rename from tests/unit/test_axis_transformation.py
rename to tests/unit/test_event_pipeline/test_axis_transformation.py
index ecaf7c3c..2bf45436 100644
--- a/tests/unit/test_axis_transformation.py
+++ b/tests/unit/test_event_pipeline/test_axis_transformation.py
@@ -192,3 +192,15 @@ class TestAxisTransformation(unittest.TestCase):
places=5,
msg=f"test continuity at {- init_args.deadzone} for {init_args}",
)
+
+ def test_expo_out_of_range(self):
+ f = Transformation(deadzone=0.1, min_=-20, max_=5, expo=1.3)
+ self.assertRaises(ValueError, f, 0)
+ f = Transformation(deadzone=0.1, min_=-20, max_=5, expo=-1.3)
+ self.assertRaises(ValueError, f, 0)
+
+ def test_returns_one_for_range_between_minus_and_plus_one(self):
+ for init_args in self.get_init_args(max_=(1,), min_=(-1,), gain=(1,)):
+ f = Transformation(*init_args.values())
+ self.assertEqual(f(1), 1)
+ self.assertEqual(f(-1), -1)
diff --git a/tests/unit/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py
similarity index 99%
rename from tests/unit/test_event_pipeline.py
rename to tests/unit/test_event_pipeline/test_event_pipeline.py
index 339db603..f8dcc5d4 100644
--- a/tests/unit/test_event_pipeline.py
+++ b/tests/unit/test_event_pipeline/test_event_pipeline.py
@@ -307,8 +307,8 @@ class TestEventPipeline(unittest.IsolatedAsyncioTestCase):
)
# each axis writes speed*gain*rate*sleep=1*0.5*60 events
- self.assertGreater(len(history), speed * gain * rate * sleep * 0.9 * 2)
- self.assertLess(len(history), speed * gain * rate * sleep * 1.1 * 2)
+ self.assertGreater(len(history), speed * gain * rate * sleep * 0.8 * 2)
+ self.assertLess(len(history), speed * gain * rate * sleep * 1.2 * 2)
# those may be in arbitrary order
count_x = history.count((EV_REL, REL_X, -1))
@@ -362,7 +362,7 @@ class TestEventPipeline(unittest.IsolatedAsyncioTestCase):
event_reader,
)
# wait a bit more for it to sum up
- sleep = 0.5
+ sleep = 0.8
await asyncio.sleep(sleep)
# stop it
await self.send_events(
diff --git a/tests/unit/test_event_pipeline/test_mapping_handlers.py b/tests/unit/test_event_pipeline/test_mapping_handlers.py
new file mode 100644
index 00000000..6413ffb5
--- /dev/null
+++ b/tests/unit/test_event_pipeline/test_mapping_handlers.py
@@ -0,0 +1,261 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# input-remapper - GUI for device specific keyboard mappings
+# Copyright (C) 2022 sezanzeb
+#
+# 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 .
+
+
+import asyncio
+import unittest
+from typing import Iterable
+from unittest.mock import MagicMock
+
+import evdev
+from evdev.ecodes import (
+ EV_KEY,
+ EV_ABS,
+ EV_REL,
+ ABS_X,
+ ABS_Y,
+ REL_X,
+ REL_Y,
+ BTN_A,
+ REL_HWHEEL,
+ REL_WHEEL,
+ REL_WHEEL_HI_RES,
+ REL_HWHEEL_HI_RES,
+ ABS_HAT0X,
+ BTN_LEFT,
+ BTN_RIGHT,
+ BTN_B,
+ KEY_A,
+ ABS_HAT0Y,
+ KEY_B,
+ KEY_C,
+ BTN_TL,
+)
+
+from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler
+from inputremapper.injection.mapping_handlers.abs_to_rel_handler import AbsToRelHandler
+from inputremapper.injection.mapping_handlers.axis_switch_handler import (
+ AxisSwitchHandler,
+)
+from inputremapper.injection.mapping_handlers.hierarchy_handler import HierarchyHandler
+from inputremapper.injection.mapping_handlers.key_handler import KeyHandler
+from inputremapper.injection.mapping_handlers.macro_handler import MacroHandler
+from inputremapper.injection.mapping_handlers.mapping_handler import MappingHandler
+from inputremapper.logger import logger
+from inputremapper.configs.mapping import Mapping
+from inputremapper.injection.context import Context
+from inputremapper.injection.event_reader import EventReader
+from tests.test import (
+ get_key_mapping,
+ InputDevice,
+ cleanup,
+ convert_to_internal_events,
+ MAX_ABS,
+ MIN_ABS,
+)
+
+from inputremapper.input_event import InputEvent, EventActions
+from inputremapper.event_combination import EventCombination
+from inputremapper.configs.system_mapping import system_mapping
+from inputremapper.configs.preset import Preset
+from inputremapper.injection.global_uinputs import global_uinputs
+
+
+class BaseTests:
+ """implements test that should pass on most mapping handlers
+ in special cases override specific tests.
+ """
+
+ handler: MappingHandler
+
+ def setUp(self):
+ raise NotImplementedError
+
+ def tearDown(self) -> None:
+ cleanup()
+
+ def test_reset(self):
+ mock = MagicMock()
+ self.handler.set_sub_handler(mock)
+ self.handler.reset()
+ mock.reset.assert_called()
+
+
+class TestAxisSwitchHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.handler = AxisSwitchHandler(
+ EventCombination.from_string("2,5,0+1,3,1"),
+ Mapping(
+ event_combination="2,5,0+1,3,1",
+ target_uinput="mouse",
+ output_type=2,
+ output_code=1,
+ ),
+ )
+
+
+class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.handler = AbsToBtnHandler(
+ EventCombination.from_string("3,5,10"),
+ Mapping(
+ event_combination="3,5,10",
+ target_uinput="mouse",
+ output_symbol="BTN_LEFT",
+ ),
+ )
+
+
+class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.handler = AbsToRelHandler(
+ EventCombination((EV_ABS, ABS_X, 0)),
+ Mapping(
+ event_combination=f"{EV_ABS},{ABS_X},0",
+ target_uinput="mouse",
+ output_type=EV_REL,
+ output_code=REL_X,
+ ),
+ )
+
+ async def test_reset(self):
+ self.handler.notify(
+ InputEvent(0, 0, EV_ABS, ABS_X, MAX_ABS),
+ source=InputDevice("/dev/input/event15"),
+ forward=evdev.UInput(),
+ )
+ await asyncio.sleep(0.2)
+ self.handler.reset()
+ await asyncio.sleep(0.05)
+
+ count = global_uinputs.get_uinput("mouse").write_count
+ self.assertGreater(count, 6) # count should be 60*0.2 = 12
+ await asyncio.sleep(0.2)
+ self.assertEqual(count, global_uinputs.get_uinput("mouse").write_count)
+
+
+class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.handler = AxisSwitchHandler(
+ EventCombination.from_string("2,0,10+1,3,1"),
+ Mapping(
+ event_combination="2,0,10+1,3,1",
+ target_uinput="mouse",
+ output_symbol="BTN_LEFT",
+ ),
+ )
+
+
+class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.mock1 = MagicMock()
+ self.mock2 = MagicMock()
+ self.mock3 = MagicMock()
+ self.handler = HierarchyHandler(
+ [self.mock1, self.mock2, self.mock3],
+ InputEvent.from_tuple((EV_KEY, KEY_A, 1)),
+ )
+
+ def test_reset(self):
+ self.handler.reset()
+ self.mock1.reset.assert_called()
+ self.mock2.reset.assert_called()
+ self.mock3.reset.assert_called()
+
+
+class TestKeyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.handler = KeyHandler(
+ EventCombination.from_string("2,0,10+1,3,1"),
+ Mapping(
+ event_combination="2,0,10+1,3,1",
+ target_uinput="mouse",
+ output_symbol="BTN_LEFT",
+ ),
+ )
+
+ def test_reset(self):
+ self.handler.notify(
+ InputEvent(0, 0, EV_REL, REL_X, 1, actions=(EventActions.as_key,)),
+ source=InputDevice("/dev/input/event11"),
+ forward=evdev.UInput(),
+ )
+ history = convert_to_internal_events(
+ global_uinputs.get_uinput("mouse").write_history
+ )
+ self.assertEqual(history[0], InputEvent.from_tuple((EV_KEY, BTN_LEFT, 1)))
+ self.assertEqual(len(history), 1)
+
+ self.handler.reset()
+ history = convert_to_internal_events(
+ global_uinputs.get_uinput("mouse").write_history
+ )
+ self.assertEqual(history[1], InputEvent.from_tuple((EV_KEY, BTN_LEFT, 0)))
+ self.assertEqual(len(history), 2)
+
+
+class TestMacroHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.context_mock = MagicMock()
+ self.handler = MacroHandler(
+ EventCombination.from_string("2,0,10+1,3,1"),
+ Mapping(
+ event_combination="2,0,10+1,3,1",
+ target_uinput="mouse",
+ output_symbol="hold_keys(BTN_LEFT, BTN_RIGHT)",
+ ),
+ context=self.context_mock,
+ )
+
+ async def test_reset(self):
+ self.handler.notify(
+ InputEvent(0, 0, EV_REL, REL_X, 1, actions=(EventActions.as_key,)),
+ source=InputDevice("/dev/input/event11"),
+ forward=evdev.UInput(),
+ )
+
+ await asyncio.sleep(0.1)
+ history = convert_to_internal_events(
+ global_uinputs.get_uinput("mouse").write_history
+ )
+ self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_LEFT, 1)), history)
+ self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_RIGHT, 1)), history)
+ self.assertEqual(len(history), 2)
+
+ self.handler.reset()
+ await asyncio.sleep(0.1)
+ history = convert_to_internal_events(
+ global_uinputs.get_uinput("mouse").write_history
+ )
+ self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_LEFT, 0)), history[-2:])
+ self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_RIGHT, 0)), history[-2:])
+ self.assertEqual(len(history), 4)
+
+
+class TestRelToBtnHanlder(BaseTests, unittest.IsolatedAsyncioTestCase):
+ def setUp(self):
+ self.handler = AxisSwitchHandler(
+ EventCombination.from_string("2,0,10+1,3,1"),
+ Mapping(
+ event_combination="2,0,10+1,3,1",
+ target_uinput="mouse",
+ output_symbol="BTN_LEFT",
+ ),
+ )
diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py
index d623b451..69cf51f6 100644
--- a/tests/unit/test_event_reader.py
+++ b/tests/unit/test_event_reader.py
@@ -58,16 +58,15 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
def tearDown(self):
quick_cleanup()
- def setup(self, source, mapping):
- """Set a a EventReader up for the test and run it in the background."""
+ async def setup(self, source, mapping):
+ """Set a EventReader up for the test and run it in the background."""
forward_to = evdev.UInput()
context = Context(mapping)
context.uinput = evdev.UInput()
- consumer_control = EventReader(context, source, forward_to, self.stop_event)
- # for consumer in consumer_control._consumers:
- # consumer._abs_range = (-10, 10)
- asyncio.ensure_future(consumer_control.run())
- return context, consumer_control
+ event_reader = EventReader(context, source, forward_to, self.stop_event)
+ asyncio.ensure_future(event_reader.run())
+ await asyncio.sleep(0.1)
+ return context, event_reader
async def test_if_single_joystick_then(self):
# TODO: Move this somewhere more sensible
@@ -112,7 +111,7 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
cfg["output_code"] = REL_WHEEL_HI_RES
self.preset.add(Mapping(**cfg))
- context, _ = self.setup(self.gamepad_source, self.preset)
+ context, _ = await self.setup(self.gamepad_source, self.preset)
self.gamepad_source.push_events(
[
@@ -125,7 +124,9 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
new_event(EV_KEY, trigger, 0),
]
)
+
await asyncio.sleep(0.1)
+ self.stop_event.set() # stop the reader
self.assertEqual(len(context.listeners), 0)
history = [a.t for a in global_uinputs.get_uinput("keyboard").write_history]
self.assertIn((EV_KEY, code_a, 1), history)
@@ -151,7 +152,7 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
# self.preset.set("gamepad.joystick.left_purpose", BUTTONS)
# self.preset.set("gamepad.joystick.right_purpose", BUTTONS)
- context, _ = self.setup(self.gamepad_source, self.preset)
+ context, _ = await self.setup(self.gamepad_source, self.preset)
self.gamepad_source.push_events(
[
diff --git a/tests/unit/test_groups.py b/tests/unit/test_groups.py
index b9a15308..ef133703 100644
--- a/tests/unit/test_groups.py
+++ b/tests/unit/test_groups.py
@@ -33,12 +33,7 @@ from inputremapper.groups import (
_FindGroups,
groups,
classify,
- GAMEPAD,
- MOUSE,
- UNKNOWN,
- GRAPHICS_TABLET,
- TOUCHPAD,
- KEYBOARD,
+ DeviceType,
_Group,
)
@@ -58,7 +53,7 @@ class TestGroups(unittest.TestCase):
group = _Group(
paths=["/dev/a", "/dev/b", "/dev/c"],
names=["name_bar", "name_a", "name_foo"],
- types=[MOUSE, KEYBOARD, UNKNOWN],
+ types=[DeviceType.MOUSE, DeviceType.KEYBOARD, DeviceType.UNKNOWN],
key="key",
)
self.assertEqual(group.name, "name_a")
@@ -85,7 +80,7 @@ class TestGroups(unittest.TestCase):
"/dev/input/event1",
],
"names": ["Foo Device"],
- "types": [KEYBOARD],
+ "types": [DeviceType.KEYBOARD],
"key": "Foo Device",
}
),
@@ -95,9 +90,19 @@ class TestGroups(unittest.TestCase):
"/dev/input/event11",
"/dev/input/event10",
"/dev/input/event13",
+ "/dev/input/event15",
+ ],
+ "names": [
+ "Foo Device foo",
+ "Foo Device",
+ "Foo Device",
+ "Foo Device bar",
+ ],
+ "types": [
+ DeviceType.GAMEPAD,
+ DeviceType.KEYBOARD,
+ DeviceType.MOUSE,
],
- "names": ["Foo Device foo", "Foo Device", "Foo Device"],
- "types": [KEYBOARD, MOUSE],
"key": "Foo Device 2",
}
),
@@ -105,7 +110,7 @@ class TestGroups(unittest.TestCase):
{
"paths": ["/dev/input/event20"],
"names": ["Bar Device"],
- "types": [KEYBOARD],
+ "types": [DeviceType.KEYBOARD],
"key": "Bar Device",
}
),
@@ -113,7 +118,7 @@ class TestGroups(unittest.TestCase):
{
"paths": ["/dev/input/event30"],
"names": ["gamepad"],
- "types": [GAMEPAD],
+ "types": [DeviceType.GAMEPAD],
"key": "gamepad",
}
),
@@ -121,7 +126,7 @@ class TestGroups(unittest.TestCase):
{
"paths": ["/dev/input/event40"],
"names": ["input-remapper Bar Device"],
- "types": [KEYBOARD],
+ "types": [DeviceType.KEYBOARD],
"key": "input-remapper Bar Device",
}
),
@@ -229,7 +234,7 @@ class TestGroups(unittest.TestCase):
}
)
),
- GAMEPAD,
+ DeviceType.GAMEPAD,
)
"""Mice"""
@@ -247,12 +252,14 @@ class TestGroups(unittest.TestCase):
}
)
),
- MOUSE,
+ DeviceType.MOUSE,
)
"""Keyboard"""
- self.assertEqual(classify(FakeDevice({EV_KEY: [evdev.ecodes.KEY_A]})), KEYBOARD)
+ self.assertEqual(
+ classify(FakeDevice({EV_KEY: [evdev.ecodes.KEY_A]})), DeviceType.KEYBOARD
+ )
"""Touchpads"""
@@ -265,7 +272,7 @@ class TestGroups(unittest.TestCase):
}
)
),
- TOUCHPAD,
+ DeviceType.TOUCHPAD,
)
"""Graphics tablets"""
@@ -279,7 +286,7 @@ class TestGroups(unittest.TestCase):
}
)
),
- GRAPHICS_TABLET,
+ DeviceType.GRAPHICS_TABLET,
)
"""Weird combos"""
@@ -293,19 +300,23 @@ class TestGroups(unittest.TestCase):
}
)
),
- UNKNOWN,
+ DeviceType.UNKNOWN,
)
self.assertEqual(
classify(
FakeDevice({EV_ABS: [evdev.ecodes.ABS_X], EV_KEY: [evdev.ecodes.BTN_A]})
),
- UNKNOWN,
+ DeviceType.UNKNOWN,
)
- self.assertEqual(classify(FakeDevice({EV_KEY: [evdev.ecodes.BTN_A]})), UNKNOWN)
+ self.assertEqual(
+ classify(FakeDevice({EV_KEY: [evdev.ecodes.BTN_A]})), DeviceType.UNKNOWN
+ )
- self.assertEqual(classify(FakeDevice({EV_ABS: [evdev.ecodes.ABS_X]})), UNKNOWN)
+ self.assertEqual(
+ classify(FakeDevice({EV_ABS: [evdev.ecodes.ABS_X]})), DeviceType.UNKNOWN
+ )
if __name__ == "__main__":
diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py
index a3f4f8f7..6fb5c9e1 100644
--- a/tests/unit/test_injector.py
+++ b/tests/unit/test_injector.py
@@ -58,6 +58,7 @@ from inputremapper.injection.injector import (
NO_GRAB,
UNKNOWN,
get_udev_name,
+ FAILED,
)
from inputremapper.injection.numlock import is_numlock_on
from inputremapper.configs.system_mapping import (
@@ -65,12 +66,11 @@ from inputremapper.configs.system_mapping import (
DISABLE_CODE,
DISABLE_NAME,
)
-from inputremapper.gui.active_preset import active_preset
from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.injection.macros.parse import parse
from inputremapper.injection.context import Context
-from inputremapper.groups import groups, classify, GAMEPAD
+from inputremapper.groups import groups, classify, DeviceType
def wait_for_uinput_write():
@@ -101,9 +101,10 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
evdev.InputDevice.grab = grab_fail_twice
def tearDown(self):
- if self.injector is not None:
+ if self.injector is not None and self.injector.is_alive():
self.injector.stop_injecting()
- self.assertEqual(self.injector.get_state(), STOPPED)
+ time.sleep(0.2)
+ self.assertIn(self.injector.get_state(), (STOPPED, FAILED, NO_GRAB))
self.injector = None
evdev.InputDevice.grab = self.grab
@@ -119,8 +120,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
# this test needs to pass around all other constraints of
# _grab_device
self.injector.context = Context(preset)
- device = self.injector._grab_device(path)
- gamepad = classify(device) == GAMEPAD
+ device = self.injector._grab_device(evdev.InputDevice(path))
+ gamepad = classify(device) == DeviceType.GAMEPAD
self.assertFalse(gamepad)
self.assertEqual(self.failed, 2)
# success on the third try
@@ -134,7 +135,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
path = "/dev/input/event10"
self.injector.context = Context(preset)
- device = self.injector._grab_device(path)
+ device = self.injector._grab_device(evdev.InputDevice(path))
self.assertIsNone(device)
self.assertGreaterEqual(self.failed, 1)
@@ -154,14 +155,15 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
)
self.injector = Injector(groups.find(name="gamepad"), preset)
self.injector.context = Context(preset)
+ self.injector.group.paths = [
+ "/dev/input/event10",
+ "/dev/input/event30",
+ "/dev/input/event1234",
+ ]
- _grab_device = self.injector._grab_device
- # doesn't have the required capability
- self.assertIsNone(_grab_device("/dev/input/event10"))
- # according to the fixtures, /dev/input/event30 can do ABS_HAT0X
- self.assertIsNotNone(_grab_device("/dev/input/event30"))
- # this doesn't exist
- self.assertIsNone(_grab_device("/dev/input/event1234"))
+ grabbed = self.injector._grab_devices()
+ self.assertEqual(len(grabbed), 1)
+ self.assertEqual(grabbed[0].path, "/dev/input/event30")
def test_forward_gamepad_events(self):
# forward abs joystick events
@@ -170,15 +172,16 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector.context = Context(preset)
path = "/dev/input/event30"
- device = self.injector._grab_device(path)
- self.assertIsNone(device) # no capability is used, so it won't grab
+ device = self.injector._grab_devices()
+ self.assertEqual(device, []) # no capability is used, so it won't grab
preset.add(
get_key_mapping(EventCombination([EV_KEY, BTN_A, 1]), "keyboard", "a"),
)
- device = self.injector._grab_device(path)
- self.assertIsNotNone(device)
- gamepad = classify(device) == GAMEPAD
+ devices = self.injector._grab_devices()
+ self.assertEqual(len(devices), 1)
+ self.assertEqual(devices[0].path, path)
+ gamepad = classify(devices[0]) == DeviceType.GAMEPAD
self.assertTrue(gamepad)
def test_skip_unused_device(self):
@@ -188,8 +191,9 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
self.injector.context = Context(preset)
path = "/dev/input/event11"
- device = self.injector._grab_device(path)
- self.assertIsNone(device)
+ self.injector.group.paths = [path]
+ devices = self.injector._grab_devices()
+ self.assertEqual(devices, [])
self.assertEqual(self.failed, 0)
def test_skip_unknown_device(self):
@@ -199,15 +203,17 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
# skips a device because its capabilities are not used in the preset
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
self.injector.context = Context(preset)
+
path = "/dev/input/event11"
- device = self.injector._grab_device(path)
+ self.injector.group.paths = [path]
+ devices = self.injector._grab_devices()
# skips the device alltogether, so no grab attempts fail
self.assertEqual(self.failed, 0)
- self.assertIsNone(device)
+ self.assertEqual(devices, [])
def test_get_udev_name(self):
- self.injector = Injector(groups.find(key="Foo Device 2"), active_preset)
+ self.injector = Injector(groups.find(key="Foo Device 2"), Preset())
suffix = "mapped"
prefix = "input-remapper"
expected = f'{prefix} {"a" * (80 - len(suffix) - len(prefix) - 2)} {suffix}'
@@ -236,15 +242,11 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector.run()
self.assertEqual(
- self.injector.context.preset.get_mapping(
- EventCombination([EV_KEY, KEY_A, 1])
- ),
+ self.injector.preset.get_mapping(EventCombination([EV_KEY, KEY_A, 1])),
m1,
)
self.assertEqual(
- self.injector.context.preset.get_mapping(
- EventCombination([EV_REL, REL_HWHEEL, 1])
- ),
+ self.injector.preset.get_mapping(EventCombination([EV_REL, REL_HWHEEL, 1])),
m2,
)
@@ -516,7 +518,7 @@ class TestModifyCapabilities(unittest.TestCase):
# I don't know what ABS_VOLUME is, for now I would like to just always
# remove it until somebody complains, since its presence broke stuff
- self.injector = Injector(None, self.preset)
+ self.injector = Injector(mock.Mock(), self.preset)
self.fake_device._capabilities = {
EV_ABS: [ABS_VOLUME, (ABS_X, evdev.AbsInfo(0, 0, 500, 0, 0, 0))],
EV_KEY: [1, 2, 3],
diff --git a/tests/unit/test_ipc.py b/tests/unit/test_ipc.py
index a7c0adb3..80e60091 100644
--- a/tests/unit/test_ipc.py
+++ b/tests/unit/test_ipc.py
@@ -17,7 +17,8 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see .
-
+import asyncio
+import multiprocessing
from tests.test import quick_cleanup, tmp
@@ -125,7 +126,7 @@ class TestSocket(unittest.TestCase):
self.assertRaises(NotImplementedError, lambda: Base.fileno(None))
-class TestPipe(unittest.TestCase):
+class TestPipe(unittest.IsolatedAsyncioTestCase):
def test_pipe_single(self):
p1 = Pipe(os.path.join(tmp, "pipe"))
self.assertEqual(p1.recv(), None)
@@ -161,6 +162,47 @@ class TestPipe(unittest.TestCase):
self.assertEqual(p2.recv(), 3)
self.assertEqual(p2.recv(), None)
+ async def test_async_for_loop(self):
+ p1 = Pipe(os.path.join(tmp, "pipe"))
+ iterator = p1.__aiter__()
+ p1.send(1)
+
+ self.assertEqual(await iterator.__anext__(), 1)
+
+ read_task = asyncio.Task(iterator.__anext__())
+ timeout_task = asyncio.Task(asyncio.sleep(1))
+
+ done, pending = await asyncio.wait(
+ (read_task, timeout_task), return_when=asyncio.FIRST_COMPLETED
+ )
+ self.assertIn(timeout_task, done)
+ self.assertIn(read_task, pending)
+ read_task.cancel()
+
+ async def test_async_for_loop_duo(self):
+ def writer():
+ p = Pipe(os.path.join(tmp, "pipe"))
+ for i in range(3):
+ p.send(i)
+ time.sleep(0.5)
+ for i in range(3):
+ p.send(i)
+ time.sleep(0.1)
+ p.send("stop now")
+
+ p1 = Pipe(os.path.join(tmp, "pipe"))
+
+ w_process = multiprocessing.Process(target=writer)
+ w_process.start()
+
+ messages = []
+ async for msg in p1:
+ messages.append(msg)
+ if msg == "stop now":
+ break
+
+ self.assertEqual(messages, [0, 1, 2, 0, 1, 2, "stop now"])
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py
index ddd96623..83ff3821 100644
--- a/tests/unit/test_macros.py
+++ b/tests/unit/test_macros.py
@@ -792,7 +792,7 @@ class TestMacros(MacroTestBase):
keystroke_sleep = DummyMapping.macro_key_sleep_ms
sleep_time = 2 * repeats * keystroke_sleep / 1000
self.assertGreater(time.time() - start, sleep_time * 0.9)
- self.assertLess(time.time() - start, sleep_time * 1.2)
+ self.assertLess(time.time() - start, sleep_time * 1.3)
self.assertListEqual(
self.result,
diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py
index 391b9848..5e581db4 100644
--- a/tests/unit/test_mapping.py
+++ b/tests/unit/test_mapping.py
@@ -24,8 +24,9 @@ from functools import partial
from evdev.ecodes import EV_KEY
from pydantic import ValidationError
-from inputremapper.configs.mapping import Mapping
+from inputremapper.configs.mapping import Mapping, UIMapping
from inputremapper.configs.system_mapping import system_mapping
+from inputremapper.gui.message_broker import MessageType
from inputremapper.input_event import EventActions
from inputremapper.event_combination import EventCombination
@@ -84,20 +85,20 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
"output_code": 3,
}
m = Mapping(**cfg)
- expected_actions = [EventActions.as_key, EventActions.as_key, EventActions.none]
- actions = [event.action for event in m.event_combination]
+ expected_actions = [(EventActions.as_key,), (EventActions.as_key,), ()]
+ actions = [event.actions for event in m.event_combination]
self.assertEqual(expected_actions, actions)
- # copy keeps the event action
+ # copy keeps the event actions
m2 = m.copy()
- actions = [event.action for event in m2.event_combination]
+ actions = [event.actions for event in m2.event_combination]
self.assertEqual(expected_actions, actions)
- # changing the combination sets the action
+ # changing the combination sets the actions
m3 = m.copy()
m3.event_combination = "1,2,1+2,1,0+3,1,10"
- expected_actions = [EventActions.as_key, EventActions.none, EventActions.as_key]
- actions = [event.action for event in m3.event_combination]
+ expected_actions = [(EventActions.as_key,), (), (EventActions.as_key,)]
+ actions = [event.actions for event in m3.event_combination]
self.assertEqual(expected_actions, actions)
def test_combination_changed_callback(self):
@@ -331,5 +332,57 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
self.assertTrue(m.is_valid())
+class TestUIMapping(unittest.IsolatedAsyncioTestCase):
+ def test_init(self):
+ """should be able to initialize without an error"""
+ UIMapping()
+
+ def test_is_valid(self):
+ """should be invalid at first
+ and become valid once all data is provided"""
+ m = UIMapping()
+ self.assertFalse(m.is_valid())
+
+ m.event_combination = "1,2,3"
+ m.output_symbol = "a"
+ self.assertFalse(m.is_valid())
+ m.target_uinput = "keyboard"
+ self.assertTrue(m.is_valid())
+
+ def test_updates_validation_error(self):
+ m = UIMapping()
+ self.assertIn("2 validation errors for UIMapping", str(m.get_error()))
+ m.event_combination = "1,2,3"
+ m.output_symbol = "a"
+ self.assertIn(
+ "1 validation error for UIMapping\ntarget_uinput", str(m.get_error())
+ )
+ m.target_uinput = "keyboard"
+ self.assertTrue(m.is_valid())
+ self.assertIsNone(m.get_error())
+
+ def test_copy_returns_ui_mapping(self):
+ """copy should also be a UIMapping with all the invalid data"""
+ m = UIMapping()
+ m2 = m.copy()
+ self.assertIsInstance(m2, UIMapping)
+ self.assertEqual(m2.event_combination, EventCombination.empty_combination())
+ self.assertIsNone(m2.target_uinput)
+
+ def test_get_bus_massage(self):
+ m = UIMapping()
+ m2 = m.get_bus_message()
+ self.assertEqual(m2.message_type, MessageType.mapping)
+
+ with self.assertRaises(TypeError):
+ # the massage should be immutable
+ m2.output_symbol = "a"
+ self.assertIsNone(m2.output_symbol)
+
+ # the original should be not immutable
+ m.output_symbol = "a"
+ self.assertEqual(m.output_symbol, "a")
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/unit/test_message_broker.py b/tests/unit/test_message_broker.py
new file mode 100644
index 00000000..42c8963c
--- /dev/null
+++ b/tests/unit/test_message_broker.py
@@ -0,0 +1,79 @@
+import unittest
+from dataclasses import dataclass
+
+from inputremapper.gui.message_broker import MessageBroker, MessageType
+
+
+class Listener:
+ def __init__(self):
+ self.calls = []
+
+ def __call__(self, data):
+ self.calls.append(data)
+
+
+@dataclass
+class Message:
+ message_type: MessageType
+ msg: str
+
+
+class TestMessageBroker(unittest.TestCase):
+ def test_calls_listeners(self):
+ """The correct Listeners get called"""
+ message_broker = MessageBroker()
+ listener = Listener()
+ message_broker.subscribe(MessageType.test1, listener)
+ message_broker.send(Message(MessageType.test1, "foo"))
+ message_broker.send(Message(MessageType.test2, "bar"))
+ self.assertEqual(listener.calls[0], Message(MessageType.test1, "foo"))
+
+ def test_unsubscribe(self):
+ message_broker = MessageBroker()
+ listener = Listener()
+ message_broker.subscribe(MessageType.test1, listener)
+ message_broker.send(Message(MessageType.test1, "a"))
+ message_broker.unsubscribe(listener)
+ message_broker.send(Message(MessageType.test1, "b"))
+ self.assertEqual(len(listener.calls), 1)
+ self.assertEqual(listener.calls[0], Message(MessageType.test1, "a"))
+
+ def test_unsubscribe_unknown_listener(self):
+ """nothing happens if we unsubscribe an unknown listener"""
+ message_broker = MessageBroker()
+ listener1 = Listener()
+ listener2 = Listener()
+ message_broker.subscribe(MessageType.test1, listener1)
+ message_broker.unsubscribe(listener2)
+ message_broker.send(Message(MessageType.test1, "a"))
+ self.assertEqual(listener1.calls[0], Message(MessageType.test1, "a"))
+
+ def test_preserves_order(self):
+ message_broker = MessageBroker()
+ calls = []
+
+ def listener1(_):
+ message_broker.send(Message(MessageType.test2, "f"))
+ calls.append(1)
+
+ def listener2(_):
+ message_broker.send(Message(MessageType.test2, "f"))
+ calls.append(2)
+
+ def listener3(_):
+ message_broker.send(Message(MessageType.test2, "f"))
+ calls.append(3)
+
+ def listener4(_):
+ calls.append(4)
+
+ message_broker.subscribe(MessageType.test1, listener1)
+ message_broker.subscribe(MessageType.test1, listener2)
+ message_broker.subscribe(MessageType.test1, listener3)
+ message_broker.subscribe(MessageType.test2, listener4)
+ message_broker.send(Message(MessageType.test1, ""))
+
+ first = calls[:3]
+ first.sort()
+ self.assertEqual([1, 2, 3], first)
+ self.assertEqual([4, 4, 4], calls[3:])
diff --git a/tests/unit/test_preset.py b/tests/unit/test_preset.py
index 70c26852..e906099c 100644
--- a/tests/unit/test_preset.py
+++ b/tests/unit/test_preset.py
@@ -481,14 +481,11 @@ class TestPreset(unittest.TestCase):
def test_save_load_with_invalid_mappings(self):
ui_preset = Preset(get_config_path("test.json"), mapping_factory=UIMapping)
- # cannot add a mapping without a valid combination
- self.assertRaises(Exception, ui_preset.add, UIMapping())
-
- ui_preset.add(UIMapping(event_combination="1,1,1"))
+ ui_preset.add(UIMapping())
self.assertFalse(ui_preset.is_valid())
# make the mapping valid
- m = ui_preset.get_mapping(EventCombination.from_string("1,1,1"))
+ m = ui_preset.get_mapping(EventCombination.empty_combination())
m.output_symbol = "a"
m.target_uinput = "keyboard"
self.assertTrue(ui_preset.is_valid())
diff --git a/tests/unit/test_presets.py b/tests/unit/test_presets.py
deleted file mode 100644
index 1b889d2c..00000000
--- a/tests/unit/test_presets.py
+++ /dev/null
@@ -1,218 +0,0 @@
-#!/usr/bin/python3
-# -*- coding: utf-8 -*-
-# input-remapper - GUI for device specific keyboard mappings
-# Copyright (C) 2022 sezanzeb
-#
-# 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 .
-
-
-from tests.test import tmp
-
-import os
-import unittest
-import shutil
-import time
-
-from inputremapper.configs.preset import (
- find_newest_preset,
- rename_preset,
- get_any_preset,
- delete_preset,
- get_available_preset_name,
- get_presets,
-)
-from inputremapper.configs.paths import CONFIG_PATH, get_preset_path, touch
-from inputremapper.gui.active_preset import active_preset
-
-
-def create_preset(group_name, name="new preset"):
- name = get_available_preset_name(group_name, name)
- active_preset.clear()
- active_preset.path = get_preset_path(group_name, name)
- active_preset.save()
-
-
-PRESETS = os.path.join(CONFIG_PATH, "presets")
-
-
-class TestPresets(unittest.TestCase):
- def test_get_available_preset_name(self):
- # no filename conflict
- self.assertEqual(get_available_preset_name("_", "qux 2"), "qux 2")
-
- touch(get_preset_path("_", "qux 5"))
- self.assertEqual(get_available_preset_name("_", "qux 5"), "qux 6")
- touch(get_preset_path("_", "qux"))
- self.assertEqual(get_available_preset_name("_", "qux"), "qux 2")
- touch(get_preset_path("_", "qux1"))
- self.assertEqual(get_available_preset_name("_", "qux1"), "qux1 2")
- touch(get_preset_path("_", "qux 2 3"))
- self.assertEqual(get_available_preset_name("_", "qux 2 3"), "qux 2 4")
-
- touch(get_preset_path("_", "qux 5"))
- self.assertEqual(get_available_preset_name("_", "qux 5", True), "qux 5 copy")
- touch(get_preset_path("_", "qux 5 copy"))
- self.assertEqual(get_available_preset_name("_", "qux 5", True), "qux 5 copy 2")
- touch(get_preset_path("_", "qux 5 copy 2"))
- self.assertEqual(get_available_preset_name("_", "qux 5", True), "qux 5 copy 3")
-
- touch(get_preset_path("_", "qux 5copy"))
- self.assertEqual(
- get_available_preset_name("_", "qux 5copy", True),
- "qux 5copy copy",
- )
- touch(get_preset_path("_", "qux 5copy 2"))
- self.assertEqual(
- get_available_preset_name("_", "qux 5copy 2", True),
- "qux 5copy 2 copy",
- )
- touch(get_preset_path("_", "qux 5copy 2 copy"))
- self.assertEqual(
- get_available_preset_name("_", "qux 5copy 2 copy", True),
- "qux 5copy 2 copy 2",
- )
-
-
-class TestCreatePreset(unittest.TestCase):
- def tearDown(self):
- if os.path.exists(tmp):
- shutil.rmtree(tmp)
-
- def test_create_preset_1(self):
- self.assertEqual(get_any_preset(), ("Foo Device", None))
- create_preset("Foo Device")
- self.assertEqual(get_any_preset(), ("Foo Device", "new preset"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
-
- def test_create_preset_2(self):
- create_preset("Foo Device")
- create_preset("Foo Device")
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset 2.json"))
-
- def test_create_preset_3(self):
- create_preset("Foo Device", "pre set")
- create_preset("Foo Device", "pre set")
- create_preset("Foo Device", "pre set")
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/pre set.json"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/pre set 2.json"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/pre set 3.json"))
-
-
-class TestDeletePreset(unittest.TestCase):
- def tearDown(self):
- if os.path.exists(tmp):
- shutil.rmtree(tmp)
-
- def test_delete_preset(self):
- create_preset("Foo Device")
- create_preset("Foo Device")
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
- delete_preset("Foo Device", "new preset")
- self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device"))
- delete_preset("Foo Device", "new preset 2")
- self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/new preset.json"))
- self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/new preset 2.json"))
- # if no preset in the directory, remove the directory
- self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device"))
-
-
-class TestRenamePreset(unittest.TestCase):
- def tearDown(self):
- if os.path.exists(tmp):
- shutil.rmtree(tmp)
-
- def test_rename_preset(self):
- create_preset("Foo Device", "preset 1")
- create_preset("Foo Device", "preset 2")
- create_preset("Foo Device", "foobar")
- rename_preset("Foo Device", "preset 1", "foobar")
- rename_preset("Foo Device", "preset 2", "foobar")
- self.assertFalse(os.path.exists(f"{PRESETS}/Foo Device/preset 1.json"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/foobar.json"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/foobar 2.json"))
- self.assertTrue(os.path.exists(f"{PRESETS}/Foo Device/foobar 3.json"))
-
-
-class TestFindPresets(unittest.TestCase):
- def tearDown(self):
- if os.path.exists(tmp):
- shutil.rmtree(tmp)
-
- def test_get_presets(self):
- os.makedirs(os.path.join(PRESETS, "1234"))
-
- os.mknod(os.path.join(PRESETS, "1234", "picture.png"))
- self.assertEqual(len(get_presets("1234")), 0)
-
- os.mknod(os.path.join(PRESETS, "1234", "foo bar 1.json"))
- time.sleep(0.01)
- os.mknod(os.path.join(PRESETS, "1234", "foo bar 2.json"))
- # the newest to the front
- self.assertListEqual(get_presets("1234"), ["foo bar 2", "foo bar 1"])
-
- def test_find_newest_preset_1(self):
- create_preset("Foo Device", "preset 1")
- time.sleep(0.01)
- create_preset("Bar Device", "preset 2")
-
- # not a preset, ignore
- time.sleep(0.01)
- path = os.path.join(PRESETS, "Bar Device", "picture.png")
- os.mknod(path)
-
- self.assertEqual(find_newest_preset(), ("Bar Device", "preset 2"))
-
- def test_find_newest_preset_2(self):
- os.makedirs(f"{PRESETS}/Foo Device")
- time.sleep(0.01)
- os.makedirs(f"{PRESETS}/device_2")
- # takes the first one that the test-fake returns
- self.assertEqual(find_newest_preset(), ("Foo Device", None))
-
- def test_find_newest_preset_3(self):
- os.makedirs(f"{PRESETS}/Foo Device")
- self.assertEqual(find_newest_preset(), ("Foo Device", None))
-
- def test_find_newest_preset_4(self):
- create_preset("Foo Device", "preset 1")
- self.assertEqual(find_newest_preset(), ("Foo Device", "preset 1"))
-
- def test_find_newest_preset_5(self):
- create_preset("Foo Device", "preset 1")
- time.sleep(0.01)
- create_preset("unknown device 3", "preset 3")
- self.assertEqual(find_newest_preset(), ("Foo Device", "preset 1"))
-
- def test_find_newest_preset_6(self):
- # takes the first one that the test-fake returns
- self.assertEqual(find_newest_preset(), ("Foo Device", None))
-
- def test_find_newest_preset_7(self):
- self.assertEqual(find_newest_preset("Foo Device"), ("Foo Device", None))
-
- def test_find_newest_preset_8(self):
- create_preset("Foo Device", "preset 1")
- time.sleep(0.01)
- create_preset("Foo Device", "preset 3")
- time.sleep(0.01)
- create_preset("Bar Device", "preset 2")
- self.assertEqual(find_newest_preset("Foo Device"), ("Foo Device", "preset 3"))
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py
index d802c20e..3621a199 100644
--- a/tests/unit/test_reader.py
+++ b/tests/unit/test_reader.py
@@ -17,20 +17,26 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see .
-
-
+import json
+from typing import List
+
+from inputremapper.gui.message_broker import (
+ MessageBroker,
+ MessageType,
+ CombinationRecorded,
+ Signal,
+)
from tests.test import (
new_event,
push_events,
- send_event_to_reader,
EVENT_READ_TIMEOUT,
START_READING_DELAY,
quick_cleanup,
MAX_ABS,
+ MIN_ABS,
)
import unittest
-from unittest import mock
import time
import multiprocessing
@@ -48,21 +54,27 @@ from evdev.ecodes import (
REL_X,
ABS_X,
ABS_RZ,
+ REL_HWHEEL,
)
-from inputremapper.gui.reader import reader, will_report_up
-from inputremapper.gui.active_preset import active_preset
-from inputremapper.configs.global_config import BUTTONS, MOUSE
+from inputremapper.gui.reader import Reader
from inputremapper.event_combination import EventCombination
from inputremapper.gui.helper import RootHelper
-from inputremapper.groups import groups
-
+from inputremapper.groups import _Groups, DeviceType
CODE_1 = 100
CODE_2 = 101
CODE_3 = 102
+class Listener:
+ def __init__(self):
+ self.calls: List = []
+
+ def __call__(self, data):
+ self.calls.append(data)
+
+
def wait(func, timeout=1.0):
"""Wait for func to return True."""
iterations = 0
@@ -77,189 +89,257 @@ def wait(func, timeout=1.0):
class TestReader(unittest.TestCase):
def setUp(self):
self.helper = None
+ self.groups = _Groups()
+ self.message_broker = MessageBroker()
+ self.reader = Reader(self.message_broker, self.groups)
def tearDown(self):
quick_cleanup()
+ try:
+ self.reader.terminate()
+ except (BrokenPipeError, OSError):
+ pass
+
if self.helper is not None:
self.helper.join()
- groups.refresh()
- def create_helper(self):
+ def create_helper(self, groups: _Groups = None):
# this will cause pending events to be copied over to the helper
# process
+ if not groups:
+ groups = self.groups
+
def start_helper():
- helper = RootHelper()
+ helper = RootHelper(groups)
helper.run()
self.helper = multiprocessing.Process(target=start_helper)
self.helper.start()
time.sleep(0.1)
- def test_will_report_up(self):
- self.assertFalse(will_report_up(EV_REL))
- self.assertTrue(will_report_up(EV_ABS))
- self.assertTrue(will_report_up(EV_KEY))
+ def test_reading(self):
+ l1 = Listener()
+ l2 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+ self.message_broker.subscribe(MessageType.recording_finished, l2)
+ self.create_helper()
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
- def test_reading_1(self):
- # a single event
push_events("Foo Device 2", [new_event(EV_ABS, ABS_HAT0X, 1)])
+
+ # relative axis events should be released automagically after 0.3s
+ push_events("Foo Device 2", [new_event(EV_REL, REL_X, 5)])
+ time.sleep(0.2)
+ # read all pending events. Having a glib mainloop would be better,
+ # as it would call read automatically periodically
+ self.reader._read()
+ self.assertEqual(
+ [
+ CombinationRecorded(EventCombination.from_string("3,16,1")),
+ CombinationRecorded(EventCombination.from_string("3,16,1+2,0,1")),
+ ],
+ l1.calls,
+ )
+
+ # release the hat switch should emit the recording finished event
+ # as both the hat and relative axis are released by now
+ push_events("Foo Device 2", [new_event(EV_ABS, ABS_HAT0X, 0)])
+ time.sleep(0.3)
+ self.reader._read()
+ self.assertEqual([Signal(MessageType.recording_finished)], l2.calls)
+
+ def test_should_release_relative_axis(self):
+ # the timeout is set to 0.3s
+ l1 = Listener()
+ l2 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+ self.message_broker.subscribe(MessageType.recording_finished, l2)
+ self.create_helper()
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
+
+ push_events("Foo Device 2", [new_event(EV_REL, REL_X, -5)])
+ time.sleep(0.1)
+ self.reader._read()
+
+ self.assertEqual(
+ [CombinationRecorded(EventCombination.from_string("2,0,-1"))],
+ l1.calls,
+ )
+ self.assertEqual([], l2.calls) # no stop recording yet
+
+ time.sleep(0.3)
+ self.reader._read()
+ self.assertEqual([Signal(MessageType.recording_finished)], l2.calls)
+
+ def test_should_not_trigger_at_low_speed_for_rel_axis(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+ self.create_helper()
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
+
+ push_events("Foo Device 2", [new_event(EV_REL, REL_X, -1)])
+ time.sleep(0.1)
+ self.reader._read()
+ self.assertEqual(0, len(l1.calls))
+
+ def test_should_trigger_wheel_at_low_speed(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+ self.create_helper()
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
+
push_events(
"Foo Device 2",
- [new_event(EV_ABS, REL_X, 1)],
- ) # mouse movements are ignored
- self.create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
- time.sleep(0.2)
- self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_HAT0X, 1)))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
+ [new_event(EV_REL, REL_WHEEL, -1), new_event(EV_REL, REL_HWHEEL, 1)],
+ )
+ time.sleep(0.1)
+ self.reader._read()
- def test_reading_wheel(self):
- # will be treated as released automatically at some point
+ self.assertEqual(
+ [
+ CombinationRecorded(EventCombination.from_string("2,8,-1")),
+ CombinationRecorded(EventCombination.from_string("2,8,-1+2,6,1")),
+ ],
+ l1.calls,
+ )
+
+ def test_wont_emit_the_same_combination_twice(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
self.create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
-
- send_event_to_reader(new_event(EV_REL, REL_WHEEL, 0))
- self.assertIsNone(reader.read())
-
- send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1))
- result = reader.read()
- self.assertIsInstance(result, EventCombination)
- self.assertIsInstance(result, tuple)
- self.assertEqual(result, EventCombination((EV_REL, REL_WHEEL, 1)))
- self.assertEqual(result, ((EV_REL, REL_WHEEL, 1),))
- self.assertNotEqual(
- result,
- EventCombination(((EV_REL, REL_WHEEL, 1), (1, 1, 1))),
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
+
+ push_events("Foo Device 2", [new_event(EV_KEY, KEY_A, 1)])
+ time.sleep(0.1)
+ self.reader._read()
+ # the duplicate event should be ignored
+ push_events("Foo Device 2", [new_event(EV_KEY, KEY_A, 1)])
+ time.sleep(0.1)
+ self.reader._read()
+
+ self.assertEqual(
+ [CombinationRecorded(EventCombination.from_string("1,30,1"))],
+ l1.calls,
)
- # it won't return the same event twice
- self.assertEqual(reader.read(), None)
+ def test_should_read_absolut_axis(self):
+ l1 = Listener()
+ l2 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+ self.message_broker.subscribe(MessageType.recording_finished, l2)
+ self.create_helper()
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
- # but it is still remembered unreleased
- self.assertEqual(len(reader._unreleased), 1)
+ # over 30% should trigger
+ push_events("Foo Device 2", [new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.4))])
+ time.sleep(0.1)
+ self.reader._read()
self.assertEqual(
- reader.get_unreleased_keys(),
- EventCombination((EV_REL, REL_WHEEL, 1)),
+ [CombinationRecorded(EventCombination.from_string("3,0,1"))],
+ l1.calls,
)
- self.assertIsInstance(reader.get_unreleased_keys(), EventCombination)
-
- # as long as new wheel events arrive, it is considered unreleased
- for _ in range(10):
- send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
-
- # read a few more times, at some point it is treated as unreleased
- for _ in range(4):
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 0)
- self.assertIsNone(reader.get_unreleased_keys())
-
- """Combinations"""
-
- send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1, 1000))
- send_event_to_reader(new_event(EV_KEY, KEY_COMMA, 1, 1001))
- combi_1 = EventCombination(((EV_REL, REL_WHEEL, 1), (EV_KEY, KEY_COMMA, 1)))
- combi_2 = EventCombination(((EV_KEY, KEY_COMMA, 1), (EV_KEY, KEY_A, 1)))
- read = reader.read()
- self.assertEqual(read, combi_1)
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 2)
- self.assertEqual(reader.get_unreleased_keys(), combi_1)
-
- # don't send new wheel down events, it should get released again
- i = 0
- while len(reader._unreleased) == 2:
- read = reader.read()
- if i == 100:
- raise AssertionError("Did not release the wheel")
- i += 1
- # and only the comma remains. However, a changed combination is
- # only returned when a new key is pressed. Only then the pressed
- # down keys are collected in a new Key object.
- self.assertEqual(read, None)
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
- self.assertEqual(reader.get_unreleased_keys(), EventCombination(combi_1[1]))
-
- # press down a new key, now it will return a different combination
- send_event_to_reader(new_event(EV_KEY, KEY_A, 1, 1002))
- self.assertEqual(reader.read(), combi_2)
- self.assertEqual(len(reader._unreleased), 2)
-
- # release all of them
- send_event_to_reader(new_event(EV_KEY, KEY_COMMA, 0))
- send_event_to_reader(new_event(EV_KEY, KEY_A, 0))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 0)
- self.assertEqual(reader.get_unreleased_keys(), None)
-
- def test_change_wheel_direction(self):
- # not just wheel, anything that suddenly reports a different value.
- # as long as type and code are equal its the same key, so there is no
- # way both directions can be held down.
- self.assertEqual(reader.read(), None)
+ self.assertEqual([], l2.calls) # no stop recording yet
+
+ # less the 30% should release
+ push_events("Foo Device 2", [new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.2))])
+ time.sleep(0.1)
+ self.reader._read()
+ self.assertEqual(
+ [CombinationRecorded(EventCombination.from_string("3,0,1"))],
+ l1.calls,
+ )
+ self.assertEqual([Signal(MessageType.recording_finished)], l2.calls)
+
+ def test_should_change_direction(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
self.create_helper()
- self.assertEqual(reader.read(), None)
- reader.start_reading(groups.find(key="Foo Device 2"))
- self.assertEqual(reader.read(), None)
-
- send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1))
- self.assertEqual(reader.read(), EventCombination((EV_REL, REL_WHEEL, 1)))
- self.assertEqual(len(reader._unreleased), 1)
- self.assertEqual(reader.read(), None)
-
- send_event_to_reader(new_event(EV_REL, REL_WHEEL, -1))
- self.assertEqual(reader.read(), EventCombination((EV_REL, REL_WHEEL, -1)))
- # notice that this is no combination of two sides, the previous
- # entry in unreleased has to get overwritten. So there is still only
- # one element in it.
- self.assertEqual(len(reader._unreleased), 1)
- self.assertEqual(reader.read(), None)
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
+
+ push_events(
+ "Foo Device 2",
+ [
+ new_event(EV_KEY, KEY_A, 1),
+ new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.4)),
+ new_event(EV_KEY, KEY_COMMA, 1),
+ new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.1)),
+ new_event(EV_ABS, ABS_X, int(MIN_ABS * 0.4)),
+ ],
+ )
+ time.sleep(0.1)
+ self.reader._read()
+ self.assertEqual(
+ [
+ CombinationRecorded(EventCombination.from_string("1,30,1")),
+ CombinationRecorded(EventCombination.from_string("1,30,1+3,0,1")),
+ CombinationRecorded(
+ EventCombination.from_string("1,30,1+3,0,1+1,51,1")
+ ),
+ CombinationRecorded(
+ EventCombination.from_string("1,30,1+3,0,-1+1,51,1")
+ ),
+ ],
+ l1.calls,
+ )
def test_change_device(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+
push_events(
"Foo Device 2",
[
new_event(EV_KEY, 1, 1),
]
- * 100,
+ * 10,
)
push_events(
"Bar Device",
[
new_event(EV_KEY, 2, 1),
+ new_event(EV_KEY, 2, 0),
]
- * 100,
+ * 3,
)
self.create_helper()
-
- reader.start_reading(groups.find(key="Foo Device 2"))
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
time.sleep(0.1)
- self.assertEqual(reader.read(), EventCombination((EV_KEY, 1, 1)))
-
- reader.start_reading(groups.find(name="Bar Device"))
+ self.reader._read()
+ self.assertEqual(l1.calls[0].combination, EventCombination((EV_KEY, 1, 1)))
- # it's plausible that right after sending the new read command more
- # events from the old device might still appear. Give the helper
- # some time to handle the new command.
+ self.reader.set_group(self.groups.find(name="Bar Device"))
time.sleep(0.1)
- reader.clear()
+ self.reader._read()
+ # we did not get the event from the "Bar Device" because the group change
+ # stopped the recording
+ self.assertEqual(len(l1.calls), 1)
+
+ self.reader.start_recorder()
+ push_events("Bar Device", [new_event(EV_KEY, 2, 1)])
time.sleep(0.1)
- self.assertEqual(reader.read(), EventCombination((EV_KEY, 2, 1)))
+ self.reader._read()
+ self.assertEqual(l1.calls[1].combination, EventCombination((EV_KEY, 2, 1)))
def test_reading_2(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
# a combination of events
push_events(
"Foo Device 2",
[
new_event(EV_KEY, CODE_1, 1, 10000.1234),
new_event(EV_KEY, CODE_3, 1, 10001.1234),
- new_event(EV_ABS, ABS_HAT0X, -1, 10002.1234),
],
)
@@ -270,131 +350,33 @@ class TestReader(unittest.TestCase):
# refresh was called as expected
pipe[1].send("refreshed")
- with mock.patch.object(groups, "refresh", refresh):
- self.create_helper()
+ groups = _Groups()
+ groups.refresh = refresh
+ self.create_helper(groups)
- reader.start_reading(groups.find(key="Foo Device 2"))
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
# sending anything arbitrary does not stop the helper
- reader._commands.send(856794)
+ self.reader._commands.send(856794)
time.sleep(0.2)
+ push_events("Foo Device 2", [new_event(EV_ABS, ABS_HAT0X, -1, 10002.1234)])
+ time.sleep(0.1)
# but it makes it look for new devices because maybe its list of
- # groups is not up-to-date
+ # self.groups is not up-to-date
self.assertTrue(pipe[0].poll())
self.assertEqual(pipe[0].recv(), "refreshed")
+ self.reader._read()
self.assertEqual(
- reader.read(),
+ l1.calls[-1].combination,
((EV_KEY, CODE_1, 1), (EV_KEY, CODE_3, 1), (EV_ABS, ABS_HAT0X, -1)),
)
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 3)
-
- def test_reading_3(self):
- self.create_helper()
- # a combination of events via Socket with reads inbetween
- reader.start_reading(groups.find(name="gamepad"))
-
- send_event_to_reader(new_event(EV_KEY, CODE_1, 1, 1001))
- self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_1, 1)))
-
- # active_preset.set("gamepad.joystick.left_purpose", BUTTONS)
- send_event_to_reader(new_event(EV_ABS, ABS_Y, 1, 1002))
- self.assertEqual(
- reader.read(),
- EventCombination(((EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1))),
- )
-
- send_event_to_reader(new_event(EV_ABS, ABS_HAT0X, -1, 1003))
- self.assertEqual(
- reader.read(),
- EventCombination(
- ((EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1), (EV_ABS, ABS_HAT0X, -1)),
- ),
- )
-
- # adding duplicate down events won't report a different combination.
- # import for triggers, as they keep reporting more down-events before
- # they are released
- send_event_to_reader(new_event(EV_ABS, ABS_Y, 1, 1005))
- self.assertEqual(reader.read(), None)
- send_event_to_reader(new_event(EV_ABS, ABS_HAT0X, -1, 1006))
- self.assertEqual(reader.read(), None)
-
- send_event_to_reader(new_event(EV_KEY, CODE_1, 0, 1004))
- read = reader.read()
- self.assertEqual(read, None)
-
- send_event_to_reader(new_event(EV_ABS, ABS_Y, 0, 1007))
- self.assertEqual(reader.read(), None)
-
- send_event_to_reader(new_event(EV_KEY, ABS_HAT0X, 0, 1008))
- self.assertEqual(reader.read(), None)
-
- def test_reads_joysticks(self):
- # if their purpose is "buttons"
- # active_preset.set("gamepad.joystick.left_purpose", BUTTONS)
- push_events(
- "gamepad",
- [
- new_event(EV_ABS, ABS_Y, MAX_ABS),
- # the value of that one is interpreted as release, because
- # it is too small
- new_event(EV_ABS, ABS_X, MAX_ABS // 10),
- ],
- )
- self.create_helper()
-
- reader.start_reading(groups.find(name="gamepad"))
- time.sleep(0.2)
- self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_Y, 1)))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
-
- reader._unreleased = {}
- # active_preset.set("gamepad.joystick.left_purpose", MOUSE)
- push_events("gamepad", [new_event(EV_ABS, ABS_Y, MAX_ABS)])
- self.create_helper()
-
- reader.start_reading(groups.find(name="gamepad"))
- time.sleep(0.1)
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 0)
-
- def test_combine_triggers(self):
- reader.start_reading(groups.find(key="Foo Device 2"))
-
- i = 0
-
- def next_timestamp():
- nonlocal i
- i += 1
- return time.time() + i
-
- # based on an observed bug
- send_event_to_reader(new_event(3, 1, 0, next_timestamp()))
- send_event_to_reader(new_event(3, 0, 0, next_timestamp()))
- send_event_to_reader(new_event(3, 2, 1, next_timestamp()))
- self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_Z, 1)))
- send_event_to_reader(new_event(3, 0, 0, next_timestamp()))
- send_event_to_reader(new_event(3, 5, 1, next_timestamp()))
- self.assertEqual(
- reader.read(),
- EventCombination(((EV_ABS, ABS_Z, 1), (EV_ABS, ABS_RZ, 1))),
- )
- send_event_to_reader(new_event(3, 5, 0, next_timestamp()))
- send_event_to_reader(new_event(3, 0, 0, next_timestamp()))
- send_event_to_reader(new_event(3, 1, 0, next_timestamp()))
- self.assertEqual(reader.read(), None)
- send_event_to_reader(new_event(3, 2, 1, next_timestamp()))
- send_event_to_reader(new_event(3, 1, 0, next_timestamp()))
- send_event_to_reader(new_event(3, 0, 0, next_timestamp()))
- # due to not properly handling the duplicate down event it cleared
- # the combination and returned it. Instead it should report None
- # and by doing that keep the previous combination.
- self.assertEqual(reader.read(), None)
def test_blacklisted_events(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+
push_events(
"Foo Device 2",
[
@@ -404,26 +386,34 @@ class TestReader(unittest.TestCase):
],
)
self.create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
time.sleep(0.1)
- self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_2, 1)))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
+ self.reader._read()
+ self.assertEqual(
+ l1.calls[-1].combination, EventCombination((EV_KEY, CODE_2, 1))
+ )
def test_ignore_value_2(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
# this is not a combination, because (EV_KEY CODE_3, 2) is ignored
push_events(
"Foo Device 2",
[new_event(EV_ABS, ABS_HAT0X, 1), new_event(EV_KEY, CODE_3, 2)],
)
self.create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
time.sleep(0.2)
- self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_HAT0X, 1)))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
+ self.reader._read()
+ self.assertEqual(
+ l1.calls[-1].combination, EventCombination((EV_ABS, ABS_HAT0X, 1))
+ )
def test_reading_ignore_up(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
push_events(
"Foo Device 2",
[
@@ -433,32 +423,18 @@ class TestReader(unittest.TestCase):
],
)
self.create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
+ self.reader.start_recorder()
time.sleep(0.1)
- self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_2, 1)))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
-
- def test_reading_ignore_duplicate_down(self):
- send_event_to_reader(new_event(EV_ABS, ABS_Z, 1, 10))
-
- self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_Z, 1)))
- self.assertEqual(reader.read(), None)
-
- # duplicate
- send_event_to_reader(new_event(EV_ABS, ABS_Z, 1, 10))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
- self.assertEqual(len(reader.get_unreleased_keys()), 1)
- self.assertIsInstance(reader.get_unreleased_keys(), EventCombination)
-
- # release
- send_event_to_reader(new_event(EV_ABS, ABS_Z, 0, 10))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 0)
- self.assertIsNone(reader.get_unreleased_keys())
+ self.reader._read()
+ self.assertEqual(
+ l1.calls[-1].combination, EventCombination((EV_KEY, CODE_2, 1))
+ )
def test_wrong_device(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
+
push_events(
"Foo Device 2",
[
@@ -468,16 +444,19 @@ class TestReader(unittest.TestCase):
],
)
self.create_helper()
- reader.start_reading(groups.find(name="Bar Device"))
+ self.reader.set_group(self.groups.find(name="Bar Device"))
+ self.reader.start_recorder()
time.sleep(EVENT_READ_TIMEOUT * 5)
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 0)
+ self.reader._read()
+ self.assertEqual(len(l1.calls), 0)
def test_inputremapper_devices(self):
# Don't read from inputremapper devices, their keycodes are not
# representative for the original key. As long as this is not
# intentionally programmed it won't even do that. But it was at some
# point.
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.combination_recorded, l1)
push_events(
"input-remapper Bar Device",
[
@@ -487,106 +466,105 @@ class TestReader(unittest.TestCase):
],
)
self.create_helper()
- reader.start_reading(groups.find(name="Bar Device"))
+ self.reader.set_group(self.groups.find(name="Bar Device"))
+ self.reader.start_recorder()
time.sleep(EVENT_READ_TIMEOUT * 5)
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 0)
-
- def test_clear(self):
- push_events(
- "Foo Device 2",
- [
- new_event(EV_KEY, CODE_1, 1),
- new_event(EV_KEY, CODE_2, 1),
- new_event(EV_KEY, CODE_3, 1),
- ]
- * 15,
- )
-
- self.create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
- time.sleep(START_READING_DELAY + EVENT_READ_TIMEOUT * 3)
-
- reader.read()
- self.assertEqual(len(reader._unreleased), 3)
- self.assertIsNotNone(reader.previous_event)
- self.assertIsNotNone(reader.previous_result)
-
- # make the helper send more events to the reader
- time.sleep(EVENT_READ_TIMEOUT * 2)
- self.assertTrue(reader._results.poll())
- reader.clear()
-
- self.assertFalse(reader._results.poll())
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 0)
- self.assertIsNone(reader.get_unreleased_keys())
- self.assertIsNone(reader.previous_event)
- self.assertIsNone(reader.previous_result)
- self.tearDown()
-
- def test_switch_device(self):
- push_events("Bar Device", [new_event(EV_KEY, CODE_1, 1)])
- push_events("Foo Device 2", [new_event(EV_KEY, CODE_3, 1)])
- self.create_helper()
-
- reader.start_reading(groups.find(name="Bar Device"))
- self.assertFalse(reader._results.poll())
- self.assertEqual(reader.group.name, "Bar Device")
- time.sleep(EVENT_READ_TIMEOUT * 5)
-
- self.assertTrue(reader._results.poll())
- reader.start_reading(groups.find(key="Foo Device 2"))
- self.assertEqual(reader.group.name, "Foo Device")
- self.assertFalse(reader._results.poll()) # pipe resets
-
- time.sleep(EVENT_READ_TIMEOUT * 5)
- self.assertTrue(reader._results.poll())
-
- self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_3, 1)))
- self.assertEqual(reader.read(), None)
- self.assertEqual(len(reader._unreleased), 1)
+ self.reader._read()
+ self.assertEqual(len(l1.calls), 0)
def test_terminate(self):
self.create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
+ self.reader.set_group(self.groups.find(key="Foo Device 2"))
push_events("Foo Device 2", [new_event(EV_KEY, CODE_3, 1)])
time.sleep(START_READING_DELAY + EVENT_READ_TIMEOUT)
- self.assertTrue(reader._results.poll())
+ self.assertTrue(self.reader._results.poll())
- reader.terminate()
- reader.clear()
+ self.reader.terminate()
time.sleep(EVENT_READ_TIMEOUT)
+ self.assertFalse(self.reader._results.poll())
# no new events arrive after terminating
push_events("Foo Device 2", [new_event(EV_KEY, CODE_3, 1)])
time.sleep(EVENT_READ_TIMEOUT * 3)
- self.assertFalse(reader._results.poll())
+ self.assertFalse(self.reader._results.poll())
def test_are_new_groups_available(self):
+ l1 = Listener()
+ self.message_broker.subscribe(MessageType.groups, l1)
self.create_helper()
- groups.set_groups({})
+ self.reader.groups.set_groups({})
+ time.sleep(0.1) # let the helper send the groups
# read stuff from the helper, which includes the devices
- self.assertFalse(reader.are_new_groups_available())
- reader.read()
-
- self.assertTrue(reader.are_new_groups_available())
- # a bit weird, but it assumes the gui handled that and returns
- # false afterwards
- self.assertFalse(reader.are_new_groups_available())
-
- # send the same devices again
- reader._get_event({"type": "groups", "message": groups.dumps()})
- self.assertFalse(reader.are_new_groups_available())
-
- # send changed devices
- message = groups.dumps()
- message = message.replace("Foo Device", "foo_device")
- reader._get_event({"type": "groups", "message": message})
- self.assertTrue(reader.are_new_groups_available())
- self.assertFalse(reader.are_new_groups_available())
+ self.assertEqual("[]", self.reader.groups.dumps())
+ self.reader._read()
+
+ self.assertEqual(
+ self.reader.groups.dumps(),
+ json.dumps(
+ [
+ json.dumps(
+ {
+ "paths": [
+ "/dev/input/event1",
+ ],
+ "names": ["Foo Device"],
+ "types": [DeviceType.KEYBOARD],
+ "key": "Foo Device",
+ }
+ ),
+ json.dumps(
+ {
+ "paths": [
+ "/dev/input/event11",
+ "/dev/input/event10",
+ "/dev/input/event13",
+ "/dev/input/event15",
+ ],
+ "names": [
+ "Foo Device foo",
+ "Foo Device",
+ "Foo Device",
+ "Foo Device bar",
+ ],
+ "types": [
+ DeviceType.GAMEPAD,
+ DeviceType.KEYBOARD,
+ DeviceType.MOUSE,
+ ],
+ "key": "Foo Device 2",
+ }
+ ),
+ json.dumps(
+ {
+ "paths": ["/dev/input/event20"],
+ "names": ["Bar Device"],
+ "types": [DeviceType.KEYBOARD],
+ "key": "Bar Device",
+ }
+ ),
+ json.dumps(
+ {
+ "paths": ["/dev/input/event30"],
+ "names": ["gamepad"],
+ "types": [DeviceType.GAMEPAD],
+ "key": "gamepad",
+ }
+ ),
+ json.dumps(
+ {
+ "paths": ["/dev/input/event40"],
+ "names": ["input-remapper Bar Device"],
+ "types": [DeviceType.KEYBOARD],
+ "key": "input-remapper Bar Device",
+ }
+ ),
+ ]
+ ),
+ )
+
+ self.assertEqual(len(l1.calls), 1) # ensure we got the event
if __name__ == "__main__":
diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py
index 7529f1a8..5d7ee1eb 100644
--- a/tests/unit/test_test.py
+++ b/tests/unit/test_test.py
@@ -17,8 +17,7 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see .
-
-
+from inputremapper.gui.message_broker import MessageBroker
from tests.test import (
InputDevice,
quick_cleanup,
@@ -38,8 +37,8 @@ import multiprocessing
import evdev
from evdev.ecodes import EV_ABS, EV_KEY
-from inputremapper.groups import groups
-from inputremapper.gui.reader import reader
+from inputremapper.groups import groups, _Groups
+from inputremapper.gui.reader import Reader
from inputremapper.gui.helper import RootHelper
@@ -89,12 +88,15 @@ class TestTest(unittest.TestCase):
Using push_events after the helper is already forked should work,
as well as using push_event twice
"""
+ reader = Reader(MessageBroker(), groups)
def create_helper():
# this will cause pending events to be copied over to the helper
# process
def start_helper():
- helper = RootHelper()
+ # there is no point in using the global groups object
+ # because the helper runs in a different process
+ helper = RootHelper(_Groups())
helper.run()
self.helper = multiprocessing.Process(target=start_helper)
@@ -108,24 +110,27 @@ class TestTest(unittest.TestCase):
if reader._results.poll():
break
- event = new_event(EV_KEY, 102, 1)
create_helper()
- reader.start_reading(groups.find(key="Foo Device 2"))
+ reader.set_group(groups.find(key="Foo Device 2"))
time.sleep(START_READING_DELAY)
+ event = new_event(EV_KEY, 102, 1)
push_events("Foo Device 2", [event])
wait_for_results()
self.assertTrue(reader._results.poll())
- reader.clear()
+ reader._read()
self.assertFalse(reader._results.poll())
# can push more events to the helper that is inside a separate
# process, which end up being sent to the reader
+ event = new_event(EV_KEY, 102, 0)
push_events("Foo Device 2", [event])
wait_for_results()
self.assertTrue(reader._results.poll())
+ reader.terminate()
+
if __name__ == "__main__":
unittest.main()