diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index be24ff8bf..8dfbc31fd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,5 +21,5 @@ jobs: pip install black - name: Analysing the code with black --check --diff run: | + black --version black --check --diff ./inputremapper ./tests - diff --git a/README.md b/README.md index fa4e2ade9..08ce3b74f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Dependencies: `python3-evdev` ≥1.3.0, `gtksourceview4`, `python3-devel`, `pyth Python packages need to be installed globally for the service to be able to import them. Don't use `--user` +Conda can cause problems due to changed python paths and versions. + ```bash sudo pip install evdev -U # If newest version not in distros repo sudo pip uninstall key-mapper # In case the old package is still installed diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 606cf1c12..fd78af46b 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -175,6 +175,7 @@ def internals(options): # daemonize cmd = f'{cmd} &' + logger.debug(f'Running `{cmd}`') os.system(cmd) diff --git a/bin/input-remapper-reader-service b/bin/input-remapper-reader-service index 426809c20..db6e5d1d1 100755 --- a/bin/input-remapper-reader-service +++ b/bin/input-remapper-reader-service @@ -20,8 +20,7 @@ """Starts the root reader-service.""" - - +import asyncio import os import sys import atexit @@ -54,7 +53,7 @@ if __name__ == '__main__': os.kill(os.getpid(), signal.SIGKILL) atexit.register(on_exit) - # TODO import `groups` instead? groups = _Groups() reader_service = ReaderService(groups) - reader_service.run() + loop = asyncio.get_event_loop() + loop.run_until_complete(reader_service.run()) diff --git a/inputremapper/configs/input_config.py b/inputremapper/configs/input_config.py index 8f1c264a2..fb4601b6d 100644 --- a/inputremapper/configs/input_config.py +++ b/inputremapper/configs/input_config.py @@ -20,15 +20,18 @@ from __future__ import annotations import itertools -from typing import Tuple, Iterable, Union, List, Dict, Optional, Hashable +from typing import Tuple, Iterable, Union, List, Dict, Optional, Hashable, TypeAlias from evdev import ecodes +from evdev._ecodes import EV_ABS, EV_KEY, EV_REL + from inputremapper.input_event import InputEvent -from pydantic import BaseModel, root_validator, validator, constr +from pydantic import BaseModel, root_validator, validator from inputremapper.configs.system_mapping import system_mapping from inputremapper.gui.messages.message_types import MessageType from inputremapper.logger import logger +from inputremapper.utils import get_evdev_constant_name # having shift in combinations modifies the configured output, # ctrl might not work at all @@ -41,7 +44,9 @@ ecodes.KEY_RIGHTALT, ] -DeviceHash = constr(to_lower=True) +DeviceHash: TypeAlias = str + +EMPTY_TYPE = 99 class InputConfig(BaseModel): @@ -55,9 +60,21 @@ class InputConfig(BaseModel): # origin_hash is a hash to identify a specific /dev/input/eventXX device. # This solves a number of bugs when multiple devices have overlapping capabilities. # see utils.get_device_hash for the exact hashing function - origin_hash: Optional[DeviceHash] = None # type: ignore + origin_hash: Optional[DeviceHash] = None analog_threshold: Optional[int] = None + def __str__(self): + return f"InputConfig {get_evdev_constant_name(self.type, self.code)}" + + def __repr__(self): + return ( + f"" + ) + @property def input_match_hash(self) -> Hashable: """a Hashable object which is intended to match the InputConfig with a @@ -68,6 +85,10 @@ def input_match_hash(self) -> Hashable: """ return self.type, self.code, self.origin_hash + @property + def is_empty(self) -> bool: + return self.type == EMPTY_TYPE + @property def defines_analog_input(self) -> bool: """Whether this defines an analog input""" @@ -122,7 +143,7 @@ def _get_name(self) -> Optional[str]: # if no result, look in the linux combination constants. On a german # keyboard for example z and y are switched, which will therefore # cause the wrong letter to be displayed. - key_name = ecodes.bytype[self.type][self.code] + key_name = get_evdev_constant_name(self.type, self.code) if isinstance(key_name, list): key_name = key_name[0] @@ -223,9 +244,10 @@ def __hash__(self): @validator("analog_threshold") def _ensure_analog_threshold_is_none(cls, analog_threshold): """ensure the analog threshold is none, not zero.""" - if analog_threshold: - return analog_threshold - return None + if analog_threshold == 0 or analog_threshold is None: + return None + + return analog_threshold @root_validator def _remove_analog_threshold_for_key_input(cls, values): @@ -235,35 +257,62 @@ def _remove_analog_threshold_for_key_input(cls, values): values["analog_threshold"] = None return values + @root_validator(pre=True) + def validate_origin_hash(cls, values): + origin_hash = values.get("origin_hash") + if origin_hash is None: + # For new presets, origin_hash should be set. For old ones, it can + # be still missing. A lot of tests didn't set an origin_hash. + if values.get("type") != EMPTY_TYPE: + logger.warning("No origin_hash set for %s", values) + + return values + + values["origin_hash"] = origin_hash.lower() + return values + class Config: allow_mutation = False underscore_attrs_are_private = True InputCombinationInit = Union[ - InputConfig, Iterable[Dict[str, Union[str, int]]], Iterable[InputConfig], ] class InputCombination(Tuple[InputConfig, ...]): - """One or more InputConfig's used to trigger a mapping""" + """One or more InputConfigs used to trigger a mapping.""" # tuple is immutable, therefore we need to override __new__() # https://jfine-python-classes.readthedocs.io/en/latest/subclass-tuple.html def __new__(cls, configs: InputCombinationInit) -> InputCombination: - if isinstance(configs, InputCombination): - return super().__new__(cls, configs) # type: ignore + """Create a new InputCombination. + + Examples + -------- + InputCombination([InputConfig, ...]) + InputCombination([{type: ..., code: ..., value: ...}, ...]) + """ + if not isinstance(configs, Iterable): + raise TypeError("InputCombination requires a list of InputConfigs.") + if isinstance(configs, InputConfig): - return super().__new__(cls, [configs]) # type: ignore + # wrap the argument in square brackets + raise TypeError("InputCombination requires a list of InputConfigs.") validated_configs = [] - for cfg in configs: - if isinstance(cfg, InputConfig): - validated_configs.append(cfg) + for config in configs: + if isinstance(configs, InputEvent): + raise TypeError("InputCombinations require InputConfigs, not Events.") + + if isinstance(config, InputConfig): + validated_configs.append(config) + elif isinstance(config, dict): + validated_configs.append(InputConfig(**config)) else: - validated_configs.append(InputConfig(**cfg)) + raise TypeError(f'Can\'t handle "{config}"') if len(validated_configs) == 0: raise ValueError(f"failed to create InputCombination with {configs = }") @@ -273,10 +322,11 @@ def __new__(cls, configs: InputCombinationInit) -> InputCombination: return super().__new__(cls, validated_configs) # type: ignore def __str__(self): - return " + ".join(event.description(exclude_threshold=True) for event in self) + return f'Combination ({" + ".join(str(event) for event in self)})' def __repr__(self): - return f"" + combination = ", ".join(repr(event) for event in self) + return f"" @classmethod def __get_validators__(cls): @@ -291,6 +341,7 @@ def validate(cls, init_arg) -> InputCombination: return cls(init_arg) def to_config(self) -> Tuple[Dict[str, int], ...]: + """Turn the object into a tuple of dicts.""" return tuple(input_config.dict(exclude_defaults=True) for input_config in self) @classmethod @@ -299,7 +350,32 @@ def empty_combination(cls) -> InputCombination: Useful for the UI to indicate that this combination is not set """ - return cls([{"type": 99, "code": 99, "analog_threshold": 99}]) + return cls([{"type": EMPTY_TYPE, "code": 99, "analog_threshold": 99}]) + + @classmethod + def from_tuples(cls, *tuples): + """Construct an InputCombination from (type, code, analog_threshold) tuples.""" + dicts = [] + for tuple_ in tuples: + if len(tuple_) == 3: + dicts.append( + { + "type": tuple_[0], + "code": tuple_[1], + "analog_threshold": tuple_[2], + } + ) + elif len(tuple_) == 2: + dicts.append( + { + "type": tuple_[0], + "code": tuple_[1], + } + ) + else: + raise TypeError + + return cls(dicts) def is_problematic(self) -> bool: """Is this combination going to work properly on all systems?""" diff --git a/inputremapper/configs/mapping.py b/inputremapper/configs/mapping.py index 72f9d7b97..80484041b 100644 --- a/inputremapper/configs/mapping.py +++ b/inputremapper/configs/mapping.py @@ -52,6 +52,7 @@ from inputremapper.gui.gettext import _ from inputremapper.gui.messages.message_types import MessageType from inputremapper.injection.macros.parse import is_this_a_macro, parse +from inputremapper.utils import get_evdev_constant_name # TODO: remove pydantic VERSION check as soon as we no longer support # Ubuntu 20.04 and with it the ancient pydantic 1.2 @@ -260,9 +261,9 @@ def get_output_type_code(self) -> Optional[Tuple[int, int]]: return EV_KEY, system_mapping.get(self.output_symbol) return None - def get_output_name_constant(self) -> bool: + def get_output_name_constant(self) -> str: """Get the evdev name costant for the output.""" - return evdev.ecodes.bytype[self.output_type][self.output_code] + return get_evdev_constant_name(self.output_type, self.output_code) def is_valid(self) -> bool: """If the mapping is valid.""" @@ -308,6 +309,20 @@ class Mapping(UIMapping): input_combination: InputCombination target_uinput: KnownUinput + @classmethod + def from_combination( + cls, input_combination=None, target_uinput="keyboard", output_symbol="a" + ): + """Convenient function to get a valid mapping.""" + if not input_combination: + input_combination = [{"type": 99, "code": 99, "analog_threshold": 99}] + + return cls( + input_combination=input_combination, + target_uinput=target_uinput, + output_symbol=output_symbol, + ) + def is_valid(self) -> bool: """If the mapping is valid.""" return True diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index d0805a278..609e3071d 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -298,11 +298,11 @@ def _convert_to_individual_mappings(): to [{input_combination: ..., output_symbol: symbol, ...}] """ - for preset_path, old_preset in all_presets(): + for old_preset_path, old_preset in all_presets(): if isinstance(old_preset, list): continue - preset = Preset(preset_path, UIMapping) + migrated_preset = Preset(old_preset_path, UIMapping) if "mapping" in old_preset.keys(): for combination, symbol_target in old_preset["mapping"].items(): logger.info( @@ -324,7 +324,7 @@ def _convert_to_individual_mappings(): target_uinput=symbol_target[1], output_symbol=symbol_target[0], ) - preset.add(mapping) + migrated_preset.add(mapping) if ( "gamepad" in old_preset.keys() @@ -352,10 +352,10 @@ def _convert_to_individual_mappings(): x_config = cfg.copy() y_config = cfg.copy() x_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_X) + [InputConfig(type=EV_ABS, code=ABS_X)] ) y_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_Y) + [InputConfig(type=EV_ABS, code=ABS_Y)] ) x_config["output_code"] = REL_X y_config["output_code"] = REL_Y @@ -364,17 +364,17 @@ def _convert_to_individual_mappings(): if pointer_speed: mapping_x.gain = pointer_speed mapping_y.gain = pointer_speed - preset.add(mapping_x) - preset.add(mapping_y) + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) if right_purpose == "mouse": x_config = cfg.copy() y_config = cfg.copy() x_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_RX) + [InputConfig(type=EV_ABS, code=ABS_RX)] ) y_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_RY) + [InputConfig(type=EV_ABS, code=ABS_RY)] ) x_config["output_code"] = REL_X y_config["output_code"] = REL_Y @@ -383,17 +383,17 @@ def _convert_to_individual_mappings(): if pointer_speed: mapping_x.gain = pointer_speed mapping_y.gain = pointer_speed - preset.add(mapping_x) - preset.add(mapping_y) + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) if left_purpose == "wheel": x_config = cfg.copy() y_config = cfg.copy() x_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_X) + [InputConfig(type=EV_ABS, code=ABS_X)] ) y_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_Y) + [InputConfig(type=EV_ABS, code=ABS_Y)] ) x_config["output_code"] = REL_HWHEEL_HI_RES y_config["output_code"] = REL_WHEEL_HI_RES @@ -403,17 +403,17 @@ def _convert_to_individual_mappings(): mapping_x.gain = x_scroll_speed if y_scroll_speed: mapping_y.gain = y_scroll_speed - preset.add(mapping_x) - preset.add(mapping_y) + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) if right_purpose == "wheel": x_config = cfg.copy() y_config = cfg.copy() x_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_RX) + [InputConfig(type=EV_ABS, code=ABS_RX)] ) y_config["input_combination"] = InputCombination( - InputConfig(type=EV_ABS, code=ABS_RY) + [InputConfig(type=EV_ABS, code=ABS_RY)] ) x_config["output_code"] = REL_HWHEEL_HI_RES y_config["output_code"] = REL_WHEEL_HI_RES @@ -423,10 +423,10 @@ def _convert_to_individual_mappings(): mapping_x.gain = x_scroll_speed if y_scroll_speed: mapping_y.gain = y_scroll_speed - preset.add(mapping_x) - preset.add(mapping_y) + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) - preset.save() + migrated_preset.save() def _copy_to_beta(): diff --git a/inputremapper/configs/preset.py b/inputremapper/configs/preset.py index fce4944e0..57370e7bf 100644 --- a/inputremapper/configs/preset.py +++ b/inputremapper/configs/preset.py @@ -187,7 +187,7 @@ def save(self) -> None: if not self._has_valid_input_combination(mapping): # we save invalid mappings except for those with an invalid # input_combination - logger.debug("skipping invalid mapping %s", mapping) + logger.debug("Skipping invalid mapping %s", mapping) continue if self._is_mapped_multiple_times(mapping.input_combination): @@ -233,11 +233,19 @@ def get_mapping( if existing is not None: return existing + logger.error( + "Combination %s not found. Available: %s", + repr(combination), + list( + self._mappings.keys(), + ), + ) + return None def dangerously_mapped_btn_left(self) -> bool: """Return True if this mapping disables BTN_Left.""" - if InputCombination(InputConfig.btn_left()) not in [ + if InputCombination([InputConfig.btn_left()]) not in [ m.input_combination for m in self ]: return False diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index 653395db9..a0a6e1ead 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -106,6 +106,7 @@ def may_autoload(self, group_key: str, preset: str): def remove_timeout(func): """Remove timeout to ensure the call works if the daemon is not a proxy.""" + # the timeout kwarg is a feature of pydbus. This is needed to make tests work # that create a Daemon by calling its constructor instead of using pydbus. def wrapped(*args, **kwargs): @@ -291,8 +292,7 @@ def refresh(self, group_key: Optional[str] = None): now = time.time() if now - 10 > self.refreshed_devices_at: logger.debug("Refreshing because last info is too old") - # it may take a little bit of time until devices are visible after - # changes + # it may take a bit of time until devices are visible after changes time.sleep(0.1) groups.refresh() self.refreshed_devices_at = now diff --git a/inputremapper/groups.py b/inputremapper/groups.py index 43a7715bd..66ff322a1 100644 --- a/inputremapper/groups.py +++ b/inputremapper/groups.py @@ -37,6 +37,7 @@ import os import re import threading +import traceback from typing import List, Optional import evdev @@ -57,6 +58,7 @@ from inputremapper.configs.paths import get_preset_path from inputremapper.logger import logger +from inputremapper.utils import get_device_hash TABLET_KEYS = [ evdev.ecodes.BTN_STYLUS, @@ -321,7 +323,7 @@ def loads(cls, serialized: str): return group def __repr__(self): - return f"Group({self.key})" + return f"" class _FindGroups(threading.Thread): @@ -363,7 +365,12 @@ def run(self): # without setting an error" # - "FileNotFoundError: [Errno 2] No such file or directory: # '/dev/input/event12'" - logger.error("Failed to access %s: %s", path, str(error)) + logger.error( + 'Failed to access path "%s": %s %s', + path, + error.__class__.__name__, + str(error), + ) continue if device.name == "Power Button": @@ -381,9 +388,11 @@ def run(self): if key_capa is None and device_type != DeviceType.GAMEPAD: # skip devices that don't provide buttons that can be mapped + logger.debug('"%s" has no useful capabilities', device.name) continue if is_denylisted(device): + logger.debug('"%s" is denylisted', device.name) continue key = get_unique_key(device) @@ -391,11 +400,12 @@ def run(self): grouped[key] = [] logger.debug( - 'Found "%s", "%s", "%s", type: %s', - key, - path, + 'Found %s "%s" at "%s", hash "%s", key "%s"', + device_type.value, device.name, - device_type, + path, + get_device_hash(device), + key, ) grouped[key].append((device.name, path, device_type)) @@ -484,7 +494,7 @@ def filter(self, include_inputremapper: bool = False) -> List[_Group]: def set_groups(self, new_groups: List[_Group]): """Overwrite all groups.""" - logger.debug("overwriting groups with %s", new_groups) + logger.debug("Overwriting groups with %s", new_groups) self._groups = new_groups def list_group_names(self) -> List[str]: @@ -546,4 +556,5 @@ def find( return None +# TODO global objects are bad practice groups = _Groups() diff --git a/inputremapper/gui/components/editor.py b/inputremapper/gui/components/editor.py index add7bb188..15bba0047 100644 --- a/inputremapper/gui/components/editor.py +++ b/inputremapper/gui/components/editor.py @@ -27,7 +27,6 @@ from typing import List, Optional, Dict, Union, Callable, Literal, Set import cairo -import gi from evdev.ecodes import ( EV_KEY, EV_ABS, @@ -60,6 +59,7 @@ from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.input_event import InputEvent from inputremapper.configs.system_mapping import system_mapping, XKB_KEYCODE_OFFSET +from inputremapper.utils import get_evdev_constant_name Capabilities = Dict[int, List] @@ -267,7 +267,7 @@ def __init__( self.name_input.hide() def __repr__(self): - return f"MappingSelectionLabel for {self.combination} as {self.name}" + return f"" def _set_not_selected(self): self.edit_btn.hide() @@ -950,8 +950,7 @@ def _set_model(self, target: str): self.model.clear() self.model.append(["None, None", _("No Axis")]) for type_, code in types_codes: - - key_name = bytype[type_][code] + key_name = get_evdev_constant_name(type_, code) if isinstance(key_name, list): key_name = key_name[0] self.model.append([f"{type_}, {code}", key_name]) diff --git a/inputremapper/gui/controller.py b/inputremapper/gui/controller.py index b9383d31c..7380ed305 100644 --- a/inputremapper/gui/controller.py +++ b/inputremapper/gui/controller.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + from __future__ import annotations # needed for the TYPE_CHECKING import import re @@ -259,9 +260,11 @@ def update_combination(self, combination: InputCombination): self.show_status( CTX_WARNING, _("ctrl, alt and shift may not combine properly"), - _("Your system might reinterpret combinations ") - + _("with those after they are injected, and by doing so ") - + _("break them."), + _( + "Your system might reinterpret combinations " + + "with those after they are injected, and by doing so " + + "break them." + ), ) def move_input_config_in_combination( @@ -559,7 +562,7 @@ def show_injector_result(self, msg: InjectorStateMessage): def running(): msg = _("Applied preset %s") % self.data_manager.active_preset.name if self.data_manager.active_preset.get_mapping( - InputCombination(InputConfig.btn_left()) + InputCombination([InputConfig.btn_left()]) ): msg += _(", CTRL + DEL to stop") self.show_status(CTX_APPLY, msg) diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index 2aae26437..741f2455e 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -147,20 +147,14 @@ def pkexec_reader_service(): if exit_code != 0: raise Exception(f"Failed to pkexec the reader-service, code {exit_code}") - def run(self): - """Start doing stuff. Blocks.""" + async def run(self): + """Start doing stuff.""" # the reader will check for new commands later, once it is running # it keeps running for one device or another. - loop = asyncio.get_event_loop() logger.debug("Discovering initial groups") self.groups.refresh() self._send_groups() - loop.run_until_complete( - asyncio.gather( - self._read_commands(), - self._timeout(), - ) - ) + await asyncio.gather(self._read_commands(), self._timeout()) def _send_groups(self): """Send the groups to the gui.""" @@ -253,7 +247,7 @@ def _start_reading(self, group: _Group): context = self._create_event_pipeline(sources) # create the event reader and start it for device in sources: - reader = EventReader(context, device, ForwardDummy, self._stop_event) + reader = EventReader(context, device, self._stop_event) self._tasks.add(asyncio.create_task(reader.run())) async def _stop_reading(self): @@ -265,11 +259,11 @@ async def _stop_reading(self): self._stop_event.clear() def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDummy: - """Create a custom event pipeline for each event code in the - device capabilities. - Instead of sending the events to a uinput they will be sent to the frontend. + """Create a custom event pipeline for each event code in the capabilities. + + Instead of sending the events to an uinput they will be sent to the frontend. """ - context = ContextDummy() + context_dummy = ContextDummy() # create a context for each source for device in sources: device_hash = get_device_hash(device) @@ -279,7 +273,7 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum input_config = InputConfig( type=EV_KEY, code=ev_code, origin_hash=device_hash ) - context.add_handler( + context_dummy.add_handler( input_config, ForwardToUIHandler(self._results_pipe) ) @@ -292,26 +286,26 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum origin_hash=device_hash, ) mapping = Mapping( - input_combination=InputCombination(input_config), + input_combination=InputCombination([input_config]), target_uinput="keyboard", output_symbol="KEY_A", ) handler: MappingHandler = AbsToBtnHandler( - InputCombination(input_config), mapping + InputCombination([input_config]), mapping ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) - context.add_handler(input_config, handler) + context_dummy.add_handler(input_config, handler) # negative direction input_config = input_config.modify(analog_threshold=-30) mapping = Mapping( - input_combination=InputCombination(input_config), + input_combination=InputCombination([input_config]), target_uinput="keyboard", output_symbol="KEY_A", ) - handler = AbsToBtnHandler(InputCombination(input_config), mapping) + handler = AbsToBtnHandler(InputCombination([input_config]), mapping) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) - context.add_handler(input_config, handler) + context_dummy.add_handler(input_config, handler) for ev_code in capabilities.get(EV_REL) or (): # positive direction @@ -322,53 +316,60 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum origin_hash=device_hash, ) mapping = Mapping( - input_combination=InputCombination(input_config), + input_combination=InputCombination([input_config]), target_uinput="keyboard", output_symbol="KEY_A", release_timeout=0.3, force_release_timeout=True, ) - handler = RelToBtnHandler(InputCombination(input_config), mapping) + handler = RelToBtnHandler(InputCombination([input_config]), mapping) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) - context.add_handler(input_config, handler) + context_dummy.add_handler(input_config, handler) # negative direction input_config = input_config.modify( analog_threshold=-self.rel_xy_speed[ev_code] ) mapping = Mapping( - input_combination=InputCombination(input_config), + input_combination=InputCombination([input_config]), target_uinput="keyboard", output_symbol="KEY_A", release_timeout=0.3, force_release_timeout=True, ) - handler = RelToBtnHandler(InputCombination(input_config), mapping) + handler = RelToBtnHandler(InputCombination([input_config]), mapping) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) - context.add_handler(input_config, handler) + context_dummy.add_handler(input_config, handler) + + return context_dummy + - return context +class ForwardDummy: + @staticmethod + def write(*_): + pass class ContextDummy: + """Used for the reader so that no events are actually written to any uinput.""" + def __init__(self): self.listeners = set() self._notify_callbacks = defaultdict(list) + self.forward_dummy = ForwardDummy() def add_handler(self, input_config: InputConfig, handler: InputEventHandler): self._notify_callbacks[input_config.input_match_hash].append(handler.notify) - def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]: + def get_notify_callbacks(self, input_event: InputEvent) -> List[NotifyCallback]: return self._notify_callbacks[input_event.input_match_hash] def reset(self): pass - -class ForwardDummy: - @staticmethod - def write(*_): - pass + def get_forward_uinput(self, origin_hash) -> evdev.UInput: + """Don't actually write anything.""" + return self.forward_dummy class ForwardToUIHandler: @@ -382,7 +383,6 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: """Filter duplicates and send into the pipe.""" @@ -391,7 +391,7 @@ def notify( if EventActions.negative_trigger in event.actions: event = event.modify(value=-1) - logger.debug_key(event.event_tuple, "to frontend:") + logger.debug("Sending to %s frontend", event) self.pipe.send( { "type": MSG_EVENT, diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index eaef1ae0f..904610c23 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -19,11 +19,16 @@ """Stores injection-process wide information.""" + +from __future__ import annotations + from collections import defaultdict -from typing import List, Dict, Tuple, Set, Hashable +from typing import List, Dict, Set, Hashable -from inputremapper.input_event import InputEvent +import evdev +from inputremapper.configs.input_config import DeviceHash +from inputremapper.input_event import InputEvent from inputremapper.configs.preset import Preset from inputremapper.injection.mapping_handlers.mapping_handler import ( EventListener, @@ -33,6 +38,7 @@ parse_mappings, EventPipelines, ) +from inputremapper.logger import logger class Context: @@ -53,22 +59,39 @@ class Context: - makes the injection class shorter and more specific to a certain task, which is actually spinning up the injection. + Note, that for the reader_service a ContextDummy is used. + Members ------- preset : Preset The preset holds all Mappings for the injection process listeners : Set[EventListener] - a set of callbacks which receive all events + A set of callbacks which receive all events callbacks : Dict[Tuple[int, int], List[NotifyCallback]] - all entry points to the event pipeline sorted by InputEvent.type_and_code + All entry points to the event pipeline sorted by InputEvent.type_and_code """ listeners: Set[EventListener] _notify_callbacks: Dict[Hashable, List[NotifyCallback]] _handlers: EventPipelines + _forward_devices: Dict[DeviceHash, evdev.UInput] + _source_devices: Dict[DeviceHash, evdev.InputDevice] + + def __init__( + self, + preset: Preset, + source_devices: Dict[DeviceHash, evdev.InputDevice], + forward_devices: Dict[DeviceHash, evdev.UInput], + ): + if len(forward_devices) == 0: + logger.warning("Not forward_devices set") + + if len(source_devices) == 0: + logger.warning("Not source_devices set") - def __init__(self, preset: Preset): self.listeners = set() + self._source_devices = source_devices + self._forward_devices = forward_devices self._notify_callbacks = defaultdict(list) self._handlers = parse_mappings(preset, self) @@ -83,9 +106,19 @@ def reset(self) -> None: def _create_callbacks(self) -> None: """Add the notify method from all _handlers to self.callbacks.""" for input_config, handler_list in self._handlers.items(): - self._notify_callbacks[input_config.input_match_hash].extend( + input_match_hash = input_config.input_match_hash + logger.info("Adding NotifyCallback for %s", input_match_hash) + self._notify_callbacks[input_match_hash].extend( handler.notify for handler in handler_list ) - def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]: - return self._notify_callbacks[input_event.input_match_hash] + def get_notify_callbacks(self, input_event: InputEvent) -> List[NotifyCallback]: + input_match_hash = input_event.input_match_hash + return self._notify_callbacks[input_match_hash] + + def get_forward_uinput(self, origin_hash: DeviceHash) -> evdev.UInput: + """Get the "forward" uinput events from the given origin should go into.""" + return self._forward_devices[origin_hash] + + def get_source(self, key: DeviceHash) -> evdev.InputDevice: + return self._source_devices[key] diff --git a/inputremapper/injection/event_reader.py b/inputremapper/injection/event_reader.py index bb0482e47..ff64b8510 100644 --- a/inputremapper/injection/event_reader.py +++ b/inputremapper/injection/event_reader.py @@ -22,11 +22,12 @@ import asyncio import os +import traceback from typing import AsyncIterator, Protocol, Set, List import evdev -from inputremapper.utils import get_device_hash +from inputremapper.utils import get_device_hash, DeviceHash from inputremapper.injection.mapping_handlers.mapping_handler import ( EventListener, NotifyCallback, @@ -41,7 +42,10 @@ class Context(Protocol): def reset(self): ... - def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]: + def get_notify_callbacks(self, input_event: InputEvent) -> List[NotifyCallback]: + ... + + def get_forward_uinput(self, origin_hash: DeviceHash) -> evdev.UInput: ... @@ -60,7 +64,6 @@ def __init__( self, context: Context, source: evdev.InputDevice, - forward_to: evdev.UInput, stop_event: asyncio.Event, ) -> None: """Initialize all mapping_handlers @@ -69,14 +72,9 @@ def __init__( ---------- source where to read keycodes from - forward_to - where to write keycodes to that were not mapped to anything. - Should be an UInput with capabilities that work for all forwarded - events, so ideally they should be copied from source. """ self._device_hash = get_device_hash(source) self._source = source - self._forward_to = forward_to self.context = context self.stop_event = stop_event @@ -115,26 +113,24 @@ async def read_loop(self) -> AsyncIterator[evdev.InputEvent]: yield event def send_to_handlers(self, event: InputEvent) -> bool: - """Send the event to callback.""" + """Send the event to the NotifyCallbacks. + + Return if anyone took care of the event. + """ if event.type == evdev.ecodes.EV_MSC: return False if event.type == evdev.ecodes.EV_SYN: return False - results = set() - notify_callbacks = self.context.get_entry_points(event) + handled = False + notify_callbacks = self.context.get_notify_callbacks(event) + if notify_callbacks: for notify_callback in notify_callbacks: - results.add( - notify_callback( - event, - source=self._source, - forward=self._forward_to, - ) - ) + handled = notify_callback(event, source=self._source) | handled - return True in results + return handled async def send_to_listeners(self, event: InputEvent) -> None: """Send the event to listeners.""" @@ -165,10 +161,12 @@ async def send_to_listeners(self, event: InputEvent) -> None: def forward(self, event: InputEvent) -> None: """Forward an event, which injects it unmodified.""" + forward_to = self.context.get_forward_uinput(self._device_hash) + if event.type == evdev.ecodes.EV_KEY: - logger.debug_key(event.event_tuple, "forwarding") + logger.write(event, forward_to) - self._forward_to.write(*event.event_tuple) + forward_to.write(*event.event_tuple) async def handle(self, event: InputEvent) -> None: if event.type == evdev.ecodes.EV_KEY and event.value == 2: @@ -194,10 +192,15 @@ async def run(self): self._source.path, self._source.fd, ) + async for event in self.read_loop(): - await self.handle( - InputEvent.from_event(event, origin_hash=self._device_hash) - ) + try: + await self.handle( + InputEvent.from_event(event, origin_hash=self._device_hash) + ) + except Exception as e: + logger.error("Handling event %s failed: %s", event, e) + traceback.print_exception(e) self.context.reset() logger.info("read loop for %s stopped", self._source.path) diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 4fd2abe14..a6d8dc4c2 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from typing import Dict, Union, Tuple +from typing import Dict, Union, Tuple, Optional import evdev @@ -102,13 +102,22 @@ def __init__(self): def __iter__(self): return iter(uinput for _, uinput in self.devices.items()) + def reset(self): + self.is_service = inputremapper.utils.is_service() + self._uinput_factory = None + self.devices = {} + self.prepare_all() + def ensure_uinput_factory_set(self): if self._uinput_factory is not None: return + # overwrite global_uinputs.is_service in tests to control this if self.is_service: + logger.debug("Creating regular UInputs") self._uinput_factory = UInput else: + logger.debug("Creating FrontendUInputs") self._uinput_factory = FrontendUInput def prepare_all(self): @@ -154,10 +163,11 @@ def write(self, event: Tuple[int, int, int], target_uinput): if not uinput.can_emit(event): raise inputremapper.exceptions.EventNotHandled(event) + logger.write(event, uinput) uinput.write(*event) uinput.syn() - def get_uinput(self, name: str): + def get_uinput(self, name: str) -> Optional[evdev.UInput]: """UInput with name Or None if there is no uinput with this name. @@ -167,10 +177,14 @@ def get_uinput(self, name: str): name uniqe name of the uinput device """ - if name in self.devices.keys(): - return self.devices[name] + if name not in self.devices: + logger.error( + f'UInput "{name}" is unknown. ' + + f"Available: {list(self.devices.keys())}" + ) + return None - return None + return self.devices.get(name) global_uinputs = GlobalUInputs() diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index e4ce29fc3..fbd89c28c 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -33,7 +33,7 @@ import evdev -from inputremapper.configs.input_config import InputCombination, InputConfig +from inputremapper.configs.input_config import InputCombination, InputConfig, DeviceHash from inputremapper.configs.preset import Preset from inputremapper.groups import ( _Group, @@ -48,7 +48,6 @@ from inputremapper.utils import get_device_hash CapabilitiesDict = Dict[int, List[int]] -GroupSources = List[evdev.InputDevice] DEV_NAME = "input-remapper" @@ -237,8 +236,8 @@ def _find_input_device_fallback( logger.error(f"Could not find input for {input_config}") return None - def _grab_devices(self) -> GroupSources: - # find all devices which have an associated mapping + def _grab_devices(self) -> Dict[DeviceHash, evdev.InputDevice]: + """Grab all InputDevices that match a mappings' origin_hash.""" # use a dict because the InputDevice is not directly hashable needed_devices = {} input_configs = set() @@ -256,10 +255,11 @@ def _grab_devices(self) -> GroupSources: continue needed_devices[device.path] = device - grabbed_devices = [] + grabbed_devices = {} for device in needed_devices.values(): if device := self._grab_device(device): - grabbed_devices.append(device) + grabbed_devices[get_device_hash(device)] = device + return grabbed_devices def _update_preset(self): @@ -409,10 +409,13 @@ def run(self) -> None: # grab devices as early as possible. If events appear that won't get # released anymore before the grab they appear to be held down forever sources = self._grab_devices() + forward_devices = {} + for device_hash, device in sources.items(): + forward_devices[device_hash] = self._create_forwarding_device(device) # create this within the process after the event loop creation, # so that the macros use the correct loop - self.context = Context(self.preset) + self.context = Context(self.preset, sources, forward_devices) self._stop_event = asyncio.Event() if len(sources) == 0: @@ -424,13 +427,11 @@ def run(self) -> None: numlock_state = is_numlock_on() coroutines = [] - for source in sources: - forward_to = self._create_forwarding_device(source) + for device_hash in sources: # actually doing things event_reader = EventReader( self.context, - source, - forward_to, + sources[device_hash], self._stop_event, ) coroutines.append(event_reader.run()) @@ -460,7 +461,7 @@ def run(self) -> None: # reached otherwise. logger.debug("Injector coroutines ended") - for source in sources: + for source in sources.values(): # ungrab at the end to make the next injection process not fail # its grabs try: diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index e1f6c7a98..093b27bc0 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -82,7 +82,7 @@ def resolve(self): return macro_variables.get(self.name) def __repr__(self): - return f'' + return f'' def _type_check(value: Any, allowed_types, display_name=None, position=None) -> Any: @@ -309,7 +309,7 @@ async def _keycode_pause(self, _=None): await asyncio.sleep(self.keystroke_sleep_ms / 1000) def __repr__(self): - return f'' + return f'' """Functions that prepare the macro.""" diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index d932168f3..b6199da42 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -70,10 +70,10 @@ def __init__( def __str__(self): name = get_evdev_constant_name(*self._map_axis.type_and_code) - return f'AbsToAbsHandler for "{name}" {self._map_axis} <{id(self)}>:' + return f'AbsToAbsHandler for "{name}" {self._map_axis}' def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -87,10 +87,8 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput = None, suppress: bool = False, ) -> bool: - if event.input_match_hash != self._map_axis.input_match_hash: return False diff --git a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py index 004fb5e26..8695a5240 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py @@ -18,7 +18,7 @@ # along with input-remapper. If not, see . -from typing import Tuple, Optional +from typing import Tuple import evdev from evdev.ecodes import EV_ABS @@ -55,13 +55,10 @@ def __init__( def __str__(self): name = get_evdev_constant_name(*self._input_config.type_and_code) - return ( - f'AbsToBtnHandler for "{name}" ' - f"{self._input_config.type_and_code} <{id(self)}>:" - ) + return f'AbsToBtnHandler for "{name}" ' f"{self._input_config.type_and_code}" def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -91,7 +88,6 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: if event.input_match_hash != self._input_config.input_match_hash: @@ -119,11 +115,10 @@ def notify( event = event.modify(value=1, actions=(EventActions.as_key, direction)) self._active = bool(event.value) - # logger.debug_key(event.event_tuple, "sending to sub_handler") + # logger.debug(event.event_tuple, "sending to sub_handler") return self._sub_handler.notify( event, source=source, - forward=forward, suppress=suppress, ) diff --git a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py index 50fd01496..f62561791 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py @@ -166,10 +166,10 @@ def __init__( def __str__(self): name = get_evdev_constant_name(*self._map_axis.type_and_code) - return f'AbsToRelHandler for "{name}" {self._map_axis} <{id(self)}>:' + return f'AbsToRelHandler for "{name}" {self._map_axis}' def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -183,7 +183,6 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput = None, suppress: bool = False, ) -> bool: if event.input_match_hash != self._map_axis.input_match_hash: diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index ac032e12c..11b0f624e 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -16,7 +16,8 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from typing import Dict, Tuple, Hashable + +from typing import Dict, Tuple, Hashable, TYPE_CHECKING import evdev from inputremapper.configs.input_config import InputConfig @@ -27,9 +28,11 @@ MappingHandler, HandlerEnums, InputEventHandler, + ContextProtocol, ) from inputremapper.input_event import InputEvent, EventActions from inputremapper.logger import logger +from inputremapper.utils import get_device_hash class AxisSwitchHandler(MappingHandler): @@ -55,6 +58,7 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + context: ContextProtocol, **_, ): super().__init__(combination, mapping) @@ -73,11 +77,13 @@ def __init__( self._axis_source = None self._forward_device = None + self.context = context + def __str__(self): - return f"AxisSwitchHandler for {self._map_axis.type_and_code} <{id(self)}>" + return f"AxisSwitchHandler for {self._map_axis.type_and_code}" def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): @@ -101,7 +107,7 @@ def _handle_key_input(self, event: InputEvent): if not key_is_pressed: # recenter the axis - logger.debug_key(self.mapping.input_combination, "stopping axis") + logger.debug("Stopping axis for %s", self.mapping.input_combination) event = InputEvent( 0, 0, @@ -110,13 +116,13 @@ def _handle_key_input(self, event: InputEvent): actions=(EventActions.recenter,), origin_hash=self._map_axis.origin_hash, ) - self._sub_handler.notify(event, self._axis_source, self._forward_device) + self._sub_handler.notify(event, self._axis_source) return True if self._map_axis.type == evdev.ecodes.EV_ABS: # send the last cached value so that the abs axis # is at the correct position - logger.debug_key(self.mapping.input_combination, "starting axis") + logger.debug("Starting axis for %s", self.mapping.input_combination) event = InputEvent( 0, 0, @@ -124,7 +130,7 @@ def _handle_key_input(self, event: InputEvent): self._last_value, origin_hash=self._map_axis.origin_hash, ) - self._sub_handler.notify(event, self._axis_source, self._forward_device) + self._sub_handler.notify(event, self._axis_source) return True return True @@ -139,10 +145,8 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: - if not self._should_map(event): return False @@ -151,15 +155,18 @@ def notify( # do some caching so that we can generate the # recenter event and an initial abs event - if not self._forward_device: - self._forward_device = forward + if self._axis_source is None: self._axis_source = source + if self._forward_device is None: + device_hash = get_device_hash(source) + self._forward_device = self.context.get_forward_uinput(device_hash) + # always cache the value self._last_value = event.value if self._active: - return self._sub_handler.notify(event, source, forward, suppress) + return self._sub_handler.notify(event, source, suppress) return False diff --git a/inputremapper/injection/mapping_handlers/combination_handler.py b/inputremapper/injection/mapping_handlers/combination_handler.py index 96150e065..7be625f69 100644 --- a/inputremapper/injection/mapping_handlers/combination_handler.py +++ b/inputremapper/injection/mapping_handlers/combination_handler.py @@ -17,7 +17,8 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from typing import Dict, Tuple, Hashable +from __future__ import annotations # needed for the TYPE_CHECKING import +from typing import TYPE_CHECKING, Dict, Hashable import evdev from evdev.ecodes import EV_ABS, EV_REL @@ -32,6 +33,9 @@ from inputremapper.input_event import InputEvent from inputremapper.logger import logger +if TYPE_CHECKING: + from inputremapper.injection.context import Context + class CombinationHandler(MappingHandler): """Keeps track of a combination and notifies a sub handler.""" @@ -40,52 +44,71 @@ class CombinationHandler(MappingHandler): _pressed_keys: Dict[Hashable, bool] _output_state: bool # the last update we sent to a sub-handler _sub_handler: InputEventHandler + _handled_input_hashes: list[Hashable] def __init__( self, combination: InputCombination, mapping: Mapping, + context: Context, **_, ) -> None: - logger.debug(mapping) + logger.debug(str(mapping)) super().__init__(combination, mapping) self._pressed_keys = {} self._output_state = False + self._context = context # prepare a key map for all events with non-zero value for input_config in combination: assert not input_config.defines_analog_input self._pressed_keys[input_config.input_match_hash] = False + self._handled_input_hashes = [ + input_config.input_match_hash for input_config in combination + ] + assert len(self._pressed_keys) > 0 # no combination handler without a key def __str__(self): return ( - f'CombinationHandler for "{self.mapping.input_combination}" ' - f"{tuple(t for t in self._pressed_keys.keys())} <{id(self)}>:" + f'CombinationHandler for "{str(self.mapping.input_combination)}" ' + f"{tuple(t for t in self._pressed_keys.keys())}" ) def __repr__(self): - return self.__str__() + description = ( + f'CombinationHandler for "{repr(self.mapping.input_combination)}" ' + f"{tuple(t for t in self._pressed_keys.keys())}" + ) + return f"<{description} at {hex(id(self))}>" @property - def child(self): # used for logging + def child(self): + # used for logging return self._sub_handler def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: - if event.input_match_hash not in self._pressed_keys.keys(): - return False # we are not responsible for the event + if event.input_match_hash not in self._handled_input_hashes: + # we are not responsible for the event + return False - last_state = self.get_active() - self._pressed_keys[event.input_match_hash] = event.value == 1 + was_activated = self.is_activated() - if self.get_active() == last_state or self.get_active() == self._output_state: + # update the state + # The value of non-key input should have been changed to either 0 or 1 at this + # point by other handlers. + is_pressed = event.value == 1 + self._pressed_keys[event.input_match_hash] = is_pressed + # maybe this changes the activation status (triggered/not-triggered) + is_activated = self.is_activated() + + if is_activated == was_activated or is_activated == self._output_state: # nothing changed if self._output_state: # combination is active, consume the event @@ -94,9 +117,9 @@ def notify( # combination inactive, forward the event return False - if self.get_active(): + if is_activated: # send key up events to the forwarded uinput - self.forward_release(forward) + self.forward_release() event = event.modify(value=1) else: if self._output_state or self.mapping.is_axis_mapping(): @@ -112,11 +135,9 @@ def notify( if suppress: return False - logger.debug_key( - self.mapping.input_combination, "triggered: sending to sub-handler" - ) + logger.debug("Sending %s to sub-handler", self.mapping.input_combination) self._output_state = bool(event.value) - return self._sub_handler.notify(event, source, forward, suppress) + return self._sub_handler.notify(event, source, suppress) def reset(self) -> None: self._sub_handler.reset() @@ -124,14 +145,14 @@ def reset(self) -> None: self._pressed_keys[key] = False self._output_state = False - def get_active(self) -> bool: + def is_activated(self) -> bool: """Return if all keys in the keymap are set to True.""" return False not in self._pressed_keys.values() - def forward_release(self, forward: evdev.UInput) -> None: - """Forward a button release for all keys if this is a combination + def forward_release(self) -> None: + """Forward a button release for all keys if this is a combination. - this might cause duplicate key-up events but those are ignored by evdev anyway + This might cause duplicate key-up events but those are ignored by evdev anyway """ if len(self._pressed_keys) == 1 or not self.mapping.release_combination_keys: return @@ -140,25 +161,37 @@ def forward_release(self, forward: evdev.UInput) -> None: lambda cfg: self._pressed_keys.get(cfg.input_match_hash), self.mapping.input_combination, ) + + logger.debug("Forwarding release for %s", self.mapping.input_combination) + for input_config in keys_to_release: - forward.write(*input_config.type_and_code, 0) - forward.syn() + origin_hash = input_config.origin_hash + if origin_hash is None: + logger.error( + f"Can't forward due to missing origin_hash in {repr(input_config)}" + ) + continue + + forward_to = self._context.get_forward_uinput(origin_hash) + logger.write(input_config, forward_to) + forward_to.write(*input_config.type_and_code, 0) + forward_to.syn() def needs_ranking(self) -> bool: return bool(self.input_configs) def rank_by(self) -> InputCombination: return InputCombination( - event for event in self.input_configs if not event.defines_analog_input + [event for event in self.input_configs if not event.defines_analog_input] ) def wrap_with(self) -> Dict[InputCombination, HandlerEnums]: return_dict = {} for config in self.input_configs: if config.type == EV_ABS and not config.defines_analog_input: - return_dict[InputCombination(config)] = HandlerEnums.abs2btn + return_dict[InputCombination([config])] = HandlerEnums.abs2btn if config.type == EV_REL and not config.defines_analog_input: - return_dict[InputCombination(config)] = HandlerEnums.rel2btn + return_dict[InputCombination([config])] = HandlerEnums.rel2btn return return_dict diff --git a/inputremapper/injection/mapping_handlers/hierarchy_handler.py b/inputremapper/injection/mapping_handlers/hierarchy_handler.py index 864db205d..f278820cd 100644 --- a/inputremapper/injection/mapping_handlers/hierarchy_handler.py +++ b/inputremapper/injection/mapping_handlers/hierarchy_handler.py @@ -44,16 +44,16 @@ def __init__( ) -> None: self.handlers = handlers self._input_config = input_config - combination = InputCombination(input_config) + combination = InputCombination([input_config]) # use the mapping from the first child TODO: find a better solution mapping = handlers[0].mapping super().__init__(combination, mapping) def __str__(self): - return f"HierarchyHandler for {self._input_config} <{id(self)}>:" + return f"HierarchyHandler for {self._input_config}" def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -63,7 +63,6 @@ def notify( self, event: InputEvent, source: evdev.InputDevice = None, - forward: evdev.UInput = None, suppress: bool = False, ) -> bool: if event.input_match_hash != self._input_config.input_match_hash: @@ -72,9 +71,9 @@ def notify( success = False for handler in self.handlers: if not success: - success = handler.notify(event, source, forward) + success = handler.notify(event, source) else: - handler.notify(event, source, forward, suppress=True) + handler.notify(event, source, suppress=True) return success def reset(self) -> None: @@ -86,12 +85,12 @@ def wrap_with(self) -> Dict[InputCombination, HandlerEnums]: self._input_config.type == EV_ABS and not self._input_config.defines_analog_input ): - return {InputCombination(self._input_config): HandlerEnums.abs2btn} + return {InputCombination([self._input_config]): HandlerEnums.abs2btn} if ( self._input_config.type == EV_REL and not self._input_config.defines_analog_input ): - return {InputCombination(self._input_config): HandlerEnums.rel2btn} + return {InputCombination([self._input_config]): HandlerEnums.rel2btn} return {} def set_sub_handler(self, handler: InputEventHandler) -> None: diff --git a/inputremapper/injection/mapping_handlers/key_handler.py b/inputremapper/injection/mapping_handlers/key_handler.py index 9b6ec704e..d48f10561 100644 --- a/inputremapper/injection/mapping_handlers/key_handler.py +++ b/inputremapper/injection/mapping_handlers/key_handler.py @@ -56,10 +56,10 @@ def __init__( self._active = False def __str__(self): - return f"KeyHandler <{id(self)}>:" + return f"KeyHandler to {self._maps_to}" def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -72,7 +72,6 @@ def notify(self, event: InputEvent, *_, **__) -> bool: event_tuple = (*self._maps_to, event.value) try: global_uinputs.write(event_tuple, self.mapping.target_uinput) - logger.debug_key(event_tuple, "sending to %s", self.mapping.target_uinput) self._active = bool(event.value) return True except exceptions.Error: diff --git a/inputremapper/injection/mapping_handlers/macro_handler.py b/inputremapper/injection/mapping_handlers/macro_handler.py index 586b3e94e..855b38e77 100644 --- a/inputremapper/injection/mapping_handlers/macro_handler.py +++ b/inputremapper/injection/mapping_handlers/macro_handler.py @@ -53,10 +53,10 @@ def __init__( self._macro = parse(self.mapping.output_symbol, context, mapping) def __str__(self): - return f"MacroHandler <{id(self)}>:" + return f"MacroHandler" def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -78,11 +78,6 @@ def notify(self, event: InputEvent, *_, **__) -> bool: def handler(type_, code, value) -> None: """Handler for macros.""" - logger.debug_key( - (type_, code, value), - "sending from macro to %s", - self.mapping.target_uinput, - ) global_uinputs.write((type_, code, value), self.mapping.target_uinput) asyncio.ensure_future(self.run_macro(handler)) diff --git a/inputremapper/injection/mapping_handlers/mapping_handler.py b/inputremapper/injection/mapping_handlers/mapping_handler.py index 12812e515..8a00afae8 100644 --- a/inputremapper/injection/mapping_handlers/mapping_handler.py +++ b/inputremapper/injection/mapping_handlers/mapping_handler.py @@ -78,10 +78,13 @@ async def __call__(self, event: evdev.InputEvent) -> None: class ContextProtocol(Protocol): - """The parts from context needed for macros.""" + """The parts from context needed for handlers.""" listeners: Set[EventListener] + def get_forward_uinput(self, origin_hash) -> evdev.UInput: + pass + class NotifyCallback(Protocol): """Type signature of InputEventHandler.notify @@ -93,7 +96,6 @@ def __call__( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: ... @@ -106,7 +108,6 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: ... @@ -174,7 +175,6 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: """Notify this handler about an incoming event. @@ -186,8 +186,6 @@ def notify( something else source Where `event` comes from - forward - Where to write keycodes to that were not mapped to anything """ raise NotImplementedError diff --git a/inputremapper/injection/mapping_handlers/null_handler.py b/inputremapper/injection/mapping_handlers/null_handler.py index 978ad9d76..7799ab6dc 100644 --- a/inputremapper/injection/mapping_handlers/null_handler.py +++ b/inputremapper/injection/mapping_handlers/null_handler.py @@ -55,7 +55,6 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: return True diff --git a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py index dd010669f..c6e660aa9 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py @@ -106,10 +106,10 @@ def __init__( self._observed_rate = DEFAULT_REL_RATE def __str__(self): - return f"RelToAbsHandler for {self._map_axis} <{id(self)}>:" + return f"RelToAbsHandler for {self._map_axis}" def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -161,7 +161,7 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput = None, + forward_to: evdev.UInput = None, suppress: bool = False, ) -> bool: self._observe_rate(event) diff --git a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py index 4f66a5ce4..dd030d2f8 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py @@ -61,10 +61,10 @@ def __init__( assert len(combination) == 1 def __str__(self): - return f'RelToBtnHandler for "{self._input_config}" <{id(self)}>:' + return f'RelToBtnHandler for "{self._input_config}"' def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -73,7 +73,6 @@ def child(self): # used for logging async def _stage_release( self, source: InputEvent, - forward: evdev.InputDevice, suppress: bool, ): while time.time() < self._last_activation + self.mapping.release_timeout: @@ -91,18 +90,16 @@ async def _stage_release( actions=(EventActions.as_key,), origin_hash=self._input_config.origin_hash, ) - logger.debug_key(event.event_tuple, "sending to sub_handler") - self._sub_handler.notify(event, source, forward, suppress) + logger.debug("Sending %s to sub_handler", event) + self._sub_handler.notify(event, source, suppress) self._active = False def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput, suppress: bool = False, ) -> bool: - assert event.type == EV_REL if event.input_match_hash != self._input_config.input_match_hash: return False @@ -117,7 +114,7 @@ def notify( # consume the event return True event = event.modify(value=0, actions=(EventActions.as_key,)) - logger.debug_key(event.event_tuple, "sending to sub_handler") + logger.debug("Sending %s to sub_handler", event) self._abort_release = True else: # don't consume the event. @@ -126,7 +123,7 @@ def notify( else: # the axis is above the threshold if not self._active: - asyncio.ensure_future(self._stage_release(source, forward, suppress)) + asyncio.ensure_future(self._stage_release(source, suppress)) if value >= threshold > 0: direction = EventActions.positive_trigger else: @@ -135,10 +132,8 @@ def notify( event = event.modify(value=1, actions=(EventActions.as_key, direction)) self._active = bool(event.value) - # logger.debug_key(event.event_tuple, "sending to sub_handler") - return self._sub_handler.notify( - event, source=source, forward=forward, suppress=suppress - ) + # logger.debug("Sending %s to sub_handler", event) + return self._sub_handler.notify(event, source=source, suppress=suppress) def reset(self) -> None: if self._active: diff --git a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py index 7441897de..c1d4f01c5 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py @@ -116,10 +116,10 @@ def __init__( ) def __str__(self): - return f"RelToRelHandler for {self._input_config} <{id(self)}>:" + return f"RelToRelHandler for {self._input_config}" def __repr__(self): - return self.__str__() + return f"<{str(self)} at {hex(id(self))}>" @property def child(self): # used for logging @@ -133,7 +133,7 @@ def notify( self, event: InputEvent, source: evdev.InputDevice, - forward: evdev.UInput = None, + forward_to: evdev.UInput = None, suppress: bool = False, ) -> bool: if not self._should_map(event): diff --git a/inputremapper/input_event.py b/inputremapper/input_event.py index 470953ee8..50f26713a 100644 --- a/inputremapper/input_event.py +++ b/inputremapper/input_event.py @@ -21,11 +21,13 @@ import enum from dataclasses import dataclass -from typing import Tuple, Optional, Hashable +from typing import Tuple, Optional, Hashable, Literal import evdev from evdev import ecodes +from inputremapper.utils import get_evdev_constant_name + class EventActions(enum.Enum): """Additional information an InputEvent can send through the event pipeline.""" @@ -39,6 +41,21 @@ class EventActions(enum.Enum): negative_trigger = enum.auto() # original event was negative direction +def validate_event(event): + """Test if the event is valid.""" + if not isinstance(event.type, int): + raise TypeError(f"Expected type to be an int, but got {event.type}") + + if not isinstance(event.code, int): + raise TypeError(f"Expected code to be an int, but got {event.code}") + + if not isinstance(event.value, int): + # this happened to me because I screwed stuff up + raise TypeError(f"Expected value to be an int, but got {event.value}") + + return event + + # Todo: add slots=True as soon as python 3.10 is in common distros @dataclass(frozen=True) class InputEvent: @@ -54,6 +71,7 @@ class InputEvent: value: int actions: Tuple[EventActions, ...] = () origin_hash: Optional[str] = None + forward_to: Optional[evdev.UInput] = None def __eq__(self, other: InputEvent | evdev.InputEvent | Tuple[int, int, int]): # useful in tests @@ -90,18 +108,71 @@ def from_event( ) from exception @classmethod - def from_tuple(cls, event_tuple: Tuple[int, int, int]) -> InputEvent: + def from_tuple( + cls, event_tuple: Tuple[int, int, int], origin_hash: Optional[str] = None + ) -> InputEvent: """Create a InputEvent from a (type, code, value) tuple.""" + # use this as rarely as possible. Construct objects early on and pass them + # around instead of passing around integers if len(event_tuple) != 3: raise TypeError( - f"failed to create InputEvent {event_tuple = }" f" must have length 3" + f"failed to create InputEvent {event_tuple = } must have length 3" + ) + + return validate_event( + cls( + 0, + 0, + int(event_tuple[0]), + int(event_tuple[1]), + int(event_tuple[2]), + origin_hash=origin_hash, + ) + ) + + @classmethod + def abs(cls, code: int, value: int, origin_hash: Optional[str] = None): + """Create an abs event, like joystick movements.""" + return validate_event( + cls( + 0, + 0, + ecodes.EV_ABS, + code, + value, + origin_hash=origin_hash, + ) + ) + + @classmethod + def rel(cls, code: int, value: int, origin_hash: Optional[str] = None): + """Create a rel event, like mouse movements.""" + return validate_event( + cls( + 0, + 0, + ecodes.EV_REL, + code, + value, + origin_hash=origin_hash, + ) + ) + + @classmethod + def key(cls, code: int, value: Literal[0, 1], origin_hash: Optional[str] = None): + """Create a key event, like keyboard keys or gamepad buttons. + + A value of 1 means "press", a value of 0 means "release". + """ + return validate_event( + cls( + 0, + 0, + ecodes.EV_KEY, + code, + value, + origin_hash=origin_hash, ) - return cls( - 0, - 0, - int(event_tuple[0]), - int(event_tuple[1]), - int(event_tuple[2]), ) @property @@ -136,7 +207,11 @@ def is_wheel_hi_res_event(self) -> bool: ] def __str__(self): - return f"InputEvent{self.event_tuple}" + name = get_evdev_constant_name(self.type, self.code) + return f"InputEvent for {self.event_tuple} {name}" + + def __repr__(self): + return f"<{str(self)} at {hex(id(self))}>" def timestamp(self): """Return the unix timestamp of when the event was seen.""" diff --git a/inputremapper/logger.py b/inputremapper/logger.py index dacf77ff6..d68c45601 100644 --- a/inputremapper/logger.py +++ b/inputremapper/logger.py @@ -37,6 +37,7 @@ start = time.time() previous_key_debug_log = None +previous_write_debug_log = None def parse_mapping_handler(mapping_handler): @@ -55,7 +56,7 @@ def parse_mapping_handler(mapping_handler): lines_and_indent.extend(sub_list) break - lines_and_indent.append([str(mapping_handler), indent]) + lines_and_indent.append([repr(mapping_handler), indent]) try: mapping_handler = mapping_handler.child except AttributeError: @@ -77,11 +78,8 @@ def debug_mapping_handler(self, mapping_handler): msg = indent * line[1] + line[0] self._log(logging.DEBUG, msg, args=None) - def debug_key(self, key, msg, *args): - """Log a key-event message. - - Example: - ... DEBUG event_reader.py:143: forwarding ···················· (1, 71, 1) + def write(self, key, uinput): + """Log that an event is being written Parameters ---------- @@ -93,21 +91,18 @@ def debug_key(self, key, msg, *args): if not self.isEnabledFor(logging.DEBUG): return - global previous_key_debug_log + global previous_write_debug_log - msg = msg % args - str_key = str(key) + str_key = repr(key) str_key = str_key.replace(",)", ")") - spacing = " " + "·" * max(0, 30 - len(msg)) - if len(spacing) == 1: - spacing = "" - msg = f"{msg}{spacing} {str_key}" - if msg == previous_key_debug_log: + msg = f'Writing {str_key} to "{uinput.name}"' + + if msg == previous_write_debug_log: # avoid some super spam from EV_ABS events return - previous_key_debug_log = msg + previous_write_debug_log = msg self._log(logging.DEBUG, msg, args=None) @@ -247,6 +242,8 @@ def format(self, record: logging.LogRecord): logging.getLogger("asyncio").setLevel(logging.WARNING) +# using pkg_resources to figure out the version fails in many cases, +# so we hardcode it instead VERSION = "1.6.0-beta" EVDEV_VERSION = None try: diff --git a/inputremapper/utils.py b/inputremapper/utils.py index 85ee695e2..b36d2eec8 100644 --- a/inputremapper/utils.py +++ b/inputremapper/utils.py @@ -43,13 +43,19 @@ def get_device_hash(device: evdev.InputDevice) -> DeviceHash: return md5(s.encode()).hexdigest().lower() -def get_evdev_constant_name(type_: int, code: int, *_) -> Optional[str]: - """Handy function to get the evdev constant name.""" - # this is more readable than +def get_evdev_constant_name(type_: Optional[int], code: Optional[int], *_) -> str: + """Handy function to get the evdev constant name for display purposes. + + Returns "unknown" for unknown events. + """ + # using this function is more readable than # type_, code = event.type_and_code # name = evdev.ecodes.bytype[type_][code] name = evdev.ecodes.bytype.get(type_, {}).get(code) if isinstance(name, list): - return name[0] + name = name[0] + + if name is None: + return "unknown" return name diff --git a/readme/coverage.svg b/readme/coverage.svg index 705b8c654..2132964c2 100644 --- a/readme/coverage.svg +++ b/readme/coverage.svg @@ -17,7 +17,7 @@ coverage - 93% - 93% + 92% + 92% diff --git a/readme/development.md b/readme/development.md index d9a069622..2c5f29c82 100644 --- a/readme/development.md +++ b/readme/development.md @@ -117,3 +117,4 @@ https://miro.com/app/board/uXjVPLa8ilM=/?share_link_id=272180986764 - [Python Unix Domain Sockets](https://pymotw.com/2/socket/uds.html) - [GNOME HIG](https://developer.gnome.org/hig/stable/) - [GtkSource Example](https://github.com/wolfthefallen/py-GtkSourceCompletion-example) +- [linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h) diff --git a/readme/pylint.svg b/readme/pylint.svg index 07c0f6867..26889e456 100644 --- a/readme/pylint.svg +++ b/readme/pylint.svg @@ -17,7 +17,7 @@ pylint - 9.27 - 9.27 + 8.88 + 8.88 diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index d66e969bf..43cfd8b68 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -304,7 +304,9 @@ def test_selects_preset(self): ( MappingData( name="m1", - input_combination=InputCombination(InputConfig(type=1, code=2)), + input_combination=InputCombination( + [InputConfig(type=1, code=2)] + ), ), ), ) @@ -317,7 +319,9 @@ def test_selects_preset(self): ( MappingData( name="m1", - input_combination=InputCombination(InputConfig(type=1, code=2)), + input_combination=InputCombination( + [InputConfig(type=1, code=2)] + ), ), ), ) @@ -331,7 +335,9 @@ def test_avoids_infinite_recursion(self): ( MappingData( name="m1", - input_combination=InputCombination(InputConfig(type=1, code=2)), + input_combination=InputCombination( + [InputConfig(type=1, code=2)] + ), ), ), ) @@ -358,22 +364,22 @@ def setUp(self) -> None: MappingData( name="mapping1", input_combination=InputCombination( - InputConfig(type=1, code=KEY_C) + [InputConfig(type=1, code=KEY_C)] ), ), MappingData( name="", input_combination=InputCombination( - ( + [ InputConfig(type=1, code=KEY_A), InputConfig(type=1, code=KEY_B), - ) + ] ), ), MappingData( name="mapping2", input_combination=InputCombination( - InputConfig(type=1, code=KEY_B) + [InputConfig(type=1, code=KEY_B)] ), ), ), @@ -407,27 +413,27 @@ def test_activates_correct_row(self): self.message_broker.publish( MappingData( name="mapping1", - input_combination=InputCombination(InputConfig(type=1, code=KEY_C)), + input_combination=InputCombination([InputConfig(type=1, code=KEY_C)]), ) ) selected = self.get_selected_row() self.assertEqual(selected.name, "mapping1") self.assertEqual( selected.combination, - InputCombination(InputConfig(type=1, code=KEY_C)), + InputCombination([InputConfig(type=1, code=KEY_C)]), ) def test_loads_mapping(self): - self.select_row(InputCombination(InputConfig(type=1, code=KEY_B))) + self.select_row(InputCombination([InputConfig(type=1, code=KEY_B)])) self.controller_mock.load_mapping.assert_called_once_with( - InputCombination(InputConfig(type=1, code=KEY_B)) + InputCombination([InputConfig(type=1, code=KEY_B)]) ) def test_avoids_infinite_recursion(self): self.message_broker.publish( MappingData( name="mapping1", - input_combination=InputCombination(InputConfig(type=1, code=KEY_C)), + input_combination=InputCombination([InputConfig(type=1, code=KEY_C)]), ) ) self.controller_mock.load_mapping.assert_not_called() @@ -440,7 +446,7 @@ def test_sorts_empty_mapping_to_bottom(self): MappingData( name="qux", input_combination=InputCombination( - InputConfig(type=1, code=KEY_C) + [InputConfig(type=1, code=KEY_C)] ), ), MappingData( @@ -450,7 +456,7 @@ def test_sorts_empty_mapping_to_bottom(self): MappingData( name="bar", input_combination=InputCombination( - InputConfig(type=1, code=KEY_B) + [InputConfig(type=1, code=KEY_B)] ), ), ), @@ -469,13 +475,13 @@ def test_sorts_empty_mapping_to_bottom(self): MappingData( name="qux", input_combination=InputCombination( - InputConfig(type=1, code=KEY_C) + [InputConfig(type=1, code=KEY_C)] ), ), MappingData( name="bar", input_combination=InputCombination( - InputConfig(type=1, code=KEY_B) + [InputConfig(type=1, code=KEY_B)] ), ), ), @@ -512,10 +518,9 @@ def assert_selected(self): def test_repr(self): self.mapping_selection_label.name = "name" - self.assertEqual( - repr(self.mapping_selection_label), - "MappingSelectionLabel for a + b as name", - ) + self.assertIn("name", repr(self.mapping_selection_label)) + self.assertIn("KEY_A", repr(self.mapping_selection_label)) + self.assertIn("KEY_B", repr(self.mapping_selection_label)) def test_shows_combination_without_name(self): self.assertEqual(self.mapping_selection_label.label.get_label(), "a + b") @@ -553,12 +558,12 @@ def test_updates_combination_when_selected(self): InputConfig(type=1, code=KEY_B), ) ), - InputCombination(InputConfig(type=1, code=KEY_A)), + InputCombination([InputConfig(type=1, code=KEY_A)]), ) ) self.assertEqual( self.mapping_selection_label.combination, - InputCombination(InputConfig(type=1, code=KEY_A)), + InputCombination([InputConfig(type=1, code=KEY_A)]), ) def test_doesnt_update_combination_when_not_selected(self): @@ -579,7 +584,7 @@ def test_doesnt_update_combination_when_not_selected(self): InputConfig(type=1, code=KEY_B), ) ), - InputCombination(InputConfig(type=1, code=KEY_A)), + InputCombination([InputConfig(type=1, code=KEY_A)]), ) ) self.assertEqual( @@ -1248,7 +1253,7 @@ def setUp(self) -> None: self.message_broker.publish( MappingData( input_combination=InputCombination( - InputConfig(type=2, code=0, analog_threshold=1) + [InputConfig(type=2, code=0, analog_threshold=1)] ), target_uinput="keyboard", ) @@ -1258,7 +1263,7 @@ def test_updates_timeout_on_mapping_message(self): self.message_broker.publish( MappingData( input_combination=InputCombination( - InputConfig(type=2, code=0, analog_threshold=1) + [InputConfig(type=2, code=0, analog_threshold=1)] ), release_timeout=1, ) @@ -1273,7 +1278,7 @@ def test_avoids_infinite_recursion(self): self.message_broker.publish( MappingData( input_combination=InputCombination( - InputConfig(type=2, code=0, analog_threshold=1) + [InputConfig(type=2, code=0, analog_threshold=1)] ), release_timeout=1, ) @@ -1357,7 +1362,7 @@ def setUp(self) -> None: self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=1, code=1)), + input_combination=InputCombination([InputConfig(type=1, code=1)]), ) ) @@ -1523,7 +1528,7 @@ def setUp(self) -> None: ) self.message_broker.publish( MappingData( - input_combination=InputCombination(InputConfig(type=3, code=0)), + input_combination=InputCombination([InputConfig(type=3, code=0)]), target_uinput="mouse", ) ) @@ -1587,7 +1592,7 @@ def setUp(self) -> None: self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=2, code=0)), + input_combination=InputCombination([InputConfig(type=2, code=0)]), rel_to_abs_input_cutoff=1, output_type=3, output_code=0, @@ -1606,7 +1611,7 @@ def test_avoids_infinite_recursion(self): self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=2, code=0)), + input_combination=InputCombination([InputConfig(type=2, code=0)]), rel_to_abs_input_cutoff=3, output_type=3, output_code=0, @@ -1619,7 +1624,7 @@ def test_updates_value(self): self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=2, code=0)), + input_combination=InputCombination([InputConfig(type=2, code=0)]), rel_to_abs_input_cutoff=rel_to_abs_input_cutoff, output_type=3, output_code=0, @@ -1636,7 +1641,7 @@ def test_disables_input_when_no_rel_axis_input(self): self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=3, code=0)), + input_combination=InputCombination([InputConfig(type=3, code=0)]), output_type=3, output_code=0, ) @@ -1648,7 +1653,7 @@ def test_disables_input_when_no_abs_axis_output(self): self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=2, code=0)), + input_combination=InputCombination([InputConfig(type=2, code=0)]), rel_to_abs_input_cutoff=3, output_type=2, output_code=0, @@ -1660,7 +1665,7 @@ def test_enables_input(self): self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=3, code=0)), + input_combination=InputCombination([InputConfig(type=3, code=0)]), output_type=3, output_code=0, ) @@ -1669,7 +1674,7 @@ def test_enables_input(self): self.message_broker.publish( MappingData( target_uinput="mouse", - input_combination=InputCombination(InputConfig(type=2, code=0)), + input_combination=InputCombination([InputConfig(type=2, code=0)]), rel_to_abs_input_cutoff=1, output_type=3, output_code=0, @@ -1686,7 +1691,7 @@ def test_no_reqorded_input_required(self): self.box, require_recorded_input=False, ) - combination = InputCombination(InputConfig(type=1, code=KEY_A)) + combination = InputCombination([InputConfig(type=1, code=KEY_A)]) self.message_broker.publish(MappingData()) self.assert_inactive(self.box) @@ -1712,7 +1717,7 @@ def test_recorded_input_required(self): self.box, require_recorded_input=True, ) - combination = InputCombination(InputConfig(type=1, code=KEY_A)) + combination = InputCombination([InputConfig(type=1, code=KEY_A)]) self.message_broker.publish(MappingData()) self.assert_inactive(self.box) @@ -1848,7 +1853,7 @@ def test_breadcrumbs(self): self.assertEqual(self.label_4.get_text(), "group / preset / a + b") self.assertEqual(self.label_5.get_text(), "a + b") - combination = InputCombination(InputConfig(type=1, code=KEY_A)) + combination = InputCombination([InputConfig(type=1, code=KEY_A)]) self.message_broker.publish( MappingData(name="qux", input_combination=combination) ) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index c175225fc..e3430b20b 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -18,17 +18,19 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +import asyncio # the tests file needs to be imported first to make sure patches are loaded from contextlib import contextmanager from typing import Tuple, List, Optional, Iterable +from inputremapper.injection.global_uinputs import global_uinputs +from tests.lib.global_uinputs import reset_global_uinputs_for_service from tests.test import get_project_root -from tests.lib.fixtures import new_event from tests.lib.cleanup import cleanup from tests.lib.stuff import spy from tests.lib.constants import EVENT_READ_TIMEOUT -from tests.lib.fixtures import prepare_presets, get_combination_config +from tests.lib.fixtures import prepare_presets from tests.lib.logger import logger from tests.lib.fixtures import fixtures from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe @@ -124,6 +126,15 @@ def launch( ) +def start_reader_service(): + def process(): + reader_service = ReaderService(_Groups()) + loop = asyncio.new_event_loop() + loop.run_until_complete(reader_service.run()) + + multiprocessing.Process(target=process).start() + + @contextmanager def patch_launch(): """patch the launch function such that we don't connect to @@ -136,7 +147,8 @@ def os_system(cmd): # instead of running pkexec, fork instead. This will make # the reader-service aware of all the test patches if "pkexec input-remapper-control --command start-reader-service" in cmd: - multiprocessing.Process(target=ReaderService(_Groups()).run).start() + logger.info("pkexec-patch starting ReaderService process") + start_reader_service() return 0 return original_os_system(cmd) @@ -185,7 +197,8 @@ def os_system(cmd): # instead of running pkexec, fork instead. This will make # the reader-service aware of all the test patches if "pkexec input-remapper-control --command start-reader-service" in cmd: - self.reader_service_started() # don't start the reader-service just log that it was. + # don't start the reader-service just log that it was. + self.reader_service_started() return 0 return self.original_os_system(cmd) @@ -213,7 +226,7 @@ def test_knows_devices(self): self.assertEqual(len(self.data_manager.get_group_keys()), 0) # start the reader-service delayed - multiprocessing.Process(target=ReaderService(_Groups()).run).start() + start_reader_service() # perform some iterations so that the reader ends up reading from the pipes # which will make it receive devices. for _ in range(10): @@ -338,7 +351,7 @@ def _test_initial_state(self): self.assertEqual(self.target_selection.get_active_id(), "keyboard") self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(InputConfig(type=1, code=5)), + InputCombination([InputConfig(type=1, code=5)]), ) self.assertEqual( self.data_manager.active_input_config, InputConfig(type=1, code=5) @@ -526,12 +539,14 @@ def test_initial_state(self): self.assertFalse(self.autoload_toggle.get_active()) self.assertEqual( self.selection_label_listbox.get_selected_row().combination, - InputCombination(InputConfig(type=1, code=5)), + InputCombination([InputConfig(type=1, code=5)]), ) self.assertEqual( self.data_manager.active_mapping.input_combination, InputCombination( - InputConfig(type=1, code=5), + [ + InputConfig(type=1, code=5), + ] ), ) self.assertEqual(self.selection_label_listbox.get_selected_row().name, "4") @@ -665,26 +680,29 @@ def test_events_from_reader_service_arrive(self): push_events( fixtures.foo_device_2_keyboard, - [InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 31, 1)], + [ + InputEvent(0, 0, 1, 30, 1), + InputEvent(0, 0, 1, 31, 1), + ], ) - self.throttle(40) + self.throttle(60) origin = fixtures.foo_device_2_keyboard.get_device_hash() mock1.assert_has_calls( ( call( CombinationRecorded( InputCombination( - InputConfig(type=1, code=30, origin_hash=origin) + [InputConfig(type=1, code=30, origin_hash=origin)] ) ) ), call( CombinationRecorded( InputCombination( - ( + [ InputConfig(type=1, code=30, origin_hash=origin), InputConfig(type=1, code=31, origin_hash=origin), - ) + ] ) ) ), @@ -695,12 +713,12 @@ def test_events_from_reader_service_arrive(self): mock2.assert_not_called() push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 0)]) - self.throttle(40) + self.throttle(60) self.assertEqual(mock1.call_count, 2) mock2.assert_not_called() push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)]) - self.throttle(40) + self.throttle(60) self.assertEqual(mock1.call_count, 2) mock2.assert_called_once() @@ -718,14 +736,14 @@ def test_cannot_create_duplicate_input_combination(self): fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)], ) - self.throttle(40) + self.throttle(60) # if this fails with : this is the initial # mapping or something, so it was never overwritten. origin = fixtures.foo_device_2_keyboard.get_device_hash() self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(InputConfig(type=1, code=30, origin_hash=origin)), + InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]), ) # create a new mapping @@ -742,7 +760,7 @@ def test_cannot_create_duplicate_input_combination(self): fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)], ) - self.throttle(40) + self.throttle(60) # should still be the empty mapping self.assertEqual( self.data_manager.active_mapping.input_combination, @@ -752,36 +770,36 @@ def test_cannot_create_duplicate_input_combination(self): # try to record a different combination self.controller.start_key_recording() push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)]) - self.throttle(40) + self.throttle(60) # nothing changed yet, as we got the duplicate combination self.assertEqual( self.data_manager.active_mapping.input_combination, InputCombination.empty_combination(), ) push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 1)]) - self.throttle(40) + self.throttle(60) # now the combination is different self.assertEqual( self.data_manager.active_mapping.input_combination, InputCombination( - ( + [ InputConfig(type=1, code=30, origin_hash=origin), InputConfig(type=1, code=31, origin_hash=origin), - ) + ] ), ) # let's make the combination even longer push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 32, 1)]) - self.throttle(40) + self.throttle(60) self.assertEqual( self.data_manager.active_mapping.input_combination, InputCombination( - ( + [ InputConfig(type=1, code=30, origin_hash=origin), InputConfig(type=1, code=31, origin_hash=origin), InputConfig(type=1, code=32, origin_hash=origin), - ) + ] ), ) @@ -794,21 +812,21 @@ def test_cannot_create_duplicate_input_combination(self): InputEvent(0, 0, 1, 32, 0), ], ) - self.throttle(40) + self.throttle(60) # sending a combination update now should not do anything self.message_broker.publish( - CombinationRecorded(InputCombination(InputConfig(type=1, code=35))) + CombinationRecorded(InputCombination([InputConfig(type=1, code=35)])) ) gtk_iteration() self.assertEqual( self.data_manager.active_mapping.input_combination, InputCombination( - ( + [ InputConfig(type=1, code=30, origin_hash=origin), InputConfig(type=1, code=31, origin_hash=origin), InputConfig(type=1, code=32, origin_hash=origin), - ) + ] ), ) @@ -839,19 +857,19 @@ def test_create_simple_mapping(self): self.recording_toggle.set_active(True) gtk_iteration() push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)]) - self.throttle(40) + self.throttle(60) push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)]) - self.throttle(40) + self.throttle(60) # check the input_combination origin = fixtures.foo_device_2_keyboard.get_device_hash() self.assertEqual( self.selection_label_listbox.get_selected_row().combination, - InputCombination(InputConfig(type=1, code=30, origin_hash=origin)), + InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]), ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(InputConfig(type=1, code=30, origin_hash=origin)), + InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]), ) self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a") self.assertIsNone(self.data_manager.active_mapping.name) @@ -868,7 +886,7 @@ def test_create_simple_mapping(self): self.data_manager.active_mapping, Mapping( input_combination=InputCombination( - InputConfig(type=1, code=30, origin_hash=origin) + [InputConfig(type=1, code=30, origin_hash=origin)] ), output_symbol="Shift_L", target_uinput="keyboard", @@ -882,7 +900,7 @@ def test_create_simple_mapping(self): ) self.assertEqual( self.selection_label_listbox.get_selected_row().combination, - InputCombination(InputConfig(type=1, code=30, origin_hash=origin)), + InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]), ) # 4. update target to mouse @@ -892,7 +910,7 @@ def test_create_simple_mapping(self): self.data_manager.active_mapping, Mapping( input_combination=InputCombination( - InputConfig(type=1, code=30, origin_hash=origin) + [InputConfig(type=1, code=30, origin_hash=origin)] ), output_symbol="Shift_L", target_uinput="mouse", @@ -916,26 +934,27 @@ def test_hat_switch(self): gtk_iteration() # it should be possible to add all of them - ev_1 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) - ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1) - ev_3 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, -1) - ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, 1) + ev_1 = InputEvent.abs(evdev.ecodes.ABS_HAT0X, -1) + ev_2 = InputEvent.abs(evdev.ecodes.ABS_HAT0X, 1) + ev_3 = InputEvent.abs(evdev.ecodes.ABS_HAT0Y, -1) + ev_4 = InputEvent.abs(evdev.ecodes.ABS_HAT0Y, 1) - def add_mapping(event_tuple, symbol) -> InputCombination: + def add_mapping(event, symbol) -> InputCombination: """adds mapping and returns the expected input combination""" - event = InputEvent.from_tuple(event_tuple) self.controller.create_mapping() gtk_iteration() self.controller.start_key_recording() push_events(fixtures.foo_device_2_gamepad, [event, event.modify(value=0)]) - self.throttle(40) + self.throttle(60) gtk_iteration() self.code_editor.get_buffer().set_text(symbol) gtk_iteration() return InputCombination( - InputConfig.from_input_event(event).modify( - origin_hash=fixtures.foo_device_2_gamepad.get_device_hash() - ) + [ + InputConfig.from_input_event(event).modify( + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash() + ) + ] ) config_1 = add_mapping(ev_1, "a") @@ -977,10 +996,26 @@ def test_combination(self): gtk_iteration() # it should be possible to write a combination - ev_1 = (EV_KEY, evdev.ecodes.KEY_A, 1) - ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1) - ev_3 = (EV_KEY, evdev.ecodes.KEY_C, 1) - ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1) + ev_1 = InputEvent.key( + evdev.ecodes.KEY_A, + 1, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ev_2 = InputEvent.abs( + evdev.ecodes.ABS_HAT0X, + 1, + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), + ) + ev_3 = InputEvent.key( + evdev.ecodes.KEY_C, + 1, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ev_4 = InputEvent.abs( + evdev.ecodes.ABS_HAT0X, + -1, + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), + ) combination_1 = (ev_1, ev_2, ev_3) combination_2 = (ev_2, ev_1, ev_3) @@ -992,34 +1027,23 @@ def test_combination(self): combination_5 = (ev_1, ev_3, ev_2) combination_6 = (ev_3, ev_1, ev_2) - def get_combination(combi: Iterable[Tuple[int, int, int]]) -> InputCombination: + def get_combination(combi: Iterable[InputEvent]) -> InputCombination: + """Create an InputCombination from a list of events. + + Ensures the origin_hash is set correctly. + """ configs = [] - for t in combi: - config = InputConfig.from_input_event(InputEvent.from_tuple(t)) - if config.type == EV_KEY: - config = config.modify( - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash() - ) - if config.type == EV_ABS: - config = config.modify( - origin_hash=fixtures.foo_device_2_gamepad.get_device_hash() - ) - if config.type == EV_REL: - config = config.modify( - origin_hash=fixtures.foo_device_2_mouse.get_device_hash() - ) + for event in combi: + config = InputConfig.from_input_event(event) configs.append(config) return InputCombination(configs) - def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol): + def add_mapping(combi: Iterable[InputEvent], symbol): + logger.info("add_mapping %s", combi) self.controller.create_mapping() gtk_iteration() self.controller.start_key_recording() - previous_event = InputEvent(0, 0, 1, 1, 1) - for event_tuple in combi: - event = InputEvent.from_tuple(event_tuple) - if event.type != previous_event.type: - self.throttle(20) # avoid race condition if we switch fixture + for event in combi: if event.type == EV_KEY: push_event(fixtures.foo_device_2_keyboard, event) if event.type == EV_ABS: @@ -1027,8 +1051,11 @@ def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol): if event.type == EV_REL: push_event(fixtures.foo_device_2_mouse, event) - for event_tuple in combi: - event = InputEvent.from_tuple(event_tuple) + # avoid race condition if we switch fixture in push_event. The order + # of events needs to be correct. + self.throttle(20) + + for event in combi: if event.type == EV_KEY: push_event(fixtures.foo_device_2_keyboard, event.modify(value=0)) if event.type == EV_ABS: @@ -1036,12 +1063,13 @@ def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol): if event.type == EV_REL: pass - self.throttle(40) + self.throttle(60) gtk_iteration() self.code_editor.get_buffer().set_text(symbol) gtk_iteration() add_mapping(combination_1, "a") + self.assertEqual( self.data_manager.active_preset.get_mapping( get_combination(combination_1) @@ -1209,7 +1237,7 @@ def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol): def test_only_one_empty_mapping_possible(self): self.assertEqual( self.selection_label_listbox.get_selected_row().combination, - InputCombination(InputConfig(type=1, code=5)), + InputCombination([InputConfig(type=1, code=5)]), ) self.assertEqual(len(self.selection_label_listbox.get_children()), 1) self.assertEqual(len(self.data_manager.active_preset), 1) @@ -1242,7 +1270,9 @@ def test_selection_labels_sort_alphabetically(self): self.recording_toggle.set_active(True) gtk_iteration() self.message_broker.publish( - CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q))) + CombinationRecorded( + InputCombination([InputConfig(type=EV_KEY, code=KEY_Q)]) + ) ) gtk_iteration() self.message_broker.signal(MessageType.recording_finished) @@ -1262,7 +1292,9 @@ def test_selection_labels_sort_empty_mapping_to_the_bottom(self): self.recording_toggle.set_active(True) gtk_iteration() self.message_broker.publish( - CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q))) + CombinationRecorded( + InputCombination([InputConfig(type=EV_KEY, code=KEY_Q)]) + ) ) gtk_iteration() self.message_broker.signal(MessageType.recording_finished) @@ -1373,7 +1405,7 @@ def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol): fixtures.foo_device_2_keyboard, [event.modify(value=0) for event in combi], ) - self.throttle(40) + self.throttle(60) gtk_iteration() self.code_editor.get_buffer().set_text(symbol) gtk_iteration() @@ -1422,11 +1454,11 @@ def test_check_for_unknown_symbols(self): self.controller.load_preset("preset1") self.throttle(20) - self.controller.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.controller.load_mapping(InputCombination([InputConfig(type=1, code=1)])) gtk_iteration() self.controller.update_mapping(output_symbol="foo") gtk_iteration() - self.controller.load_mapping(InputCombination(InputConfig(type=1, code=2))) + self.controller.load_mapping(InputCombination([InputConfig(type=1, code=2)])) gtk_iteration() self.controller.update_mapping(output_symbol="qux") gtk_iteration() @@ -1449,7 +1481,7 @@ def test_check_for_unknown_symbols(self): self.assertTrue(error_icon.get_visible()) self.assertFalse(warning_icon.get_visible()) - self.controller.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.controller.load_mapping(InputCombination([InputConfig(type=1, code=1)])) gtk_iteration() self.controller.update_mapping(output_symbol="b") gtk_iteration() @@ -1543,8 +1575,8 @@ def test_select_preset(self): self.assertEqual( mappings, { - InputCombination(InputConfig(type=1, code=1)), - InputCombination(InputConfig(type=1, code=2)), + InputCombination([InputConfig(type=1, code=1)]), + InputCombination([InputConfig(type=1, code=2)]), }, ) self.assertFalse(self.autoload_toggle.get_active()) @@ -1558,8 +1590,8 @@ def test_select_preset(self): self.assertEqual( mappings, { - InputCombination(InputConfig(type=1, code=3)), - InputCombination(InputConfig(type=1, code=4)), + InputCombination([InputConfig(type=1, code=3)]), + InputCombination([InputConfig(type=1, code=4)]), }, ) self.assertTrue(self.autoload_toggle.get_active()) @@ -1658,7 +1690,7 @@ def test_start_with_btn_left(self): self.controller.create_mapping() gtk_iteration() self.controller.update_mapping( - input_combination=InputCombination(InputConfig.btn_left()), + input_combination=InputCombination([InputConfig.btn_left()]), output_symbol="a", ) gtk_iteration() @@ -1719,6 +1751,11 @@ def test_cannot_record_keys(self): self.assertIn("Stop", text) def test_start_injecting(self): + # It's 2023 everyone! That means this test randomly stopped working because it + # used FrontendUInputs instead of regular UInputs. I guess a fucking ghost + # was fixing this for us during 2022, but it seems to have disappeared. + reset_global_uinputs_for_service() + self.controller.load_group("Foo Device 2") with spy(self.daemon, "set_config_dir") as spy1: @@ -1750,8 +1787,8 @@ def test_start_injecting(self): push_events( fixtures.foo_device_2_keyboard, [ - new_event(evdev.events.EV_KEY, 5, 1), - new_event(evdev.events.EV_KEY, 5, 0), + InputEvent.key(5, 1), + InputEvent.key(5, 0), ], ) @@ -1773,6 +1810,8 @@ def test_start_injecting(self): self.assertNotIn("input-remapper", device_group_entry.name) def test_stop_injecting(self): + reset_global_uinputs_for_service() + self.controller.load_group("Foo Device 2") self.start_injector_btn.clicked() gtk_iteration() @@ -1782,6 +1821,7 @@ def test_stop_injecting(self): gtk_iteration() if self.data_manager.get_state() == InjectorState.RUNNING: break + # fail here so we don't block forever self.assertEqual(self.data_manager.get_state(), InjectorState.RUNNING) @@ -1795,8 +1835,8 @@ def test_stop_injecting(self): push_events( fixtures.foo_device_2_keyboard, [ - new_event(evdev.events.EV_KEY, 5, 1), - new_event(evdev.events.EV_KEY, 5, 0), + InputEvent.key(5, 1), + InputEvent.key(5, 0), ], ) @@ -1819,8 +1859,8 @@ def test_stop_injecting(self): push_events( fixtures.foo_device_2_keyboard, [ - new_event(evdev.events.EV_KEY, 5, 1), - new_event(evdev.events.EV_KEY, 5, 0), + InputEvent.key(5, 1), + InputEvent.key(5, 0), ], ) time.sleep(0.2) diff --git a/tests/integration/test_user_interface.py b/tests/integration/test_user_interface.py index ededb7e0d..87539bbf3 100644 --- a/tests/integration/test_user_interface.py +++ b/tests/integration/test_user_interface.py @@ -93,7 +93,7 @@ def test_combination_label_shows_combination(self): self.message_broker.publish( MappingData( input_combination=InputCombination( - InputConfig(type=EV_KEY, code=KEY_A) + [InputConfig(type=EV_KEY, code=KEY_A)] ), name="foo", ) diff --git a/tests/lib/cleanup.py b/tests/lib/cleanup.py index e33aa155c..efd021142 100644 --- a/tests/lib/cleanup.py +++ b/tests/lib/cleanup.py @@ -29,6 +29,10 @@ from pickle import UnpicklingError from unittest.mock import patch +# don't import anything from input_remapper gloablly here, because some files execute +# code when imported, which can screw up patches. I wish we had a dependency injection +# framework that patches together the dependencies during runtime... + from tests.lib.logger import logger from tests.lib.pipes import ( uinput_write_history_pipe, @@ -79,9 +83,10 @@ def quick_cleanup(log=True): from inputremapper.gui.utils import debounce_manager from inputremapper.configs.paths import get_config_path from inputremapper.injection.global_uinputs import global_uinputs + from tests.lib.global_uinputs import reset_global_uinputs_for_service if log: - print("Quick cleanup...") + logger.info("Quick cleanup...") debounce_manager.stop_all() @@ -151,10 +156,10 @@ def quick_cleanup(log=True): uinput.write_count = 0 uinput.write_history = [] - global_uinputs.is_service = True + reset_global_uinputs_for_service() if log: - print("Quick cleanup done") + logger.info("Quick cleanup done") def cleanup(): @@ -163,9 +168,8 @@ def cleanup(): Using this is slower, usually quick_cleanup() is sufficient. """ from inputremapper.groups import groups - from inputremapper.injection.global_uinputs import global_uinputs - print("Cleanup...") + logger.info("Cleanup...") os.system("pkill -f input-remapper-service") os.system("pkill -f input-remapper-control") @@ -173,7 +177,5 @@ def cleanup(): quick_cleanup(log=False) groups.refresh() - with patch.object(sys, "argv", ["input-remapper-service"]): - global_uinputs.prepare_all() - print("Cleanup done") + logger.info("Cleanup done") diff --git a/tests/lib/fixtures.py b/tests/lib/fixtures.py index e6506cdec..854efb922 100644 --- a/tests/lib/fixtures.py +++ b/tests/lib/fixtures.py @@ -23,12 +23,13 @@ import dataclasses import json from hashlib import md5 -from typing import Dict, Optional, Tuple, Iterable - +from typing import Dict, Optional import time import evdev +from tests.lib.logger import logger + # input-remapper is only interested in devices that have EV_KEY, add some # random other stuff to test that they are ignored. phys_foo = "usb-0000:03:00.0-1/input2" @@ -39,8 +40,8 @@ @dataclasses.dataclass(frozen=True) class Fixture: + path: str capabilities: Dict = dataclasses.field(default_factory=dict) - path: str = "" name: str = "unset" info: evdev.device.DeviceInfo = evdev.device.DeviceInfo(None, None, None, None) phys: str = "unset" @@ -51,7 +52,14 @@ def __hash__(self): def get_device_hash(self): s = str(self.capabilities) + self.name - return md5(s.encode()).hexdigest() + device_hash = md5(s.encode()).hexdigest() + logger.info( + 'Hash for fixture "%s" "%s": "%s"', + self.path, + self.name, + device_hash, + ) + return device_hash class _Fixtures: @@ -69,7 +77,7 @@ class _Fixtures: ) # Another "Foo Device", which will get an incremented key. # If possible write tests using this one, because name != key here and - # that would be important to test as well. Otherwise the tests can't + # that would be important to test as well. Otherwise, the tests can't # see if the groups correct attribute is used in functions and paths. dev_input_event11 = Fixture( capabilities={ @@ -177,7 +185,7 @@ class _Fixtures: path="/dev/input/event31", ) # input-remapper devices are not displayed in the ui, some instance - # of input-remapper started injecting apparently. + # of input-remapper started injecting, apparently. dev_input_event40 = Fixture( capabilities={evdev.ecodes.EV_KEY: keyboard_keys}, phys="input-remapper/input1", @@ -239,6 +247,10 @@ def __setitem__(self, key: str, value: [Fixture | dict]): def __iter__(self): return iter([*self._iter, *self._dynamic_fixtures.values()]) + def get_paths(self): + """Get a list of all available device paths.""" + return list(self._dynamic_fixtures.keys()) + def reset(self): self._dynamic_fixtures = {} @@ -308,54 +320,19 @@ def QuxSlashDeviceQuestionmark(self): fixtures = _Fixtures() -def get_combination_config( - *event_tuples: Tuple[int, int] | Tuple[int, int, int] -) -> Iterable[Dict[str, int]]: - """convenient function to get a iterable of dicts, InputEvent.event_tuple's""" - - for event in event_tuples: - if len(event) == 3: - yield {k: v for k, v in zip(("type", "code", "analog_threshold"), event)} - elif len(event) == 2: - yield {k: v for k, v in zip(("type", "code"), event)} - else: - raise TypeError - - -def get_ui_mapping(combination=None, target_uinput="keyboard", output_symbol="a"): - """Convenient function to get a valid mapping.""" - from inputremapper.configs.mapping import UIMapping - - if not combination: - combination = get_combination_config((99, 99)) - - return UIMapping( - input_combination=combination, - target_uinput=target_uinput, - output_symbol=output_symbol, - ) +def new_event(type, code, value, timestamp): + """Create a new InputEvent. + Handy because of the annoying sec and usec arguments of the regular + evdev.InputEvent constructor. -def get_key_mapping(combination=None, target_uinput="keyboard", output_symbol="a"): - """Convenient function to get a valid mapping.""" - from inputremapper.configs.mapping import Mapping - - if not combination: - combination = [{"type": 99, "code": 99, "analog_threshold": 99}] - - return Mapping( - input_combination=combination, - target_uinput=target_uinput, - output_symbol=output_symbol, - ) - - -def new_event(type, code, value, timestamp=None, offset=0): - """Create a new input_event.""" - from tests.lib.patches import InputEvent + Prefer using `InputEvent.key()`, `InputEvent.abs()`, `InputEvent.rel()` or just + `InputEvent(0, 0, 1234, 2345, 3456)`. + """ + from inputremapper.input_event import InputEvent if timestamp is None: - timestamp = time.time() + offset + timestamp = time.time() sec = int(timestamp) usec = timestamp % 1 * 1000000 @@ -368,27 +345,32 @@ def prepare_presets(): "Foo Device 2/preset3" is the newest and "Foo Device 2/preset2" is set to autoload """ from inputremapper.configs.preset import Preset + from inputremapper.configs.mapping import Mapping from inputremapper.configs.paths import get_config_path, get_preset_path from inputremapper.configs.global_config import global_config + from inputremapper.configs.input_config import InputCombination preset1 = Preset(get_preset_path("Foo Device", "preset1")) preset1.add( - get_key_mapping(combination=get_combination_config((1, 1)), output_symbol="b") + Mapping.from_combination( + InputCombination.from_tuples((1, 1)), + output_symbol="b", + ) ) - preset1.add(get_key_mapping(combination=get_combination_config((1, 2)))) + preset1.add(Mapping.from_combination(InputCombination.from_tuples((1, 2)))) preset1.save() time.sleep(0.1) preset2 = Preset(get_preset_path("Foo Device", "preset2")) - preset2.add(get_key_mapping(combination=get_combination_config((1, 3)))) - preset2.add(get_key_mapping(combination=get_combination_config((1, 4)))) + preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 3)))) + preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 4)))) preset2.save() # make sure the timestamp of preset 3 is the newest, # so that it will be automatically loaded by the GUI time.sleep(0.1) preset3 = Preset(get_preset_path("Foo Device", "preset3")) - preset3.add(get_key_mapping(combination=get_combination_config((1, 5)))) + preset3.add(Mapping.from_combination(InputCombination.from_tuples((1, 5)))) preset3.save() with open(get_config_path("config.json"), "w") as file: diff --git a/tests/lib/global_uinputs.py b/tests/lib/global_uinputs.py new file mode 100644 index 000000000..9e96bbcc0 --- /dev/null +++ b/tests/lib/global_uinputs.py @@ -0,0 +1,35 @@ +#!/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 sys +from unittest.mock import patch + +from inputremapper.injection.global_uinputs import global_uinputs + + +def reset_global_uinputs_for_service(): + with patch.object(sys, "argv", ["input-remapper-service"]): + # patch argv for global_uinputs to think it is a service + global_uinputs.reset() + + +def reset_global_uinputs_for_gui(): + with patch.object(sys, "argv", ["input-remapper-gtk"]): + global_uinputs.reset() diff --git a/tests/lib/patches.py b/tests/lib/patches.py index c00ae7f79..07ef074c5 100644 --- a/tests/lib/patches.py +++ b/tests/lib/patches.py @@ -29,6 +29,7 @@ import evdev +from inputremapper.utils import get_evdev_constant_name from tests.lib.constants import EVENT_READ_TIMEOUT, MIN_ABS, MAX_ABS from tests.lib.fixtures import Fixture, fixtures, new_event from tests.lib.pipes import ( @@ -56,9 +57,16 @@ class InputDevice: def __init__(self, path): if path != "justdoit" and not fixtures.get(path): + # beware that fixtures keys and the path attribute of a fixture can + # theoretically be different. I don't know if this is the case right now + logger.error( + 'path "%s" was not found in fixtures. available: %s', + path, + list(fixtures.get_paths()), + ) raise FileNotFoundError() if path == "justdoit": - self._fixture = Fixture() + self._fixture = Fixture(path="justdoit") else: self._fixture = fixtures[path] @@ -214,35 +222,43 @@ def capabilities(self, verbose=False, absinfo=True): def write(self, type, code, value): self.write_count += 1 - event = new_event(type, code, value) + event = new_event(type, code, value, time.time()) uinput_write_history.append(event) uinput_write_history_pipe[1].send(event) self.write_history.append(event) - logger.info("%s written", (type, code, value)) + logger.info( + '%s %s written to "%s"', + (type, code, value), + get_evdev_constant_name(type, code), + self.name, + ) def syn(self): pass -# TODO inherit from input-remappers InputEvent? -# makes convert_to_internal_events obsolete -class InputEvent(evdev.InputEvent): - def __init__(self, sec, usec, type, code, value): - self.t = (type, code, value) - super().__init__(sec, usec, type, code, value) - - def copy(self): - return InputEvent(self.sec, self.usec, self.type, self.code, self.value) - - def patch_evdev(): def list_devices(): return [fixture_.path for fixture_ in fixtures] + class PatchedInputEvent(evdev.InputEvent): + def __init__(self, sec, usec, type, code, value): + self.t = (type, code, value) + super().__init__(sec, usec, type, code, value) + + def copy(self): + return PatchedInputEvent( + self.sec, + self.usec, + self.type, + self.code, + self.value, + ) + evdev.list_devices = list_devices evdev.InputDevice = InputDevice evdev.UInput = UInput - evdev.InputEvent = InputEvent + evdev.InputEvent = PatchedInputEvent def patch_events(): diff --git a/tests/lib/pipes.py b/tests/lib/pipes.py index 44e1db36a..40557611d 100644 --- a/tests/lib/pipes.py +++ b/tests/lib/pipes.py @@ -18,6 +18,8 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +"""Reading events from fixtures, making fixtures act like they are sending events.""" + from __future__ import annotations import multiprocessing diff --git a/tests/lib/stuff.py b/tests/lib/stuff.py index ccc07cc67..0d03297a8 100644 --- a/tests/lib/stuff.py +++ b/tests/lib/stuff.py @@ -23,13 +23,6 @@ from unittest.mock import patch -def convert_to_internal_events(events): - """Convert an iterable of InputEvent to a list of inputremapper.InputEvent.""" - from inputremapper.input_event import InputEvent as InternalInputEvent - - return [InternalInputEvent.from_event(event) for event in events] - - def spy(obj, name): """Convenient wrapper for patch.object(..., ..., wraps=...).""" return patch.object(obj, name, wraps=obj.__getattribute__(name)) diff --git a/tests/test.py b/tests/test.py index 084397d27..6fb8da384 100644 --- a/tests/test.py +++ b/tests/test.py @@ -126,9 +126,10 @@ def is_service_running(): # patch_warnings() -def main(): - update_inputremapper_verbosity() +update_inputremapper_verbosity() + +def main(): cleanup() # https://docs.python.org/3/library/argparse.html parser = argparse.ArgumentParser(description=__doc__) diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index 84357b830..8797eef02 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -17,10 +17,8 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from inputremapper.configs.input_config import InputConfig - +from inputremapper.input_event import InputEvent from tests.lib.cleanup import quick_cleanup -from tests.lib.fixtures import get_key_mapping, get_combination_config from evdev.ecodes import ( EV_REL, EV_ABS, @@ -34,6 +32,7 @@ from inputremapper.injection.context import Context from inputremapper.configs.preset import Preset from inputremapper.configs.mapping import Mapping +from inputremapper.configs.input_config import InputConfig, InputCombination class TestContext(unittest.TestCase): @@ -44,23 +43,31 @@ def setUpClass(cls): def test_callbacks(self): preset = Preset() cfg = { - "input_combination": get_combination_config((EV_ABS, ABS_X)), + "input_combination": InputCombination.from_tuples((EV_ABS, ABS_X)), "target_uinput": "mouse", "output_type": EV_REL, "output_code": REL_HWHEEL_HI_RES, } preset.add(Mapping(**cfg)) # abs x -> wheel - cfg["input_combination"] = get_combination_config((EV_ABS, ABS_Y)) + cfg["input_combination"] = InputCombination.from_tuples((EV_ABS, ABS_Y)) cfg["output_code"] = REL_WHEEL_HI_RES preset.add(Mapping(**cfg)) # abs y -> wheel - preset.add(get_key_mapping(get_combination_config((1, 31)), "keyboard", "k(a)")) - preset.add(get_key_mapping(get_combination_config((1, 32)), "keyboard", "b")) + preset.add( + Mapping.from_combination( + InputCombination.from_tuples((1, 31)), "keyboard", "k(a)" + ) + ) + preset.add( + Mapping.from_combination( + InputCombination.from_tuples((1, 32)), "keyboard", "b" + ) + ) # overlapping combination for (1, 32, 1) preset.add( - get_key_mapping( - get_combination_config((1, 32), (1, 33), (1, 34)), + Mapping.from_combination( + InputCombination.from_tuples((1, 32), (1, 33), (1, 34)), "keyboard", "c", ) @@ -68,29 +75,37 @@ def test_callbacks(self): # map abs x to key "b" preset.add( - get_key_mapping( - get_combination_config((EV_ABS, ABS_X, 20)), + Mapping.from_combination( + InputCombination.from_tuples((EV_ABS, ABS_X, 20)), "keyboard", "d", ), ) - context = Context(preset) - # expected callbacks and their lengths: - callbacks = { + context = Context(preset, {}, {}) + + expected_num_callbacks = { # ABS_X -> "d" and ABS_X -> wheel have the same type and code - InputConfig(type=EV_ABS, code=ABS_X).input_match_hash: 2, - InputConfig(type=EV_ABS, code=ABS_Y).input_match_hash: 1, - InputConfig(type=1, code=31).input_match_hash: 1, - # even though we have 2 mappings with this type and code, we only expect one callback - # because they both map to keys. We don't want to trigger two mappings with the same key press - InputConfig(type=1, code=32).input_match_hash: 1, - InputConfig(type=1, code=33).input_match_hash: 1, - InputConfig(type=1, code=34).input_match_hash: 1, + InputEvent.abs(ABS_X, 1): 2, + InputEvent.abs(ABS_Y, 1): 1, + InputEvent.key(31, 1): 1, + # even though we have 2 mappings with this type and code, we only expect + # one callback because they both map to keys. We don't want to trigger two + # mappings with the same key press + InputEvent.key(32, 1): 1, + InputEvent.key(33, 1): 1, + InputEvent.key(34, 1): 1, } - self.assertEqual(set(callbacks.keys()), set(context._notify_callbacks.keys())) - for key, val in callbacks.items(): - self.assertEqual(val, len(context._notify_callbacks[key])) + + self.assertEqual( + set([event.input_match_hash for event in expected_num_callbacks.keys()]), + set(context._notify_callbacks.keys()), + ) + for input_event, num_callbacks in expected_num_callbacks.items(): + self.assertEqual( + num_callbacks, + len(context.get_notify_callbacks(input_event)), + ) # 7 unique input events in the preset self.assertEqual(7, len(context._handlers)) diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index db7cde9fb..6a45ea3d6 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -87,6 +87,7 @@ def test_autoload(self): start_history = [] stop_counter = 0 + # using an actual injector is not within the scope of this test class Injector: def stop_injecting(self, *args, **kwargs): diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index aceecd8c9..229f0645f 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -53,7 +53,7 @@ from tests.lib.cleanup import quick_cleanup from tests.lib.stuff import spy from tests.lib.patches import FakeDaemonProxy -from tests.lib.fixtures import fixtures, prepare_presets, get_combination_config +from tests.lib.fixtures import fixtures, prepare_presets from inputremapper.configs.global_config import GlobalConfig from inputremapper.gui.controller import Controller, MAPPING_DEFAULTS from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME @@ -457,7 +457,7 @@ def test_on_update_mapping(self): self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=4)) + combination=InputCombination([InputConfig(type=1, code=4)]) ) with patch.object(self.data_manager, "update_mapping") as mock: @@ -514,7 +514,7 @@ def test_deletes_mapping_when_confirmed(self): self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) self.message_broker.subscribe( MessageType.user_confirm_request, lambda msg: msg.respond(True) ) @@ -524,7 +524,7 @@ def test_deletes_mapping_when_confirmed(self): preset = Preset(get_preset_path("Foo Device", "preset2")) preset.load() self.assertIsNone( - preset.get_mapping(InputCombination(InputConfig(type=1, code=3))) + preset.get_mapping(InputCombination([InputConfig(type=1, code=3)])) ) def test_does_not_delete_mapping_when_not_confirmed(self): @@ -533,7 +533,7 @@ def test_does_not_delete_mapping_when_not_confirmed(self): self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) self.user_interface.confirm_delete.configure_mock( return_value=Gtk.ResponseType.CANCEL ) @@ -544,7 +544,7 @@ def test_does_not_delete_mapping_when_not_confirmed(self): preset = Preset(get_preset_path("Foo Device", "preset2")) preset.load() self.assertIsNotNone( - preset.get_mapping(InputCombination(InputConfig(type=1, code=3))) + preset.get_mapping(InputCombination([InputConfig(type=1, code=3)])) ) def test_should_update_combination(self): @@ -552,7 +552,7 @@ def test_should_update_combination(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) calls: List[CombinationUpdate] = [] @@ -561,13 +561,13 @@ def f(data): self.message_broker.subscribe(MessageType.combination_update, f) self.controller.update_combination( - InputCombination(InputConfig(type=1, code=10)) + InputCombination([InputConfig(type=1, code=10)]) ) self.assertEqual( calls[0], CombinationUpdate( - InputCombination(InputConfig(type=1, code=3)), - InputCombination(InputConfig(type=1, code=10)), + InputCombination([InputConfig(type=1, code=3)]), + InputCombination([InputConfig(type=1, code=10)]), ), ) @@ -576,7 +576,7 @@ def test_should_not_update_combination(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) calls: List[CombinationUpdate] = [] @@ -585,7 +585,7 @@ def f(data): self.message_broker.subscribe(MessageType.combination_update, f) self.controller.update_combination( - InputCombination(InputConfig(type=1, code=4)) + InputCombination([InputConfig(type=1, code=4)]) ) self.assertEqual(len(calls), 0) @@ -632,7 +632,7 @@ 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(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) calls: List[CombinationUpdate] = [] @@ -643,25 +643,25 @@ def f(data): self.controller.start_key_recording() self.message_broker.publish( - CombinationRecorded(InputCombination(InputConfig(type=1, code=10))) + CombinationRecorded(InputCombination([InputConfig(type=1, code=10)])) ) self.assertEqual( calls[0], CombinationUpdate( - InputCombination(InputConfig(type=1, code=3)), - InputCombination(InputConfig(type=1, code=10)), + InputCombination([InputConfig(type=1, code=3)]), + InputCombination([InputConfig(type=1, code=10)]), ), ) self.message_broker.publish( CombinationRecorded( - InputCombination(get_combination_config((1, 10), (1, 3))) + InputCombination(InputCombination.from_tuples((1, 10), (1, 3))) ) ) self.assertEqual( calls[1], CombinationUpdate( - InputCombination(InputConfig(type=1, code=10)), - InputCombination(get_combination_config((1, 10), (1, 3))), + InputCombination([InputConfig(type=1, code=10)]), + InputCombination(InputCombination.from_tuples((1, 10), (1, 3))), ), ) @@ -669,7 +669,7 @@ 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(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) calls: List[CombinationUpdate] = [] @@ -679,7 +679,7 @@ def f(data): self.message_broker.subscribe(MessageType.combination_update, f) self.message_broker.publish( - CombinationRecorded(InputCombination(InputConfig(type=1, code=10))) + CombinationRecorded(InputCombination([InputConfig(type=1, code=10)])) ) self.assertEqual(len(calls), 0) @@ -687,7 +687,7 @@ 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(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) calls: List[CombinationUpdate] = [] @@ -698,12 +698,12 @@ def f(data): self.controller.start_key_recording() self.message_broker.publish( - CombinationRecorded(InputCombination(InputConfig(type=1, code=10))) + CombinationRecorded(InputCombination([InputConfig(type=1, code=10)])) ) self.message_broker.signal(MessageType.recording_finished) self.message_broker.publish( CombinationRecorded( - InputCombination(get_combination_config((1, 10), (1, 3))) + InputCombination(InputCombination.from_tuples((1, 10), (1, 3))) ) ) @@ -713,7 +713,7 @@ 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(InputCombination(InputConfig(type=1, code=3))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)])) calls: List[CombinationUpdate] = [] @@ -724,12 +724,12 @@ def f(data): self.controller.start_key_recording() self.message_broker.publish( - CombinationRecorded(InputCombination(InputConfig(type=1, code=10))) + CombinationRecorded(InputCombination([InputConfig(type=1, code=10)])) ) self.controller.stop_key_recording() self.message_broker.publish( CombinationRecorded( - InputCombination(get_combination_config((1, 10), (1, 3))) + InputCombination(InputCombination.from_tuples((1, 10), (1, 3))) ) ) @@ -762,7 +762,7 @@ def test_start_injecting_warns_about_btn_left(self): self.data_manager.load_preset("foo") self.data_manager.create_mapping() self.data_manager.update_mapping( - input_combination=InputCombination(InputConfig.btn_left()), + input_combination=InputCombination([InputConfig.btn_left()]), target_uinput="keyboard", output_symbol="a", ) @@ -788,7 +788,7 @@ def test_start_injecting_starts_with_btn_left_on_second_try(self): self.data_manager.load_preset("foo") self.data_manager.create_mapping() self.data_manager.update_mapping( - input_combination=InputCombination(InputConfig.btn_left()), + input_combination=InputCombination([InputConfig.btn_left()]), target_uinput="keyboard", output_symbol="a", ) @@ -805,14 +805,14 @@ def test_start_injecting_starts_with_btn_left_when_mapped_to_other_button(self): self.data_manager.load_preset("foo") self.data_manager.create_mapping() self.data_manager.update_mapping( - input_combination=InputCombination(InputConfig.btn_left()), + input_combination=InputCombination([InputConfig.btn_left()]), target_uinput="keyboard", output_symbol="a", ) self.data_manager.create_mapping() self.data_manager.load_mapping(InputCombination.empty_combination()) self.data_manager.update_mapping( - input_combination=InputCombination(InputConfig(type=1, code=5)), + input_combination=InputCombination([InputConfig(type=1, code=5)]), target_uinput="mouse", output_symbol="BTN_LEFT", ) @@ -947,10 +947,12 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( input_combination=InputCombination( - get_combination_config((1, 1), (1, 2), (1, 3)) + InputCombination.from_tuples((1, 1), (1, 2), (1, 3)) ) ) @@ -959,7 +961,7 @@ def test_move_event_up(self): ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 2), (1, 1), (1, 3))), + InputCombination(InputCombination.from_tuples((1, 2), (1, 1), (1, 3))), ) # now nothing changes self.controller.move_input_config_in_combination( @@ -967,17 +969,19 @@ def test_move_event_up(self): ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 2), (1, 1), (1, 3))), + InputCombination(InputCombination.from_tuples((1, 2), (1, 1), (1, 3))), ) 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( input_combination=InputCombination( - get_combination_config((1, 1), (1, 2), (1, 3)) + InputCombination.from_tuples((1, 1), (1, 2), (1, 3)) ) ) @@ -986,7 +990,7 @@ def test_move_event_down(self): ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 1), (1, 3), (1, 2))), + InputCombination(InputCombination.from_tuples((1, 1), (1, 3), (1, 2))), ) # now nothing changes self.controller.move_input_config_in_combination( @@ -994,30 +998,34 @@ def test_move_event_down(self): ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 1), (1, 3), (1, 2))), + InputCombination(InputCombination.from_tuples((1, 1), (1, 3), (1, 2))), ) 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.controller.move_input_config_in_combination( InputConfig(type=1, code=3), "down" ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 3))), + InputCombination(InputCombination.from_tuples((1, 3))), ) 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( input_combination=InputCombination( - get_combination_config((1, 1), (1, 2), (1, 3)) + InputCombination.from_tuples((1, 1), (1, 2), (1, 3)) ) ) mock = MagicMock() @@ -1031,7 +1039,9 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.load_input_config(InputConfig(type=1, code=3)) mock = MagicMock() self.message_broker.subscribe(MessageType.selected_event, mock) @@ -1042,7 +1052,9 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.load_input_config(InputConfig(type=1, code=3)) mock = MagicMock() self.message_broker.subscribe(MessageType.selected_event, mock) @@ -1065,33 +1077,37 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (1, 4)) + input_combination=InputCombination.from_tuples((1, 3), (1, 4)) ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 3), (1, 4))), + InputCombination(InputCombination.from_tuples((1, 3), (1, 4))), ) self.data_manager.load_input_config(InputConfig(type=1, code=4)) self.controller.remove_event() self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 3))), + InputCombination(InputCombination.from_tuples((1, 3))), ) 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (1, 4)) + input_combination=InputCombination.from_tuples((1, 3), (1, 4)) ) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((1, 3), (1, 4))), + InputCombination(InputCombination.from_tuples((1, 3), (1, 4))), ) self.data_manager.load_input_config(InputConfig(type=1, code=4)) @@ -1104,9 +1120,11 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (1, 4)) + input_combination=InputCombination.from_tuples((1, 3), (1, 4)) ) self.data_manager.load_input_config(InputConfig(type=1, code=3)) @@ -1127,10 +1145,10 @@ def test_set_event_as_analog_saves(self): self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") self.data_manager.update_mapping( - input_combination=get_combination_config((3, 0, 10)) + input_combination=InputCombination.from_tuples((3, 0, 10)) ) self.data_manager.load_mapping( - InputCombination(get_combination_config((3, 0, 10))) + InputCombination(InputCombination.from_tuples((3, 0, 10))) ) self.data_manager.load_input_config( InputConfig(type=3, code=0, analog_threshold=10) @@ -1148,9 +1166,11 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((3, 0, 10)) + input_combination=InputCombination.from_tuples((3, 0, 10)) ) self.data_manager.load_input_config( InputConfig(type=3, code=0, analog_threshold=10) @@ -1159,23 +1179,25 @@ def test_set_event_as_analog_sets_input_to_analog(self): self.controller.set_event_as_analog(True) self.assertEqual( self.data_manager.active_mapping.input_combination, - InputCombination(get_combination_config((3, 0))), + InputCombination(InputCombination.from_tuples((3, 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((2, 0)) + input_combination=InputCombination.from_tuples((2, 0)) ) self.data_manager.load_input_config(InputConfig(type=2, code=0)) self.controller.set_event_as_analog(False) combinations = [ - InputCombination(get_combination_config((2, 0, 1))), - InputCombination(get_combination_config((2, 0, -1))), + InputCombination(InputCombination.from_tuples((2, 0, 1))), + InputCombination(InputCombination.from_tuples((2, 0, -1))), ] self.assertIn(self.data_manager.active_mapping.input_combination, combinations) @@ -1183,16 +1205,18 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((3, 0)) + input_combination=InputCombination.from_tuples((3, 0)) ) self.data_manager.load_input_config(InputConfig(type=3, code=0)) self.controller.set_event_as_analog(False) combinations = [ - InputCombination(get_combination_config((3, 0, 10))), - InputCombination(get_combination_config((3, 0, -10))), + InputCombination(InputCombination.from_tuples((3, 0, 10))), + InputCombination(InputCombination.from_tuples((3, 0, -10))), ] self.assertIn(self.data_manager.active_mapping.input_combination, combinations) @@ -1200,7 +1224,9 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.load_input_config(InputConfig(type=1, code=3)) mock = MagicMock() @@ -1217,9 +1243,11 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((3, 0, 10)) + input_combination=InputCombination.from_tuples((3, 0, 10)) ) self.data_manager.load_input_config( InputConfig(type=3, code=0, analog_threshold=10) @@ -1240,9 +1268,11 @@ 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(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((3, 0)) + input_combination=InputCombination.from_tuples((3, 0)) ) self.data_manager.load_input_config(InputConfig(type=3, code=0)) @@ -1261,7 +1291,9 @@ def test_update_mapping_type_will_ask_user_when_output_symbol_is_set(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) request: UserConfirmRequest = None def f(r: UserConfirmRequest): @@ -1276,7 +1308,9 @@ def test_update_mapping_type_will_notify_user_to_recorde_analog_input(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping(output_symbol=None) request: UserConfirmRequest = None @@ -1292,9 +1326,11 @@ def test_update_mapping_type_will_tell_user_which_input_is_used_as_analog(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (2, 1, 1)), + input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)), output_symbol=None, ) request: UserConfirmRequest = None @@ -1311,9 +1347,11 @@ def test_update_mapping_type_will_will_autoconfigure_the_input(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (2, 1, 1)), + input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)), output_symbol=None, ) @@ -1326,7 +1364,7 @@ def test_update_mapping_type_will_will_autoconfigure_the_input(self): mapping_type="analog", output_symbol=None, input_combination=InputCombination( - get_combination_config((1, 3), (2, 1)) + InputCombination.from_tuples((1, 3), (2, 1)) ), ) @@ -1334,7 +1372,9 @@ def test_update_mapping_type_will_abort_when_user_denys(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.message_broker.subscribe( MessageType.user_confirm_request, lambda r: r.respond(False) @@ -1344,7 +1384,7 @@ def test_update_mapping_type_will_abort_when_user_denys(self): mock.assert_not_called() self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (2, 1)), + input_combination=InputCombination.from_tuples((1, 3), (2, 1)), output_symbol=None, mapping_type="analog", ) @@ -1356,7 +1396,9 @@ def test_update_mapping_type_will_delete_output_symbol_when_user_confirms(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.message_broker.subscribe( MessageType.user_confirm_request, lambda r: r.respond(True) @@ -1369,9 +1411,11 @@ def test_update_mapping_will_ask_user_to_set_trigger_threshold(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (2, 1)), + input_combination=InputCombination.from_tuples((1, 3), (2, 1)), output_symbol=None, mapping_type="analog", ) @@ -1389,9 +1433,11 @@ def test_update_mapping_update_to_analog_without_asking(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (2, 1)), + input_combination=InputCombination.from_tuples((1, 3), (2, 1)), output_symbol=None, ) mock = MagicMock() @@ -1403,9 +1449,11 @@ def test_update_mapping_update_to_key_macro_without_asking(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (2, 1, 1)), + input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)), mapping_type="analog", output_symbol=None, ) @@ -1418,9 +1466,11 @@ def test_update_mapping_will_remove_output_type_and_code(self): prepare_presets() self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") - self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3)))) + self.data_manager.load_mapping( + InputCombination(InputCombination.from_tuples((1, 3))) + ) self.data_manager.update_mapping( - input_combination=get_combination_config((1, 3), (2, 1)), + input_combination=InputCombination.from_tuples((1, 3), (2, 1)), output_symbol=None, mapping_type="analog", ) diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index fa1559cb3..b7293376f 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -17,15 +17,16 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +from evdev._ecodes import EV_ABS - +from inputremapper.input_event import InputEvent from tests.test import is_service_running from tests.lib.logger import logger from tests.lib.cleanup import cleanup -from tests.lib.fixtures import new_event, get_combination_config +from tests.lib.fixtures import Fixture from tests.lib.pipes import push_events, uinput_write_history_pipe from tests.lib.tmp import tmp -from tests.lib.fixtures import fixtures, get_key_mapping +from tests.lib.fixtures import fixtures import os import unittest @@ -34,10 +35,11 @@ import json import evdev -from evdev.ecodes import EV_KEY, EV_ABS, KEY_B, KEY_A, ABS_X, BTN_A, BTN_B +from evdev.ecodes import EV_KEY, KEY_B, KEY_A, ABS_X, BTN_A, BTN_B from pydbus import SystemBus from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.mapping import Mapping from inputremapper.configs.global_config import global_config from inputremapper.groups import groups from inputremapper.configs.paths import get_config_path, mkdir, get_preset_path @@ -115,8 +117,6 @@ def test_daemon(self): preset_name = "foo" - ev = (EV_ABS, ABS_X) - group = groups.find(name="gamepad") # unrelated group that shouldn't be affected at all @@ -124,15 +124,21 @@ def test_daemon(self): preset = Preset(group.get_preset_path(preset_name)) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=BTN_A)), - "keyboard", - "a", + Mapping.from_combination( + input_combination=InputCombination( + [InputConfig(type=EV_KEY, code=BTN_A)] + ), + target_uinput="keyboard", + output_symbol="a", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config((*ev, -1))), "keyboard", "b" + Mapping.from_combination( + input_combination=InputCombination( + [InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=-1)] + ), + target_uinput="keyboard", + output_symbol="b", ) ) preset.save() @@ -141,7 +147,10 @@ def test_daemon(self): """Injection 1""" # should forward the event unchanged - push_events(fixtures.gamepad, [new_event(EV_KEY, BTN_B, 1)]) + push_events( + fixtures.gamepad, + [InputEvent.key(BTN_B, 1, fixtures.gamepad.get_device_hash())], + ) self.daemon = Daemon() @@ -184,7 +193,10 @@ def test_daemon(self): time.sleep(0.1) # -1234 will be classified as -1 by the injector - push_events(fixtures.gamepad, [new_event(*ev, -1234)]) + push_events( + fixtures.gamepad, + [InputEvent.abs(ABS_X, -1234, fixtures.gamepad.get_device_hash())], + ) time.sleep(0.1) self.assertTrue(uinput_write_history_pipe[0].poll()) @@ -213,7 +225,7 @@ def test_refresh_on_start(self): os.remove(get_config_path("xmodmap.json")) preset_name = "foo" - ev = (EV_KEY, 9) + key_code = 9 group_name = "9876 name" # expected key of the group @@ -228,8 +240,10 @@ def test_refresh_on_start(self): preset = Preset(get_preset_path(group_name, preset_name)) preset.add( - get_key_mapping( - InputCombination(get_combination_config(ev)), "keyboard", "a" + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=key_code)]), + "keyboard", + "a", ) ) @@ -246,13 +260,15 @@ def test_refresh_on_start(self): groups.refresh() # the daemon is supposed to find this device by calling refresh - fixtures[self.new_fixture_path] = { - "capabilities": {evdev.ecodes.EV_KEY: [ev[1]]}, - "phys": "9876 phys", - "info": evdev.device.DeviceInfo(4, 5, 6, 7), - "name": group_name, - } - push_events(fixtures[self.new_fixture_path], [new_event(*ev, 1)]) + fixture = Fixture( + capabilities={evdev.ecodes.EV_KEY: [key_code]}, + phys="9876 phys", + info=evdev.device.DeviceInfo(4, 5, 6, 7), + name=group_name, + path=self.new_fixture_path, + ) + fixtures[self.new_fixture_path] = fixture + push_events(fixture, [InputEvent.key(key_code, 1, fixture.get_device_hash())]) self.daemon.start_injecting(group_key, preset_name) # test if the injector called groups.refresh successfully @@ -264,16 +280,16 @@ def test_refresh_on_start(self): self.assertTrue(uinput_write_history_pipe[0].poll()) event = uinput_write_history_pipe[0].recv() - self.assertEqual(event.t, (EV_KEY, KEY_A, 1)) + self.assertEqual(event, (EV_KEY, KEY_A, 1)) self.daemon.stop_injecting(group_key) time.sleep(0.2) self.assertEqual(self.daemon.get_state(group_key), InjectorState.STOPPED) def test_refresh_for_unknown_key(self): - device = "9876 name" + device_9876 = "9876 name" # this test only makes sense if this device is unknown yet - self.assertIsNone(groups.find(name=device)) + self.assertIsNone(groups.find(name=device_9876)) self.daemon = Daemon() @@ -282,25 +298,26 @@ def test_refresh_for_unknown_key(self): self.daemon.refresh() - fixtures[self.new_fixture_path] = { - "capabilities": {evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A]}, - "phys": "9876 phys", - "info": evdev.device.DeviceInfo(4, 5, 6, 7), - "name": device, - } + fixtures[self.new_fixture_path] = Fixture( + capabilities={evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A]}, + phys="9876 phys", + info=evdev.device.DeviceInfo(4, 5, 6, 7), + name=device_9876, + path=self.new_fixture_path, + ) self.daemon._autoload("25v7j9q4vtj") # this is unknown, so the daemon will scan the devices again # test if the injector called groups.refresh successfully - self.assertIsNotNone(groups.find(name=device)) + self.assertIsNotNone(groups.find(name=device_9876)) def test_xmodmap_file(self): + """Create a custom xmodmap file, expect the daemon to read keycodes from it.""" from_keycode = evdev.ecodes.KEY_A target = "keyboard" to_name = "q" to_keycode = 100 - event = (EV_KEY, from_keycode, 1) name = "Bar Device" preset_name = "foo" @@ -312,15 +329,26 @@ def test_xmodmap_file(self): preset = Preset(path) preset.add( - get_key_mapping( - InputCombination(get_combination_config(event)), target, to_name + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=from_keycode)]), + target, + to_name, ) ) preset.save() system_mapping.clear() - push_events(fixtures.bar_device, [new_event(*event)]) + push_events( + fixtures.bar_device, + [ + InputEvent.key( + from_keycode, + 1, + origin_hash=fixtures.bar_device.get_device_hash(), + ) + ], + ) # an existing config file is needed otherwise set_config_dir refuses # to use the directory @@ -328,10 +356,13 @@ def test_xmodmap_file(self): global_config.path = config_path global_config._save_config() + # finally, create the xmodmap file xmodmap_path = os.path.join(config_dir, "xmodmap.json") with open(xmodmap_path, "w") as file: file.write(f'{{"{to_name}":{to_keycode}}}') + # test setup complete + self.daemon = Daemon() self.daemon.set_config_dir(config_dir) @@ -355,8 +386,8 @@ def test_start_stop(self): pereset = Preset(group.get_preset_path(preset_name)) pereset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=KEY_A)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=KEY_A)]), "keyboard", "a", ) @@ -423,8 +454,8 @@ def test_autoload(self): preset = Preset(group.get_preset_path(preset_name)) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=KEY_A)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=KEY_A)]), "keyboard", "a", ) @@ -481,8 +512,8 @@ def test_autoload_2(self): group = groups.find(key="Foo Device 2") preset = Preset(group.get_preset_path(preset_name)) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=3, code=2, analog_threshold=1)), + Mapping.from_combination( + InputCombination([InputConfig(type=3, code=2, analog_threshold=1)]), "keyboard", "a", ) @@ -504,8 +535,8 @@ def test_autoload_3(self): preset = Preset(group.get_preset_path(preset_name)) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=3, code=2, analog_threshold=1)), + Mapping.from_combination( + InputCombination([InputConfig(type=3, code=2, analog_threshold=1)]), "keyboard", "a", ) diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index 3a36a6a86..3ef32e99d 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -42,7 +42,7 @@ from inputremapper.injection.global_uinputs import GlobalUInputs from tests.lib.cleanup import quick_cleanup from tests.lib.patches import FakeDaemonProxy -from tests.lib.fixtures import prepare_presets, get_combination_config +from tests.lib.fixtures import prepare_presets from inputremapper.configs.paths import get_preset_path from inputremapper.configs.preset import Preset @@ -163,7 +163,7 @@ def test_save_preset(self): listener = Listener() self.message_broker.subscribe(MessageType.mapping, listener) self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=1)) + combination=InputCombination([InputConfig(type=1, code=1)]) ) mapping: MappingData = listener.calls[0] @@ -171,7 +171,7 @@ def test_save_preset(self): control_preset.load() self.assertEqual( control_preset.get_mapping( - InputCombination(InputConfig(type=1, code=1)) + InputCombination([InputConfig(type=1, code=1)]) ).output_symbol, mapping.output_symbol, ) @@ -185,7 +185,7 @@ def test_save_preset(self): control_preset.load() self.assertEqual( control_preset.get_mapping( - InputCombination(InputConfig(type=1, code=1)) + InputCombination([InputConfig(type=1, code=1)]) ).output_symbol, "key(a)", ) @@ -379,7 +379,7 @@ def test_load_mapping(self): """should be able to load a mapping""" preset, _, _ = prepare_presets() expected_mapping = preset.get_mapping( - InputCombination(InputConfig(type=1, code=1)) + InputCombination([InputConfig(type=1, code=1)]) ) self.data_manager.load_group(group_key="Foo Device") @@ -387,7 +387,7 @@ def test_load_mapping(self): listener = Listener() self.message_broker.subscribe(MessageType.mapping, listener) self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=1)) + combination=InputCombination([InputConfig(type=1, code=1)]) ) mapping = listener.calls[0] @@ -402,7 +402,7 @@ def test_cannot_load_non_existing_mapping(self): self.assertRaises( KeyError, self.data_manager.load_mapping, - combination=InputCombination(InputConfig(type=1, code=1)), + combination=InputCombination([InputConfig(type=1, code=1)]), ) def test_cannot_load_mapping_without_preset(self): @@ -413,13 +413,13 @@ def test_cannot_load_mapping_without_preset(self): self.assertRaises( DataManagementError, self.data_manager.load_mapping, - combination=InputCombination(InputConfig(type=1, code=1)), + combination=InputCombination([InputConfig(type=1, code=1)]), ) self.data_manager.load_group("Foo Device") self.assertRaises( DataManagementError, self.data_manager.load_mapping, - combination=InputCombination(InputConfig(type=1, code=1)), + combination=InputCombination([InputConfig(type=1, code=1)]), ) def test_load_event(self): @@ -428,7 +428,7 @@ def test_load_event(self): self.message_broker.subscribe(MessageType.selected_event, mock) self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset1") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)])) self.data_manager.load_input_config(InputConfig(type=1, code=1)) mock.assert_called_once_with(InputConfig(type=1, code=1)) self.assertEqual( @@ -446,7 +446,7 @@ def test_cannot_load_event_when_not_in_mapping_combination(self): prepare_presets() self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset1") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)])) with self.assertRaises(ValueError): self.data_manager.load_input_config(InputConfig(type=1, code=5)) @@ -454,7 +454,7 @@ def test_update_event(self): prepare_presets() self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset1") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)])) self.data_manager.load_input_config(InputConfig(type=1, code=1)) self.data_manager.update_input_config(InputConfig(type=1, code=5)) self.assertEqual( @@ -465,7 +465,7 @@ def test_update_event_sends_messages(self): prepare_presets() self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset1") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)])) self.data_manager.load_input_config(InputConfig(type=1, code=1)) mock = MagicMock() @@ -476,8 +476,8 @@ def test_update_event_sends_messages(self): expected = [ call( CombinationUpdate( - InputCombination(InputConfig(type=1, code=1)), - InputCombination(InputConfig(type=1, code=5)), + InputCombination([InputConfig(type=1, code=1)]), + InputCombination([InputConfig(type=1, code=5)]), ) ), call(self.data_manager.active_mapping.get_bus_message()), @@ -489,7 +489,7 @@ def test_cannot_update_event_when_resulting_combination_exists(self): prepare_presets() self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset1") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)])) self.data_manager.load_input_config(InputConfig(type=1, code=1)) with self.assertRaises(KeyError): self.data_manager.update_input_config(InputConfig(type=1, code=2)) @@ -498,7 +498,7 @@ def test_cannot_update_event_when_not_loaded(self): prepare_presets() self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset1") - self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1))) + self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)])) with self.assertRaises(DataManagementError): self.data_manager.update_input_config(InputConfig(type=1, code=2)) @@ -508,7 +508,7 @@ def test_update_mapping_emits_mapping_changed(self): self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_preset(name="preset2") self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=4)) + combination=InputCombination([InputConfig(type=1, code=4)]) ) listener = Listener() @@ -530,7 +530,7 @@ def test_updated_mapping_can_be_saved(self): self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_preset(name="preset2") self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=4)) + combination=InputCombination([InputConfig(type=1, code=4)]) ) self.data_manager.update_mapping( @@ -542,7 +542,7 @@ def test_updated_mapping_can_be_saved(self): preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping) preset.load() - mapping = preset.get_mapping(InputCombination(InputConfig(type=1, code=4))) + mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)])) self.assertEqual(mapping.format_name(), "foo") self.assertEqual(mapping.output_symbol, "f") self.assertEqual(mapping.release_timeout, 0.3) @@ -553,7 +553,7 @@ def test_updated_mapping_saves_invalid_mapping(self): self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_preset(name="preset2") self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=4)) + combination=InputCombination([InputConfig(type=1, code=4)]) ) self.data_manager.update_mapping( @@ -563,7 +563,7 @@ def test_updated_mapping_saves_invalid_mapping(self): preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping) preset.load() - mapping = preset.get_mapping(InputCombination(InputConfig(type=1, code=4))) + mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)])) self.assertIsNotNone(mapping.get_error()) self.assertEqual(mapping.output_symbol, "bar") @@ -573,7 +573,7 @@ def test_update_mapping_combination_sends_massage(self): self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_preset(name="preset2") self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=4)) + combination=InputCombination([InputConfig(type=1, code=4)]) ) listener = Listener() self.message_broker.subscribe(MessageType.mapping, listener) @@ -581,21 +581,23 @@ def test_update_mapping_combination_sends_massage(self): # we expect a message for combination update first, and then for mapping self.data_manager.update_mapping( - input_combination=InputCombination(get_combination_config((1, 5), (1, 6))) + input_combination=InputCombination( + InputCombination.from_tuples((1, 5), (1, 6)) + ) ) self.assertEqual(listener.calls[0].message_type, MessageType.combination_update) self.assertEqual( listener.calls[0].old_combination, - InputCombination(InputConfig(type=1, code=4)), + InputCombination([InputConfig(type=1, code=4)]), ) self.assertEqual( listener.calls[0].new_combination, - InputCombination(get_combination_config((1, 5), (1, 6))), + InputCombination(InputCombination.from_tuples((1, 5), (1, 6))), ) self.assertEqual(listener.calls[1].message_type, MessageType.mapping) self.assertEqual( listener.calls[1].input_combination, - InputCombination(get_combination_config((1, 5), (1, 6))), + InputCombination(InputCombination.from_tuples((1, 5), (1, 6))), ) def test_cannot_update_mapping_combination(self): @@ -605,13 +607,13 @@ def test_cannot_update_mapping_combination(self): self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_preset(name="preset2") self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=4)) + combination=InputCombination([InputConfig(type=1, code=4)]) ) self.assertRaises( KeyError, self.data_manager.update_mapping, - input_combination=InputCombination(InputConfig(type=1, code=3)), + input_combination=InputCombination([InputConfig(type=1, code=3)]), ) def test_cannot_update_mapping(self): @@ -671,7 +673,7 @@ def test_delete_mapping(self): self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_preset(name="preset2") self.data_manager.load_mapping( - combination=InputCombination(InputConfig(type=1, code=3)) + combination=InputCombination([InputConfig(type=1, code=3)]) ) listener = Listener() @@ -682,7 +684,7 @@ def test_delete_mapping(self): self.data_manager.save() deleted_mapping = old_preset.get_mapping( - InputCombination(InputConfig(type=1, code=3)) + InputCombination([InputConfig(type=1, code=3)]) ) mappings = listener.calls[0].mappings preset_name = listener.calls[0].name diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index 92870e9e1..023a84841 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -62,21 +62,15 @@ from tests.lib.cleanup import cleanup from tests.lib.logger import logger from tests.lib.constants import MAX_ABS, MIN_ABS -from tests.lib.stuff import convert_to_internal_events -from tests.lib.fixtures import ( - Fixture, - fixtures, - get_key_mapping, - get_combination_config, -) +from tests.lib.fixtures import Fixture, fixtures class EventPipelineTestBase(unittest.IsolatedAsyncioTestCase): """Test the event pipeline form event_reader to UInput.""" def setUp(self): - # print("in setup") - # global_uinputs.prepare_all() + global_uinputs.is_service = True + global_uinputs.prepare_all() self.forward_uinput = evdev.UInput() self.stop_event = asyncio.Event() @@ -84,25 +78,30 @@ def tearDown(self) -> None: cleanup() async def asyncTearDown(self) -> None: + logger.info("setting stop_event for the reader") self.stop_event.set() await asyncio.sleep(0.5) @staticmethod async def send_events(events: Iterable[InputEvent], event_reader: EventReader): for event in events: - logger.info("sending into event_pipeline: %s", event.event_tuple) + logger.info("sending into event_pipeline: %s", event) await event_reader.handle(event) - def get_event_reader( + def create_event_reader( self, preset: Preset, source: Fixture, ) -> EventReader: - context = Context(preset) + """Create and start an EventReader.""" + context = Context( + preset, + source_devices={}, + forward_devices={source.get_device_hash(): self.forward_uinput}, + ) reader = EventReader( context, evdev.InputDevice(source.path), - self.forward_uinput, self.stop_event, ) asyncio.ensure_future(reader.run()) @@ -150,46 +149,46 @@ async def test_any_event_as_button(self): preset = Preset() preset.add( - get_key_mapping( - InputCombination(get_combination_config(b_down)), "keyboard", "b" + Mapping.from_combination( + InputCombination(InputCombination.from_tuples(b_down)), "keyboard", "b" ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config(c_down)), "keyboard", "c" + Mapping.from_combination( + InputCombination(InputCombination.from_tuples(c_down)), "keyboard", "c" ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config((*w_down[:2], -10))), + Mapping.from_combination( + InputCombination(InputCombination.from_tuples((*w_down[:2], -10))), "keyboard", "w", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config((*d_down[:2], 10))), + Mapping.from_combination( + InputCombination(InputCombination.from_tuples((*d_down[:2], 10))), "keyboard", "k(d)", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config((*s_down[:2], 10))), + Mapping.from_combination( + InputCombination(InputCombination.from_tuples((*s_down[:2], 10))), "keyboard", "s", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config((*a_down[:2], -10))), + Mapping.from_combination( + InputCombination(InputCombination.from_tuples((*a_down[:2], -10))), "keyboard", "a", ) ) # gamepad fixture - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [ @@ -211,9 +210,7 @@ async def test_any_event_as_button(self): # wait a bit for the rel_to_btn handler to send the key up await asyncio.sleep(0.1) - history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_KEY, code_b, 1)), 1) self.assertEqual(history.count((EV_KEY, code_c, 1)), 1) @@ -231,17 +228,25 @@ async def test_any_event_as_button(self): async def test_reset_releases_keys(self): """Make sure that macros and keys are releases when the stop event is set.""" preset = Preset() - input_cfg = InputCombination(InputConfig(type=1, code=1)).to_config() - preset.add(get_key_mapping(combination=input_cfg, output_symbol="hold(a)")) + input_cfg = InputCombination([InputConfig(type=EV_KEY, code=1)]).to_config() + preset.add( + Mapping.from_combination( + input_combination=input_cfg, output_symbol="hold(a)" + ) + ) - input_cfg = InputCombination(InputConfig(type=1, code=2)).to_config() - preset.add(get_key_mapping(combination=input_cfg, output_symbol="b")) + input_cfg = InputCombination([InputConfig(type=EV_KEY, code=2)]).to_config() + preset.add( + Mapping.from_combination(input_combination=input_cfg, output_symbol="b") + ) - input_cfg = InputCombination(InputConfig(type=1, code=3)).to_config() + input_cfg = InputCombination([InputConfig(type=EV_KEY, code=3)]).to_config() preset.add( - get_key_mapping(combination=input_cfg, output_symbol="modify(c,hold(d))"), + Mapping.from_combination( + input_combination=input_cfg, output_symbol="modify(c,hold(d))" + ), ) - event_reader = self.get_event_reader(preset, fixtures.foo_device_2_keyboard) + event_reader = self.create_event_reader(preset, fixtures.foo_device_2_keyboard) a = system_mapping.get("a") b = system_mapping.get("b") @@ -250,20 +255,16 @@ async def test_reset_releases_keys(self): await self.send_events( [ - InputEvent.from_tuple((1, 1, 1)), - InputEvent.from_tuple((1, 2, 1)), - InputEvent.from_tuple((1, 3, 1)), + InputEvent.key(1, 1), + InputEvent.key(2, 1), + InputEvent.key(3, 1), ], event_reader, ) await asyncio.sleep(0.1) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + forwarded_history = self.forward_uinput.write_history + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(forwarded_history), 0) # a down, b down, c down, d down @@ -272,21 +273,17 @@ async def test_reset_releases_keys(self): event_reader.context.reset() await asyncio.sleep(0.1) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + forwarded_history = self.forward_uinput.write_history + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(forwarded_history), 0) # all a, b, c, d down+up self.assertEqual(len(keyboard_history), 8) keyboard_history = keyboard_history[-4:] - self.assertIn((1, a, 0), keyboard_history) - self.assertIn((1, b, 0), keyboard_history) - self.assertIn((1, c, 0), keyboard_history) - self.assertIn((1, d, 0), keyboard_history) + self.assertIn((EV_KEY, a, 0), keyboard_history) + self.assertIn((EV_KEY, b, 0), keyboard_history) + self.assertIn((EV_KEY, c, 0), keyboard_history) + self.assertIn((EV_KEY, d, 0), keyboard_history) async def test_forward_abs(self): """Test if EV_ABS events are forwarded when other events of the same input are not.""" @@ -294,33 +291,30 @@ async def test_forward_abs(self): # BTN_A -> 77 system_mapping._set("b", 77) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=1, code=BTN_A)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=BTN_A)]), "keyboard", "b", ) ) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) # should forward them unmodified await self.send_events( [ - InputEvent.from_tuple((EV_ABS, ABS_X, 10)), - InputEvent.from_tuple((EV_ABS, ABS_Y, 20)), - InputEvent.from_tuple((EV_ABS, ABS_X, -30)), - InputEvent.from_tuple((EV_ABS, ABS_Y, -40)), + InputEvent.abs(ABS_X, 10), + InputEvent.abs(ABS_Y, 20), + InputEvent.abs(ABS_X, -30), + InputEvent.abs(ABS_Y, -40), # send them to keyboard 77 - InputEvent.from_tuple((EV_KEY, BTN_A, 1)), - InputEvent.from_tuple((EV_KEY, BTN_A, 0)), + InputEvent.key(BTN_A, 1), + InputEvent.key(BTN_A, 0), ], event_reader, ) - # convert the write-history to some easier to manage list - history = convert_to_internal_events(self.forward_uinput.write_history) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + history = self.forward_uinput.write_history + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_ABS, ABS_X, 10)), 1) self.assertEqual(history.count((EV_ABS, ABS_Y, 20)), 1) @@ -335,34 +329,31 @@ async def test_forward_rel(self): # BTN_A -> 77 system_mapping._set("b", 77) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=1, code=BTN_LEFT)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=BTN_LEFT)]), "keyboard", "b", ) ) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) # should forward them unmodified await self.send_events( [ - InputEvent.from_tuple((EV_REL, REL_X, 10)), - InputEvent.from_tuple((EV_REL, REL_Y, 20)), - InputEvent.from_tuple((EV_REL, REL_X, -30)), - InputEvent.from_tuple((EV_REL, REL_Y, -40)), + InputEvent.rel(REL_X, 10), + InputEvent.rel(REL_Y, 20), + InputEvent.rel(REL_X, -30), + InputEvent.rel(REL_Y, -40), # send them to keyboard 77 - InputEvent.from_tuple((EV_KEY, BTN_LEFT, 1)), - InputEvent.from_tuple((EV_KEY, BTN_LEFT, 0)), + InputEvent.key(BTN_LEFT, 1), + InputEvent.key(BTN_LEFT, 0), ], event_reader, ) await asyncio.sleep(0.1) - # convert the write-history to some easier to manage list - history = convert_to_internal_events(self.forward_uinput.write_history) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + history = self.forward_uinput.write_history + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_REL, REL_X, 10)), 1) self.assertEqual(history.count((EV_REL, REL_Y, 20)), 1) @@ -373,26 +364,54 @@ async def test_forward_rel(self): async def test_combination(self): """Test if combinations map to keys properly.""" - a = system_mapping.get("a") b = system_mapping.get("b") c = system_mapping.get("c") - mapping_1 = get_key_mapping( - InputCombination(InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=1)), + origin = fixtures.gamepad + origin_hash = origin.get_device_hash() + + mapping_1 = Mapping.from_combination( + InputCombination( + [ + InputConfig( + type=EV_ABS, + code=ABS_X, + analog_threshold=1, + origin_hash=origin_hash, + ) + ] + ), output_symbol="a", ) - mapping_2 = get_key_mapping( + + mapping_2 = Mapping.from_combination( InputCombination( - get_combination_config((EV_ABS, ABS_X, 1), (EV_KEY, BTN_A, 1)) + [ + InputConfig( + type=EV_ABS, + code=ABS_X, + analog_threshold=1, + origin_hash=origin_hash, + ), + InputConfig(type=EV_KEY, code=BTN_A, origin_hash=origin_hash), + ] ), output_symbol="b", ) - m3 = get_key_mapping( + + mapping_3 = Mapping.from_combination( InputCombination( - get_combination_config( - (EV_ABS, ABS_X, 1), (EV_KEY, BTN_A, 1), (EV_KEY, BTN_B, 1) - ), + [ + InputConfig( + type=EV_ABS, + code=ABS_X, + analog_threshold=1, + origin_hash=origin_hash, + ), + InputConfig(type=EV_KEY, code=BTN_A, origin_hash=origin_hash), + InputConfig(type=EV_KEY, code=BTN_B, origin_hash=origin_hash), + ] ), output_symbol="c", ) @@ -400,30 +419,33 @@ async def test_combination(self): preset = Preset() preset.add(mapping_1) preset.add(mapping_2) - preset.add(m3) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + preset.add(mapping_3) + + event_reader = self.create_event_reader(preset, origin) + # send_events awaits the event_reader to do its thing await self.send_events( [ # forwarded - InputEvent.from_tuple((EV_KEY, BTN_A, 1)), + InputEvent.key(BTN_A, 1, origin_hash), # triggers b, releases BTN_A, ABS_X - InputEvent.from_tuple((EV_ABS, ABS_X, 1234)), + InputEvent.abs(ABS_X, 1234, origin_hash), # triggers c, releases BTN_A, ABS_X, BTN_B - InputEvent.from_tuple((EV_KEY, BTN_B, 1)), + InputEvent.key(BTN_B, 1, origin_hash), ], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) - self.assertNotIn((1, a, 1), keyboard_history) - self.assertEqual(keyboard_history.count((1, c, 1)), 1) - self.assertEqual(keyboard_history.count((1, b, 1)), 1) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + + forwarded_history = self.forward_uinput.write_history + + self.assertNotIn((EV_KEY, a, 1), keyboard_history) + + # c and b should have been written, because the input from send_events + # should trigger the combination + self.assertEqual(keyboard_history.count((EV_KEY, c, 1)), 1) + self.assertEqual(keyboard_history.count((EV_KEY, b, 1)), 1) self.assertEqual(forwarded_history.count((EV_KEY, BTN_A, 1)), 1) self.assertIn((EV_KEY, BTN_A, 0), forwarded_history) @@ -432,87 +454,106 @@ async def test_combination(self): # release b and c) await self.send_events( - [InputEvent.from_tuple((EV_ABS, ABS_X, 0))], + [InputEvent.abs(ABS_X, 0, origin_hash)], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - self.assertNotIn((1, a, 1), keyboard_history) - self.assertNotIn((1, a, 0), keyboard_history) - self.assertEqual(keyboard_history.count((1, c, 0)), 1) - self.assertEqual(keyboard_history.count((1, b, 0)), 1) + + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + + self.assertNotIn((EV_KEY, a, 1), keyboard_history) + self.assertNotIn((EV_KEY, a, 0), keyboard_history) + self.assertEqual(keyboard_history.count((EV_KEY, c, 0)), 1) + self.assertEqual(keyboard_history.count((EV_KEY, b, 0)), 1) async def test_ignore_hold(self): # hold as in event-value 2, not in macro-hold. # linux will generate events with value 2 after input-remapper injected # the key-press, so input-remapper doesn't need to forward them. That # would cause duplicate events of those values otherwise. - key = (EV_KEY, KEY_A) - ev_1 = (*key, 1) - ev_2 = (*key, 2) - ev_3 = (*key, 0) + ev_1 = InputEvent.key(KEY_A, 1) + ev_2 = InputEvent.key(KEY_A, 2) + ev_3 = InputEvent.key(KEY_A, 0) preset = Preset() preset.add( - get_key_mapping( - InputCombination(get_combination_config(ev_1)), output_symbol="a" + Mapping.from_combination( + input_combination=InputCombination( + [InputConfig(type=EV_KEY, code=KEY_A)] + ), + output_symbol="a", ) ) a = system_mapping.get("a") - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( - [ - InputEvent.from_tuple(ev_1), - InputEvent.from_tuple(ev_2), - InputEvent.from_tuple(ev_3), - ], + [ev_1, ev_2, ev_3], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history self.assertEqual(len(keyboard_history), 2) self.assertEqual(len(forwarded_history), 0) - self.assertNotIn((1, a, 2), keyboard_history) + self.assertNotIn((EV_KEY, a, 2), keyboard_history) async def test_ignore_disabled(self): - ev_1 = (EV_ABS, ABS_HAT0Y, 1) - ev_2 = (EV_ABS, ABS_HAT0Y, 0) + origin = fixtures.gamepad + origin_hash = origin.get_device_hash() - ev_3 = (EV_ABS, ABS_HAT0X, 1) # disabled - ev_4 = (EV_ABS, ABS_HAT0X, 0) + ev_1 = InputEvent.abs(ABS_HAT0Y, 1, origin_hash) + ev_2 = InputEvent.abs(ABS_HAT0Y, 0, origin_hash) - ev_5 = (EV_KEY, KEY_A, 1) - ev_6 = (EV_KEY, KEY_A, 0) + ev_3 = InputEvent.abs(ABS_HAT0X, 1, origin_hash) # disabled + ev_4 = InputEvent.abs(ABS_HAT0X, 0, origin_hash) + + ev_5 = InputEvent.key(KEY_A, 1, origin_hash) + ev_6 = InputEvent.key(KEY_A, 0, origin_hash) combi_1 = (ev_5, ev_3) combi_2 = (ev_3, ev_5) preset = Preset() preset.add( - get_key_mapping( - InputCombination(get_combination_config(ev_1)), output_symbol="a" + Mapping.from_combination( + input_combination=InputCombination( + [ + InputConfig.from_input_event(ev_1), + ] + ), + output_symbol="a", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config(ev_3)), output_symbol="disable" + Mapping.from_combination( + input_combination=InputCombination( + [ + InputConfig.from_input_event(ev_3), + ] + ), + output_symbol="disable", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config(*combi_1)), output_symbol="b" + Mapping.from_combination( + input_combination=InputCombination( + ( + InputConfig.from_input_event(combi_1[0]), + InputConfig.from_input_event(combi_1[1]), + ) + ), + output_symbol="b", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config(*combi_2)), output_symbol="c" + Mapping.from_combination( + input_combination=InputCombination( + ( + InputConfig.from_input_event(combi_2[0]), + InputConfig.from_input_event(combi_2[1]), + ) + ), + output_symbol="c", ) ) @@ -520,98 +561,66 @@ async def test_ignore_disabled(self): b = system_mapping.get("b") c = system_mapping.get("c") - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, origin) """Single keys""" await self.send_events( [ - InputEvent.from_tuple(ev_1), # press a - InputEvent.from_tuple(ev_3), # disabled - InputEvent.from_tuple(ev_2), # release a - InputEvent.from_tuple(ev_4), # disabled + ev_1, # press a + ev_3, # disabled + ev_2, # release a + ev_4, # disabled ], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) - self.assertIn((1, a, 1), keyboard_history) - self.assertIn((1, a, 0), keyboard_history) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history + self.assertIn((EV_KEY, a, 1), keyboard_history) + self.assertIn((EV_KEY, a, 0), keyboard_history) self.assertEqual(len(keyboard_history), 2) self.assertEqual(len(forwarded_history), 0) """A combination that ends in a disabled key""" # ev_5 should be forwarded and the combination triggered - await self.send_events(map(InputEvent.from_tuple, combi_1), event_reader) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) - self.assertIn((1, b, 1), keyboard_history) + await self.send_events(combi_1, event_reader) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history + self.assertIn((EV_KEY, b, 1), keyboard_history) self.assertEqual(len(keyboard_history), 3) self.assertEqual(forwarded_history.count(ev_3), 0) self.assertEqual(forwarded_history.count(ev_5), 1) - self.assertTrue(forwarded_history.count((*ev_5[0:2], 0)) >= 1) + self.assertTrue(forwarded_history.count(ev_6) >= 1) # release what the combination maps to - await self.send_events( - [ - InputEvent.from_tuple((*ev_3[0:2], 0)), - InputEvent.from_tuple((*ev_5[0:2], 0)), - ], - event_reader, - ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) - self.assertIn((1, b, 0), keyboard_history) + await self.send_events([ev_4, ev_6], event_reader) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history + self.assertIn((EV_KEY, b, 0), keyboard_history) self.assertEqual(len(keyboard_history), 4) self.assertEqual(forwarded_history.count(ev_3), 0) - self.assertTrue(forwarded_history.count((*ev_5[0:2], 0)) >= 1) + self.assertTrue(forwarded_history.count(ev_6) >= 1) """A combination that starts with a disabled key""" # only the combination should get triggered - await self.send_events(map(InputEvent.from_tuple, combi_2), event_reader) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) - self.assertIn((1, c, 1), keyboard_history) + await self.send_events(combi_2, event_reader) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history + self.assertIn((EV_KEY, c, 1), keyboard_history) self.assertEqual(len(keyboard_history), 5) self.assertEqual(forwarded_history.count(ev_3), 0) self.assertEqual(forwarded_history.count(ev_5), 1) - self.assertTrue(forwarded_history.count((*ev_5[0:2], 0)) >= 1) + self.assertTrue(forwarded_history.count(ev_6) >= 1) # release what the combination maps to - await self.send_events( - [ - InputEvent.from_tuple((*ev_3[0:2], 0)), - InputEvent.from_tuple((*ev_5[0:2], 0)), - ], - event_reader, - ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + await self.send_events([ev_4, ev_6], event_reader) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history for event in keyboard_history: print(event.event_tuple) - self.assertIn((1, c, 0), keyboard_history) + self.assertIn((EV_KEY, c, 0), keyboard_history) self.assertEqual(len(keyboard_history), 6) self.assertEqual(forwarded_history.count(ev_3), 0) - self.assertTrue(forwarded_history.count((*ev_5[0:2], 0)) >= 1) + self.assertTrue(forwarded_history.count(ev_6) >= 1) async def test_combination_keycode_macro_mix(self): """Ev_1 triggers macro, ev_1 + ev_2 triggers key while the macro is @@ -627,40 +636,34 @@ async def test_combination_keycode_macro_mix(self): preset = Preset() preset.add( - get_key_mapping( - InputCombination(get_combination_config(down_1)), + Mapping.from_combination( + InputCombination(InputCombination.from_tuples(down_1)), output_symbol="h(k(a))", ) ) preset.add( - get_key_mapping( - InputCombination(get_combination_config(down_1, down_2)), + Mapping.from_combination( + InputCombination(InputCombination.from_tuples(down_1, down_2)), output_symbol="b", ) ) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) # macro starts await self.send_events([InputEvent.from_tuple(down_1)], event_reader) await asyncio.sleep(0.05) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history self.assertEqual(len(forwarded_history), 0) self.assertGreater(len(keyboard_history), 1) - self.assertNotIn((1, b, 1), keyboard_history) - self.assertIn((1, a, 1), keyboard_history) - self.assertIn((1, a, 0), keyboard_history) + self.assertNotIn((EV_KEY, b, 1), keyboard_history) + self.assertIn((EV_KEY, a, 1), keyboard_history) + self.assertIn((EV_KEY, a, 0), keyboard_history) # combination triggered await self.send_events([InputEvent.from_tuple(down_2)], event_reader) await asyncio.sleep(0) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertIn((EV_KEY, b, 1), keyboard_history) len_a = len(global_uinputs.get_uinput("keyboard").write_history) @@ -671,9 +674,7 @@ async def test_combination_keycode_macro_mix(self): # release await self.send_events([InputEvent.from_tuple(up_1)], event_reader) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history[-1], (EV_KEY, b, 0)) await asyncio.sleep(0.05) len_c = len(global_uinputs.get_uinput("keyboard").write_history) @@ -707,82 +708,72 @@ async def test_wheel_combination_release_failure(self): scroll = InputEvent.from_tuple((2, 8, -1)) scroll_release = InputEvent.from_tuple((2, 8, 0)) - btn_down = InputEvent.from_tuple((1, 276, 1)) - btn_up = InputEvent.from_tuple((1, 276, 0)) - combination = InputCombination(get_combination_config((1, 276, 1), (2, 8, -1))) + btn_down = InputEvent.key(276, 1) + btn_up = InputEvent.key(276, 0) + combination = InputCombination( + InputCombination.from_tuples((1, 276, 1), (2, 8, -1)) + ) system_mapping.clear() system_mapping._set("a", 30) a = 30 - m = get_key_mapping(combination, output_symbol="a") + m = Mapping.from_combination(combination, output_symbol="a") m.release_timeout = 0.1 # a higher release timeout to give time for assertions preset = Preset() preset.add(m) - event_reader = self.get_event_reader(preset, fixtures.foo_device_2_mouse) + event_reader = self.create_event_reader(preset, fixtures.foo_device_2_mouse) await self.send_events([btn_down], event_reader) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + forwarded_history = self.forward_uinput.write_history self.assertEqual(forwarded_history[0], btn_down) await self.send_events([scroll], event_reader) # "maps to 30" - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - self.assertEqual(keyboard_history[0], (1, a, 1)) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + self.assertEqual(keyboard_history[0], (EV_KEY, a, 1)) await self.send_events([scroll] * 5, event_reader) # nothing new since all of them were duplicate key downs - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(keyboard_history), 1) await self.send_events([btn_up], event_reader) # releasing the combination - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - self.assertEqual(keyboard_history[1], (1, a, 0)) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + self.assertEqual(keyboard_history[1], (EV_KEY, a, 0)) # more scroll events # it should be ignored as duplicate key-down await self.send_events([scroll] * 5, event_reader) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + forwarded_history = self.forward_uinput.write_history self.assertEqual(forwarded_history.count(scroll), 5) await self.send_events([scroll_release], event_reader) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + forwarded_history = self.forward_uinput.write_history self.assertEqual(forwarded_history[-1], scroll_release) async def test_can_not_map(self): """Inject events to wrong or invalid uinput.""" - ev_1 = (EV_KEY, KEY_A, 1) - ev_2 = (EV_KEY, KEY_B, 1) - ev_3 = (EV_KEY, KEY_C, 1) + ev_1 = InputEvent.key(KEY_A, 1) + ev_2 = InputEvent.key(KEY_B, 1) + ev_3 = InputEvent.key(KEY_C, 1) - ev_4 = (EV_KEY, KEY_A, 0) - ev_5 = (EV_KEY, KEY_B, 0) - ev_6 = (EV_KEY, KEY_C, 0) + ev_4 = InputEvent.key(KEY_A, 0) + ev_5 = InputEvent.key(KEY_B, 0) + ev_6 = InputEvent.key(KEY_C, 0) mapping_1 = Mapping( - input_combination=InputCombination(get_combination_config(ev_2)), + input_combination=InputCombination([InputConfig.from_input_event(ev_2)]), target_uinput="keyboard", output_type=EV_KEY, output_code=BTN_TL, ) mapping_2 = Mapping( - input_combination=InputCombination(get_combination_config(ev_3)), + input_combination=InputCombination([InputConfig.from_input_event(ev_3)]), target_uinput="keyboard", output_type=EV_KEY, output_code=KEY_A, @@ -792,26 +783,22 @@ async def test_can_not_map(self): preset.add(mapping_1) preset.add(mapping_2) - event_reader = self.get_event_reader(preset, fixtures.foo_device_2_mouse) + event_reader = self.create_event_reader(preset, fixtures.foo_device_2_mouse) # send key-down and up await self.send_events( [ - InputEvent.from_tuple(ev_1), - InputEvent.from_tuple(ev_2), - InputEvent.from_tuple(ev_3), - InputEvent.from_tuple(ev_4), - InputEvent.from_tuple(ev_5), - InputEvent.from_tuple(ev_6), + ev_1, + ev_2, + ev_3, + ev_4, + ev_5, + ev_6, ], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history self.assertEqual(len(forwarded_history), 4) self.assertEqual(len(keyboard_history), 2) @@ -837,7 +824,7 @@ async def test_axis_switch(self): # ABS_X to REL_Y if ABS_Y is above 10% combination = InputCombination( - get_combination_config((EV_ABS, ABS_X, 0), (EV_ABS, ABS_Y, 10)) + InputCombination.from_tuples((EV_ABS, ABS_X, 0), (EV_ABS, ABS_Y, 10)) ) cfg = { "input_combination": combination.to_config(), @@ -851,10 +838,10 @@ async def test_axis_switch(self): m1 = Mapping(**cfg) preset.add(m1) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) # set ABS_X input to 100% - await event_reader.handle(InputEvent.from_tuple((EV_ABS, ABS_X, MAX_ABS))) + await event_reader.handle(InputEvent.abs(ABS_X, MAX_ABS)) # wait a bit more for nothing to sum up, because ABS_Y is still 0 await asyncio.sleep(0.2) @@ -868,9 +855,9 @@ async def test_axis_switch(self): # move ABS_Y above 10% await self.send_events( ( - InputEvent.from_tuple((EV_ABS, ABS_Y, MAX_ABS * 0.05)), - InputEvent.from_tuple((EV_ABS, ABS_Y, MAX_ABS * 0.11)), - InputEvent.from_tuple((EV_ABS, ABS_Y, MAX_ABS * 0.5)), + InputEvent.abs(ABS_Y, int(MAX_ABS * 0.05)), + InputEvent.abs(ABS_Y, int(MAX_ABS * 0.11)), + InputEvent.abs(ABS_Y, int(MAX_ABS * 0.5)), ), event_reader, ) @@ -885,16 +872,14 @@ async def test_axis_switch(self): # send some more x events await self.send_events( ( - InputEvent.from_tuple((EV_ABS, ABS_X, MAX_ABS)), - InputEvent.from_tuple((EV_ABS, ABS_X, MAX_ABS * 0.9)), + InputEvent.abs(ABS_X, MAX_ABS), + InputEvent.abs(ABS_X, int(MAX_ABS * 0.9)), ), event_reader, ) # stop it - await event_reader.handle( - InputEvent.from_tuple((EV_ABS, ABS_Y, MAX_ABS * 0.05)) - ) + await event_reader.handle(InputEvent.abs(ABS_Y, int(MAX_ABS * 0.05))) await asyncio.sleep(0.2) # wait a bit more for nothing to sum up if mouse_history[0].type == EV_ABS: @@ -907,7 +892,7 @@ async def test_axis_switch(self): # does not contain anything else expected_rel_event = (EV_REL, REL_X, int(gain * REL_XY_SCALING)) - count_x = convert_to_internal_events(mouse_history).count(expected_rel_event) + count_x = mouse_history.count(expected_rel_event) self.assertEqual(len(mouse_history), count_x) async def test_key_axis_combination_to_disable(self): @@ -927,7 +912,7 @@ async def test_key_axis_combination_to_disable(self): ) preset.add(mapping) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [ @@ -954,7 +939,7 @@ async def test_abs_to_abs(self): # left x to mouse x input_config = InputConfig(type=EV_ABS, code=ABS_X) mapping_config = { - "input_combination": InputCombination(input_config).to_config(), + "input_combination": InputCombination([input_config]).to_config(), "target_uinput": "gamepad", "output_type": EV_ABS, "output_code": ABS_X, @@ -965,7 +950,9 @@ async def test_abs_to_abs(self): preset = Preset() preset.add(mapping_1) input_config = InputConfig(type=EV_ABS, code=ABS_Y) - mapping_config["input_combination"] = InputCombination(input_config).to_config() + mapping_config["input_combination"] = InputCombination( + [input_config] + ).to_config() mapping_config["output_code"] = ABS_Y mapping_2 = Mapping(**mapping_config) preset.add(mapping_2) @@ -973,21 +960,19 @@ async def test_abs_to_abs(self): x = MAX_ABS y = MAX_ABS - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [ - InputEvent.from_tuple((EV_ABS, ABS_X, -x)), - InputEvent.from_tuple((EV_ABS, ABS_Y, y)), + InputEvent.abs(ABS_X, -x), + InputEvent.abs(ABS_Y, y), ], event_reader, ) await asyncio.sleep(0.2) - # convert the write-history to some easier to manage list - history = convert_to_internal_events( - global_uinputs.get_uinput("gamepad").write_history - ) + + history = global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1020,26 +1005,24 @@ async def test_abs_to_abs_with_input_switch(self): x = MAX_ABS y = MAX_ABS - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [ - InputEvent.from_tuple((EV_ABS, ABS_X, -x // 5)), # will not map - InputEvent.from_tuple((EV_ABS, ABS_X, -x)), # will map later + InputEvent.abs(ABS_X, -x // 5), # will not map + InputEvent.abs(ABS_X, -x), # will map later # switch axis on sends initial position (previous event) - InputEvent.from_tuple((EV_ABS, ABS_Y, y)), - InputEvent.from_tuple((EV_ABS, ABS_X, x)), # normally mapped - InputEvent.from_tuple((EV_ABS, ABS_Y, y // 15)), # off, re-centers axis - InputEvent.from_tuple((EV_ABS, ABS_X, -x // 5)), # will not map + InputEvent.abs(ABS_Y, y), + InputEvent.abs(ABS_X, x), # normally mapped + InputEvent.abs(ABS_Y, y // 15), # off, re-centers axis + InputEvent.abs(ABS_X, -x // 5), # will not map ], event_reader, ) await asyncio.sleep(0.2) - # convert the write-history to some easier to manage list - history = convert_to_internal_events( - global_uinputs.get_uinput("gamepad").write_history - ) + + history = global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1062,7 +1045,7 @@ def next_usec_time(): gain = 0.5 # left mouse x to abs x cutoff = 2 - input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X)) + input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) mapping_config = { "input_combination": input_combination.to_config(), "target_uinput": "gamepad", @@ -1076,13 +1059,13 @@ def next_usec_time(): mapping_1 = Mapping(**mapping_config) preset = Preset() preset.add(mapping_1) - input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_Y)) + input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_Y)]) mapping_config["input_combination"] = input_combination.to_config() mapping_config["output_code"] = ABS_Y mapping_2 = Mapping(**mapping_config) preset.add(mapping_2) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) next_time = next_usec_time() await self.send_events( @@ -1094,10 +1077,8 @@ def next_usec_time(): ) await asyncio.sleep(0.1) - # convert the write-history to some easier to manage list - history = convert_to_internal_events( - global_uinputs.get_uinput("gamepad").write_history - ) + + history = global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1116,9 +1097,7 @@ def next_usec_time(): event_reader, ) await asyncio.sleep(0.7) - history = convert_to_internal_events( - global_uinputs.get_uinput("gamepad").write_history - ) + history = global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1158,7 +1137,7 @@ async def test_rel_to_abs_with_input_switch(self): preset = Preset() preset.add(mapping_1) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) # if the cutoff is higher, the test sends higher values to overcome the cutoff await self.send_events( @@ -1178,10 +1157,8 @@ async def test_rel_to_abs_with_input_switch(self): ) await asyncio.sleep(0.2) - # convert the write-history to some easier to manage list - history = convert_to_internal_events( - global_uinputs.get_uinput("gamepad").write_history - ) + + history = global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1200,7 +1177,7 @@ async def test_abs_to_rel(self): # left x to mouse x input_config = InputConfig(type=EV_ABS, code=ABS_X) mapping_config = { - "input_combination": InputCombination(input_config).to_config(), + "input_combination": InputCombination([input_config]).to_config(), "target_uinput": "mouse", "output_type": EV_REL, "output_code": REL_X, @@ -1213,7 +1190,9 @@ async def test_abs_to_rel(self): preset.add(mapping_1) # left y to mouse y input_config = InputConfig(type=EV_ABS, code=ABS_Y) - mapping_config["input_combination"] = InputCombination(input_config).to_config() + mapping_config["input_combination"] = InputCombination( + [input_config] + ).to_config() mapping_config["output_code"] = REL_Y mapping_2 = Mapping(**mapping_config) preset.add(mapping_2) @@ -1223,12 +1202,12 @@ async def test_abs_to_rel(self): x = MAX_ABS y = MAX_ABS - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [ - InputEvent.from_tuple((EV_ABS, ABS_X, -x)), - InputEvent.from_tuple((EV_ABS, ABS_Y, -y)), + InputEvent.abs(ABS_X, -x), + InputEvent.abs(ABS_Y, -y), ], event_reader, ) @@ -1238,16 +1217,13 @@ async def test_abs_to_rel(self): # stop it await self.send_events( [ - InputEvent.from_tuple((EV_ABS, ABS_X, 0)), - InputEvent.from_tuple((EV_ABS, ABS_Y, 0)), + InputEvent.abs(ABS_X, 0), + InputEvent.abs(ABS_Y, 0), ], event_reader, ) - # convert the write-history to some easier to manage list - mouse_history = convert_to_internal_events( - global_uinputs.get_uinput("mouse").write_history - ) + mouse_history = global_uinputs.get_uinput("mouse").write_history if mouse_history[0].type == EV_ABS: raise AssertionError( @@ -1275,7 +1251,7 @@ async def test_abs_to_wheel_hi_res_quirk(self): # left x to mouse x input_config = InputConfig(type=EV_ABS, code=ABS_X) mapping_config = { - "input_combination": InputCombination(input_config).to_config(), + "input_combination": InputCombination([input_config]).to_config(), "target_uinput": "mouse", "output_type": EV_REL, "output_code": REL_WHEEL, @@ -1289,7 +1265,9 @@ async def test_abs_to_wheel_hi_res_quirk(self): preset.add(mapping_1) # left y to mouse y input_config = InputConfig(type=EV_ABS, code=ABS_Y) - mapping_config["input_combination"] = InputCombination(input_config).to_config() + mapping_config["input_combination"] = InputCombination( + [input_config] + ).to_config() mapping_config["output_code"] = REL_HWHEEL_HI_RES mapping_2 = Mapping(**mapping_config) preset.add(mapping_2) @@ -1299,12 +1277,12 @@ async def test_abs_to_wheel_hi_res_quirk(self): x = MAX_ABS y = MAX_ABS - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [ - InputEvent.from_tuple((EV_ABS, ABS_X, x)), - InputEvent.from_tuple((EV_ABS, ABS_Y, -y)), + InputEvent.abs(ABS_X, x), + InputEvent.abs(ABS_Y, -y), ], event_reader, ) @@ -1314,14 +1292,12 @@ async def test_abs_to_wheel_hi_res_quirk(self): # stop it await self.send_events( [ - InputEvent.from_tuple((EV_ABS, ABS_X, 0)), - InputEvent.from_tuple((EV_ABS, ABS_Y, 0)), + InputEvent.abs(ABS_X, 0), + InputEvent.abs(ABS_Y, 0), ], event_reader, ) - m_history = convert_to_internal_events( - global_uinputs.get_uinput("mouse").write_history - ) + m_history = global_uinputs.get_uinput("mouse").write_history rel_wheel = sum([event.value for event in m_history if event.code == REL_WHEEL]) rel_wheel_hi_res = sum( @@ -1357,11 +1333,11 @@ async def test_rel_to_btn(self): # set a high release timeout to make sure the tests pass release_timeout = 0.2 - mapping_1 = get_key_mapping( - InputCombination(get_combination_config(hw_right)), "keyboard", "k(b)" + mapping_1 = Mapping.from_combination( + InputCombination(InputCombination.from_tuples(hw_right)), "keyboard", "k(b)" ) - mapping_2 = get_key_mapping( - InputCombination(get_combination_config(w_up)), "keyboard", "c" + mapping_2 = Mapping.from_combination( + InputCombination(InputCombination.from_tuples(w_up)), "keyboard", "c" ) mapping_1.release_timeout = release_timeout mapping_2.release_timeout = release_timeout @@ -1370,7 +1346,7 @@ async def test_rel_to_btn(self): preset.add(mapping_1) preset.add(mapping_2) - event_reader = self.get_event_reader(preset, fixtures.foo_device_2_mouse) + event_reader = self.create_event_reader(preset, fixtures.foo_device_2_mouse) await self.send_events( [InputEvent.from_tuple(hw_right), InputEvent.from_tuple(w_up)] * 5, @@ -1387,12 +1363,8 @@ async def test_rel_to_btn(self): # wait more than the release_timeout to make sure all handlers finish await asyncio.sleep(release_timeout * 1.2) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, code_b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, code_c, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, code_b, 0)), 1) @@ -1408,13 +1380,17 @@ async def test_rel_trigger_threshold(self): """Test that different activation points for rel_to_btn work correctly.""" # at 5 map to a - mapping_1 = get_key_mapping( - InputCombination(InputConfig(type=EV_REL, code=REL_X, analog_threshold=5)), + mapping_1 = Mapping.from_combination( + InputCombination( + [InputConfig(type=EV_REL, code=REL_X, analog_threshold=5)] + ), output_symbol="a", ) # at 15 map to b - mapping_2 = get_key_mapping( - InputCombination(InputConfig(type=EV_REL, code=REL_X, analog_threshold=15)), + mapping_2 = Mapping.from_combination( + InputCombination( + [InputConfig(type=EV_REL, code=REL_X, analog_threshold=15)] + ), output_symbol="b", ) release_timeout = 0.2 # give some time to do assertions before the release @@ -1427,21 +1403,19 @@ async def test_rel_trigger_threshold(self): a = system_mapping.get("a") b = system_mapping.get("b") - event_reader = self.get_event_reader(preset, fixtures.foo_device_2_mouse) + event_reader = self.create_event_reader(preset, fixtures.foo_device_2_mouse) await self.send_events( [ - InputEvent.from_tuple((EV_REL, REL_X, -5)), # forward - InputEvent.from_tuple((EV_REL, REL_X, 0)), # forward - InputEvent.from_tuple((EV_REL, REL_X, 3)), # forward - InputEvent.from_tuple((EV_REL, REL_X, 10)), # trigger a + InputEvent.rel(REL_X, -5), # forward + InputEvent.rel(REL_X, 0), # forward + InputEvent.rel(REL_X, 3), # forward + InputEvent.rel(REL_X, 10), # trigger a ], event_reader, ) await asyncio.sleep(release_timeout * 1.5) # release a - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history, [(EV_KEY, a, 1), (EV_KEY, a, 0)]) self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) @@ -1450,27 +1424,21 @@ async def test_rel_trigger_threshold(self): await self.send_events( [ - InputEvent.from_tuple((EV_REL, REL_X, 10)), # trigger a - InputEvent.from_tuple((EV_REL, REL_X, 20)), # trigger b - InputEvent.from_tuple((EV_REL, REL_X, 10)), # release b + InputEvent.rel(REL_X, 10), # trigger a + InputEvent.rel(REL_X, 20), # trigger b + InputEvent.rel(REL_X, 10), # release b ], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 2) self.assertEqual(keyboard_history.count((EV_KEY, b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 0)), 1) self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 1) await asyncio.sleep(release_timeout * 1.5) # release a - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 2) self.assertEqual( forwarded_history, @@ -1483,13 +1451,17 @@ async def test_abs_trigger_threshold(self): """Test that different activation points for abs_to_btn work correctly.""" # at 30% map to a - mapping_1 = get_key_mapping( - InputCombination(InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=30)), + mapping_1 = Mapping.from_combination( + InputCombination( + [InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=30)] + ), output_symbol="a", ) # at 70% map to b - mapping_2 = get_key_mapping( - InputCombination(InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=70)), + mapping_2 = Mapping.from_combination( + InputCombination( + [InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=70)] + ), output_symbol="b", ) preset = Preset() @@ -1499,24 +1471,22 @@ async def test_abs_trigger_threshold(self): a = system_mapping.get("a") b = system_mapping.get("b") - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [ # -10%, do nothing - InputEvent.from_tuple((EV_ABS, ABS_X, MIN_ABS // 10)), + InputEvent.abs(ABS_X, MIN_ABS // 10), # 0%, do noting - InputEvent.from_tuple((EV_ABS, ABS_X, 0)), + InputEvent.abs(ABS_X, 0), # 10%, do nothing - InputEvent.from_tuple((EV_ABS, ABS_X, MAX_ABS // 10)), + InputEvent.abs(ABS_X, MAX_ABS // 10), # 50%, trigger a - InputEvent.from_tuple((EV_ABS, ABS_X, MAX_ABS // 2)), + InputEvent.abs(ABS_X, MAX_ABS // 2), ], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) self.assertNotIn((EV_KEY, a, 0), keyboard_history) @@ -1525,27 +1495,21 @@ async def test_abs_trigger_threshold(self): await self.send_events( [ # 80%, trigger b - InputEvent.from_tuple((EV_ABS, ABS_X, int(MAX_ABS * 0.8))), - InputEvent.from_tuple((EV_ABS, ABS_X, MAX_ABS // 2)), # 50%, release b + InputEvent.abs(ABS_X, int(MAX_ABS * 0.8)), + InputEvent.abs(ABS_X, MAX_ABS // 2), # 50%, release b ], event_reader, ) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 0)), 1) self.assertNotIn((EV_KEY, a, 0), keyboard_history) # 0% release a - await event_reader.handle(InputEvent.from_tuple((EV_ABS, ABS_X, 0))) - keyboard_history = convert_to_internal_events( - global_uinputs.get_uinput("keyboard").write_history - ) - forwarded_history = convert_to_internal_events( - self.forward_uinput.write_history - ) + await event_reader.handle(InputEvent.abs(ABS_X, 0)) + keyboard_history = global_uinputs.get_uinput("keyboard").write_history + forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 1) self.assertEqual(len(forwarded_history), 0) @@ -1556,7 +1520,7 @@ async def _test(self, input_code, input_value, output_code, output_value, gain=1 input_config = InputConfig(type=EV_REL, code=input_code) mapping = Mapping( - input_combination=InputCombination(input_config).to_config(), + input_combination=InputCombination([input_config]).to_config(), target_uinput="mouse", output_type=EV_REL, output_code=output_code, @@ -1565,17 +1529,14 @@ async def _test(self, input_code, input_value, output_code, output_value, gain=1 ) preset.add(mapping) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [InputEvent(0, 0, EV_REL, input_code, input_value)], event_reader, ) - # convert the write-history to some easier to manage list - history = convert_to_internal_events( - global_uinputs.get_uinput("mouse").write_history - ) + history = global_uinputs.get_uinput("mouse").write_history self.assertEqual(len(history), 1) self.assertEqual( @@ -1616,7 +1577,7 @@ async def test_x_to_hwheel(self): input_config = InputConfig(type=EV_REL, code=input_code) mapping = Mapping( - input_combination=InputCombination(input_config).to_config(), + input_combination=InputCombination([input_config]).to_config(), target_uinput="mouse", output_type=EV_REL, output_code=output_code, @@ -1625,7 +1586,7 @@ async def test_x_to_hwheel(self): ) preset.add(mapping) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( [InputEvent(0, 0, EV_REL, input_code, input_value)], @@ -1665,7 +1626,7 @@ async def test_remainder(self): input_config = InputConfig(type=EV_REL, code=REL_WHEEL_HI_RES) gain = 0.01 mapping = Mapping( - input_combination=InputCombination(input_config).to_config(), + input_combination=InputCombination([input_config]).to_config(), target_uinput="mouse", output_type=EV_REL, output_code=REL_Y, @@ -1674,7 +1635,7 @@ async def test_remainder(self): ) preset.add(mapping) - event_reader = self.get_event_reader(preset, fixtures.gamepad) + event_reader = self.create_event_reader(preset, fixtures.gamepad) events_until_one_rel_y_written = int( WHEEL_HI_RES_SCALING / REL_XY_SCALING / gain diff --git a/tests/unit/test_event_pipeline/test_mapping_handlers.py b/tests/unit/test_event_pipeline/test_mapping_handlers.py index 8744af066..8855c6af7 100644 --- a/tests/unit/test_event_pipeline/test_mapping_handlers.py +++ b/tests/unit/test_event_pipeline/test_mapping_handlers.py @@ -39,6 +39,7 @@ REL_Y, REL_WHEEL, ) + from inputremapper.injection.mapping_handlers.combination_handler import ( CombinationHandler, ) @@ -61,10 +62,12 @@ from inputremapper.injection.mapping_handlers.mapping_handler import MappingHandler from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler from inputremapper.input_event import InputEvent, EventActions + from tests.lib.cleanup import cleanup +from tests.lib.logger import logger from tests.lib.patches import InputDevice from tests.lib.constants import MAX_ABS -from tests.lib.stuff import convert_to_internal_events +from tests.lib.fixtures import fixtures class BaseTests: @@ -103,13 +106,14 @@ def setUp(self): output_type=2, output_code=1, ), + MagicMock(), ) class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( - InputConfig(type=3, code=5, analog_threshold=10) + [InputConfig(type=3, code=5, analog_threshold=10)] ) self.handler = AbsToBtnHandler( input_combination, @@ -123,7 +127,7 @@ def setUp(self): class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): - input_combination = InputCombination(InputConfig(type=EV_ABS, code=ABS_X)) + input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) self.handler = AbsToAbsHandler( input_combination, Mapping( @@ -138,7 +142,6 @@ 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(), ) self.handler.reset() history = global_uinputs.get_uinput("gamepad").write_history @@ -150,7 +153,7 @@ async def test_reset(self): class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): - input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X)) + input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) self.handler = RelToAbsHandler( input_combination, Mapping( @@ -165,7 +168,6 @@ async def test_reset(self): self.handler.notify( InputEvent(0, 0, EV_REL, REL_X, 123), source=InputDevice("/dev/input/event15"), - forward=evdev.UInput(), ) self.handler.reset() history = global_uinputs.get_uinput("gamepad").write_history @@ -186,13 +188,11 @@ async def test_rate_changes(self): self.handler.notify( InputEvent(0, delta, EV_REL, REL_X, 100), source=InputDevice("/dev/input/event15"), - forward=evdev.UInput(), ) self.handler.notify( InputEvent(0, delta * 2, EV_REL, REL_X, 100), source=InputDevice("/dev/input/event15"), - forward=evdev.UInput(), ) self.assertEqual(self.handler._observed_rate, expected_rate) @@ -204,13 +204,11 @@ async def test_rate_stays(self): self.handler.notify( InputEvent(0, 50, EV_REL, REL_X, 100), source=InputDevice("/dev/input/event15"), - forward=evdev.UInput(), ) self.handler.notify( InputEvent(0, 50, EV_REL, REL_X, 100), source=InputDevice("/dev/input/event15"), - forward=evdev.UInput(), ) self.assertEqual(self.handler._observed_rate, DEFAULT_REL_RATE) @@ -218,7 +216,7 @@ async def test_rate_stays(self): class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): - input_combination = InputCombination(InputConfig(type=EV_ABS, code=ABS_X)) + input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) self.handler = AbsToRelHandler( input_combination, Mapping( @@ -233,7 +231,6 @@ 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() @@ -249,12 +246,40 @@ class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase): handler: CombinationHandler def setUp(self): + mouse = fixtures.foo_device_2_mouse + self.mouse_hash = mouse.get_device_hash() + + keyboard = fixtures.foo_device_2_keyboard + self.keyboard_hash = keyboard.get_device_hash() + + gamepad = fixtures.gamepad + self.gamepad_hash = gamepad.get_device_hash() + input_combination = InputCombination( ( - InputConfig(type=2, code=0, analog_threshold=10), - InputConfig(type=1, code=3), + InputConfig( + type=EV_REL, + code=5, + analog_threshold=10, + origin_hash=self.mouse_hash, + ), + InputConfig( + type=EV_KEY, + code=3, + origin_hash=self.keyboard_hash, + ), + InputConfig( + type=EV_KEY, + code=4, + origin_hash=self.gamepad_hash, + ), ) ) + + self.input_combination = input_combination + + self.context_mock = MagicMock() + self.handler = CombinationHandler( input_combination, Mapping( @@ -262,8 +287,100 @@ def setUp(self): target_uinput="mouse", output_symbol="BTN_LEFT", ), + self.context_mock, ) + def test_forward_correctly(self): + # In the past, if a mapping has inputs from two different sub devices, it + # always failed to send the release events to the correct one. + # Nowadays, self._context.get_forward_uinput(origin_hash) is used to + # release them correctly. + mock = MagicMock() + self.handler.set_sub_handler(mock) + + # insert our own test-uinput to see what is being written to it + uinputs = { + self.mouse_hash: evdev.UInput(), + self.keyboard_hash: evdev.UInput(), + self.gamepad_hash: evdev.UInput(), + } + self.context_mock.get_forward_uinput = lambda origin_hash: uinputs[origin_hash] + + # 1. trigger the combination + self.handler.notify( + InputEvent.rel( + code=self.input_combination[0].code, + value=1, + origin_hash=self.input_combination[0].origin_hash, + ), + source=fixtures.foo_device_2_mouse, + ) + self.handler.notify( + InputEvent.key( + code=self.input_combination[1].code, + value=1, + origin_hash=self.input_combination[1].origin_hash, + ), + source=fixtures.foo_device_2_keyboard, + ) + self.handler.notify( + InputEvent.key( + code=self.input_combination[2].code, + value=1, + origin_hash=self.input_combination[2].origin_hash, + ), + source=fixtures.gamepad, + ) + + # 2. expect release events to be written to the correct devices, as indicated + # by the origin_hash of the InputConfigs + self.assertListEqual( + uinputs[self.mouse_hash].write_history, + [InputEvent.rel(self.input_combination[0].code, 0)], + ) + self.assertListEqual( + uinputs[self.keyboard_hash].write_history, + [InputEvent.key(self.input_combination[1].code, 0)], + ) + self.assertListEqual( + uinputs[self.gamepad_hash].write_history, + [InputEvent.key(self.input_combination[2].code, 0)], + ) + + def test_no_forwards(self): + # if a combination is not triggered, nothing is released + mock = MagicMock() + self.handler.set_sub_handler(mock) + + # insert our own test-uinput to see what is being written to it + uinputs = { + self.mouse_hash: evdev.UInput(), + self.keyboard_hash: evdev.UInput(), + } + self.context_mock.get_forward_uinput = lambda origin_hash: uinputs[origin_hash] + + # 1. inject any two events + self.handler.notify( + InputEvent.rel( + code=self.input_combination[0].code, + value=1, + origin_hash=self.input_combination[0].origin_hash, + ), + source=fixtures.foo_device_2_mouse, + ) + self.handler.notify( + InputEvent.key( + code=self.input_combination[1].code, + value=1, + origin_hash=self.input_combination[1].origin_hash, + ), + source=fixtures.foo_device_2_keyboard, + ) + + # 2. expect no release events to be written + self.assertListEqual(uinputs[self.mouse_hash].write_history, []) + self.assertListEqual(uinputs[self.keyboard_hash].write_history, []) + class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): @@ -303,19 +420,14 @@ 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))) + history = global_uinputs.get_uinput("mouse").write_history + self.assertEqual(history[0], InputEvent.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))) + history = global_uinputs.get_uinput("mouse").write_history + self.assertEqual(history[1], InputEvent.key(BTN_LEFT, 0)) self.assertEqual(len(history), 2) @@ -342,31 +454,26 @@ 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) + history = global_uinputs.get_uinput("mouse").write_history + self.assertIn(InputEvent.key(BTN_LEFT, 1), history) + self.assertIn(InputEvent.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:]) + history = global_uinputs.get_uinput("mouse").write_history + self.assertIn(InputEvent.key(BTN_LEFT, 0), history[-2:]) + self.assertIn(InputEvent.key(BTN_RIGHT, 0), history[-2:]) self.assertEqual(len(history), 4) class TestRelToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( - InputConfig(type=2, code=0, analog_threshold=10) + [InputConfig(type=2, code=0, analog_threshold=10)] ) self.handler = RelToBtnHandler( input_combination, @@ -382,7 +489,7 @@ class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase): handler: RelToRelHandler def setUp(self): - input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X)) + input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) self.handler = RelToRelHandler( input_combination, Mapping( diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index a5d600b84..fd67cff63 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -43,12 +43,9 @@ from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader from inputremapper.injection.global_uinputs import global_uinputs -from tests.lib.fixtures import ( - new_event, - get_combination_config, - get_key_mapping, - fixtures, -) +from inputremapper.input_event import InputEvent +from inputremapper.utils import get_device_hash +from tests.lib.fixtures import fixtures from tests.lib.cleanup import quick_cleanup @@ -58,15 +55,17 @@ def setUp(self): self.stop_event = asyncio.Event() self.preset = Preset() + global_uinputs.is_service = True + global_uinputs.prepare_all() + def tearDown(self): quick_cleanup() 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 = Context(mapping, {}, {}) context.uinput = evdev.UInput() - event_reader = EventReader(context, source, forward_to, self.stop_event) + event_reader = EventReader(context, source, self.stop_event) asyncio.ensure_future(event_reader.run()) await asyncio.sleep(0.1) return context, event_reader @@ -80,27 +79,31 @@ async def test_if_single_joystick_then(self): trigger = evdev.ecodes.BTN_A self.preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig( - type=EV_KEY, - code=trigger, - origin_hash=fixtures.gamepad.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=trigger, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ] ), "keyboard", "if_single(key(a), key(KEY_LEFTSHIFT))", ) ) self.preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig( - type=EV_ABS, - code=ABS_Y, - analog_threshold=1, - origin_hash=fixtures.gamepad.get_device_hash(), - ) + [ + InputConfig( + type=EV_ABS, + code=ABS_Y, + analog_threshold=1, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ] ), "keyboard", "b", @@ -108,86 +111,111 @@ async def test_if_single_joystick_then(self): ) # left x to mouse x - cfg = { - "input_combination": InputConfig( - type=EV_ABS, code=ABS_X, origin_hash=fixtures.gamepad.get_device_hash() - ), + config = { + "input_combination": [ + InputConfig( + type=EV_ABS, + code=ABS_X, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ], "target_uinput": "mouse", "output_type": EV_REL, "output_code": REL_X, } - self.preset.add(Mapping(**cfg)) + self.preset.add(Mapping(**config)) # left y to mouse y - cfg["input_combination"] = InputConfig( - type=EV_ABS, code=ABS_Y, origin_hash=fixtures.gamepad.get_device_hash() - ) - cfg["output_code"] = REL_Y - self.preset.add(Mapping(**cfg)) + config["input_combination"] = [ + InputConfig( + type=EV_ABS, + code=ABS_Y, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ] + config["output_code"] = REL_Y + self.preset.add(Mapping(**config)) # right x to wheel x - cfg["input_combination"] = InputConfig( - type=EV_ABS, code=ABS_RX, origin_hash=fixtures.gamepad.get_device_hash() - ) - cfg["output_code"] = REL_HWHEEL_HI_RES - self.preset.add(Mapping(**cfg)) + config["input_combination"] = [ + InputConfig( + type=EV_ABS, + code=ABS_RX, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ] + config["output_code"] = REL_HWHEEL_HI_RES + self.preset.add(Mapping(**config)) # right y to wheel y - cfg["input_combination"] = InputConfig( - type=EV_ABS, code=ABS_RY, origin_hash=fixtures.gamepad.get_device_hash() - ) - cfg["output_code"] = REL_WHEEL_HI_RES - self.preset.add(Mapping(**cfg)) + config["input_combination"] = [ + InputConfig( + type=EV_ABS, + code=ABS_RY, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ] + config["output_code"] = REL_WHEEL_HI_RES + self.preset.add(Mapping(**config)) context, _ = await self.setup(self.gamepad_source, self.preset) + gamepad_hash = get_device_hash(self.gamepad_source) self.gamepad_source.push_events( [ - new_event(EV_KEY, trigger, 1), # start the macro - new_event(EV_ABS, ABS_Y, 10), # ignored - new_event(EV_KEY, evdev.ecodes.BTN_B, 2), # ignored - new_event(EV_KEY, evdev.ecodes.BTN_B, 0), # ignored - # stop it, the only way to trigger `then` - new_event(EV_KEY, trigger, 0), + InputEvent.key(evdev.ecodes.BTN_Y, 0, gamepad_hash), # start the macro + InputEvent.key(trigger, 1, gamepad_hash), # start the macro + InputEvent.abs(ABS_Y, 10, gamepad_hash), # ignored + InputEvent.key(evdev.ecodes.BTN_B, 2, gamepad_hash), # ignored + InputEvent.key(evdev.ecodes.BTN_B, 0, gamepad_hash), # ignored + # release the trigger, which runs `then` of if_single + InputEvent.key(trigger, 0, gamepad_hash), ] ) 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] + + history = global_uinputs.get_uinput("keyboard").write_history self.assertIn((EV_KEY, code_a, 1), history) self.assertIn((EV_KEY, code_a, 0), history) self.assertNotIn((EV_KEY, code_shift, 1), history) self.assertNotIn((EV_KEY, code_shift, 0), history) + # after if_single takes an action, the listener should have been removed + self.assertSetEqual(context.listeners, set()) + async def test_if_single_joystick_under_threshold(self): """Triggers then because the joystick events value is too low.""" # TODO: Move this somewhere more sensible code_a = system_mapping.get("a") trigger = evdev.ecodes.BTN_A self.preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig( - type=EV_KEY, - code=trigger, - origin_hash=fixtures.gamepad.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=trigger, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ] ), "keyboard", "if_single(k(a), k(KEY_LEFTSHIFT))", ) ) self.preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig( - type=EV_ABS, - code=ABS_Y, - analog_threshold=1, - origin_hash=fixtures.gamepad.get_device_hash(), - ) + [ + InputConfig( + type=EV_ABS, + code=ABS_Y, + analog_threshold=1, + origin_hash=fixtures.gamepad.get_device_hash(), + ) + ] ), "keyboard", "b", @@ -200,14 +228,14 @@ async def test_if_single_joystick_under_threshold(self): self.gamepad_source.push_events( [ - new_event(EV_KEY, trigger, 1), # start the macro - new_event(EV_ABS, ABS_Y, 1), # ignored because value too low - new_event(EV_KEY, trigger, 0), # stop, only way to trigger `then` + InputEvent.key(trigger, 1), # start the macro + InputEvent.abs(ABS_Y, 1), # ignored because value too low + InputEvent.key(trigger, 0), # stop, only way to trigger `then` ] ) await asyncio.sleep(0.1) self.assertEqual(len(context.listeners), 0) - history = [a.t for a in global_uinputs.get_uinput("keyboard").write_history] + history = global_uinputs.get_uinput("keyboard").write_history # the key that triggered if_single should be injected after # if_single had a chance to inject keys (if the macro is fast enough), diff --git a/tests/unit/test_global_uinputs.py b/tests/unit/test_global_uinputs.py index 8af872f2b..2fc0ad2dc 100644 --- a/tests/unit/test_global_uinputs.py +++ b/tests/unit/test_global_uinputs.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.input_event import InputEvent from tests.lib.cleanup import cleanup import sys @@ -71,19 +70,19 @@ def test_write(self): implicitly tests get_uinput and UInput.can_emit """ - ev_1 = (EV_KEY, KEY_A, 1) - ev_2 = (EV_ABS, ABS_X, 10) + ev_1 = InputEvent.key(KEY_A, 1) + ev_2 = InputEvent.abs(ABS_X, 10) keyboard = global_uinputs.get_uinput("keyboard") - global_uinputs.write(ev_1, "keyboard") + global_uinputs.write(ev_1.event_tuple, "keyboard") self.assertEqual(keyboard.write_count, 1) with self.assertRaises(EventNotHandled): - global_uinputs.write(ev_2, "keyboard") + global_uinputs.write(ev_2.event_tuple, "keyboard") with self.assertRaises(UinputNotAvailable): - global_uinputs.write(ev_1, "foo") + global_uinputs.write(ev_1.event_tuple, "foo") def test_creates_frontend_uinputs(self): frontend_uinputs = GlobalUInputs() diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index d55f5182a..cb6fdd47c 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -17,19 +17,21 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + from pydantic import ValidationError -from tests.lib.fixtures import new_event +from inputremapper.input_event import InputEvent +from tests.lib.global_uinputs import ( + reset_global_uinputs_for_service, + reset_global_uinputs_for_gui, +) from tests.lib.patches import uinputs from tests.lib.cleanup import quick_cleanup from tests.lib.constants import EVENT_READ_TIMEOUT -from tests.lib.fixtures import fixtures, get_combination_config +from tests.lib.fixtures import fixtures from tests.lib.pipes import uinput_write_history_pipe from tests.lib.pipes import read_write_history_pipe, push_events -from tests.lib.fixtures import ( - keyboard_keys, - get_key_mapping, -) +from tests.lib.fixtures import keyboard_keys import unittest from unittest import mock @@ -61,6 +63,7 @@ DISABLE_NAME, ) from inputremapper.configs.preset import Preset +from inputremapper.configs.mapping import Mapping from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.injection.macros.parse import parse from inputremapper.injection.context import Context @@ -117,8 +120,8 @@ def test_grab(self): path = "/dev/input/event10" preset = Preset() preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=10)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=10)]), "keyboard", "a", ) @@ -127,7 +130,7 @@ def test_grab(self): self.injector = Injector(groups.find(key="Foo Device 2"), preset) # this test needs to pass around all other constraints of # _grab_device - self.injector.context = Context(preset) + self.injector.context = Context(preset, {}, {}) device = self.injector._grab_device(evdev.InputDevice(path)) gamepad = classify(device) == DeviceType.GAMEPAD self.assertFalse(gamepad) @@ -139,8 +142,8 @@ def test_fail_grab(self): self.make_it_fail = 999 preset = Preset() preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=10)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=10)]), "keyboard", "a", ) @@ -148,7 +151,7 @@ def test_fail_grab(self): self.injector = Injector(groups.find(key="Foo Device 2"), preset) path = "/dev/input/event10" - self.injector.context = Context(preset) + self.injector.context = Context(preset, {}, {}) device = self.injector._grab_device(evdev.InputDevice(path)) self.assertIsNone(device) self.assertGreaterEqual(self.failed, 1) @@ -163,18 +166,27 @@ def test_fail_grab(self): self.assertEqual(self.injector.get_state(), InjectorState.NO_GRAB) def test_grab_device_1(self): + device_hash = fixtures.gamepad.get_device_hash() + preset = Preset() preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=1) + [ + InputConfig( + type=EV_ABS, + code=ABS_HAT0X, + analog_threshold=1, + origin_hash=device_hash, + ) + ] ), "keyboard", "a", ), ) self.initialize_injector(groups.find(name="gamepad"), preset) - self.injector.context = Context(preset) + self.injector.context = Context(preset, {}, {}) self.injector.group.paths = [ "/dev/input/event10", "/dev/input/event30", @@ -183,41 +195,45 @@ def test_grab_device_1(self): grabbed = self.injector._grab_devices() self.assertEqual(len(grabbed), 1) - self.assertEqual(grabbed[0].path, "/dev/input/event30") + self.assertEqual(grabbed[device_hash].path, "/dev/input/event30") def test_forward_gamepad_events(self): + device_hash = fixtures.gamepad.get_device_hash() + # forward abs joystick events preset = Preset() preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=BTN_A)), - "keyboard", - "a", + Mapping.from_combination( + input_combination=InputCombination( + [InputConfig(type=EV_KEY, code=BTN_A, origin_hash=device_hash)] + ), + target_uinput="keyboard", + output_symbol="a", ), ) self.initialize_injector(groups.find(name="gamepad"), preset) - self.injector.context = Context(preset) + self.injector.context = Context(preset, {}, {}) path = "/dev/input/event30" devices = self.injector._grab_devices() self.assertEqual(len(devices), 1) - self.assertEqual(devices[0].path, path) - gamepad = classify(devices[0]) == DeviceType.GAMEPAD + self.assertEqual(devices[device_hash].path, path) + gamepad = classify(devices[device_hash]) == DeviceType.GAMEPAD self.assertTrue(gamepad) def test_skip_unused_device(self): # skips a device because its capabilities are not used in the preset preset = Preset() preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=10)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=10)]), "keyboard", "a", ) ) self.initialize_injector(groups.find(key="Foo Device 2"), preset) - self.injector.context = Context(preset) + self.injector.context = Context(preset, {}, {}) # grabs only one device even though the group has 4 devices devices = self.injector._grab_devices() @@ -227,8 +243,8 @@ def test_skip_unused_device(self): def test_skip_unknown_device(self): preset = Preset() preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=1234)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=1234)]), "keyboard", "a", ) @@ -236,12 +252,12 @@ def test_skip_unknown_device(self): # skips a device because its capabilities are not used in the preset self.initialize_injector(groups.find(key="Foo Device 2"), preset) - self.injector.context = Context(preset) + self.injector.context = Context(preset, {}, {}) devices = self.injector._grab_devices() # skips the device alltogether, so no grab attempts fail self.assertEqual(self.failed, 0) - self.assertEqual(devices, []) + self.assertEqual(devices, {}) def test_get_udev_name(self): self.injector = Injector(groups.find(key="Foo Device 2"), Preset()) @@ -260,25 +276,29 @@ def test_get_udev_name(self): @mock.patch("evdev.InputDevice.ungrab") def test_capabilities_and_uinput_presence(self, ungrab_patch): preset = Preset() - m1 = get_key_mapping( + m1 = Mapping.from_combination( InputCombination( - InputConfig( - type=EV_KEY, - code=KEY_A, - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=KEY_A, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ] ), "keyboard", "c", ) - m2 = get_key_mapping( + m2 = Mapping.from_combination( InputCombination( - InputConfig( - type=EV_REL, - code=REL_HWHEEL, - analog_threshold=1, - origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), - ) + [ + InputConfig( + type=EV_REL, + code=REL_HWHEEL, + analog_threshold=1, + origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), + ) + ] ), "keyboard", "key(b)", @@ -292,11 +312,13 @@ def test_capabilities_and_uinput_presence(self, ungrab_patch): self.assertEqual( self.injector.preset.get_mapping( InputCombination( - InputConfig( - type=EV_KEY, - code=KEY_A, - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=KEY_A, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ] ) ), m1, @@ -304,29 +326,21 @@ def test_capabilities_and_uinput_presence(self, ungrab_patch): self.assertEqual( self.injector.preset.get_mapping( InputCombination( - InputConfig( - type=EV_REL, - code=REL_HWHEEL, - analog_threshold=1, - origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), - ) + [ + InputConfig( + type=EV_REL, + code=REL_HWHEEL, + analog_threshold=1, + origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), + ) + ] ) ), m2, ) - self.assertListEqual( - sorted(uinputs.keys()), - sorted( - [ - # reading and preventing original events from reaching the - # display server - "input-remapper Foo Device foo forwarded", - "input-remapper Foo Device forwarded", - ] - ), - ) - + # reading and preventing original events from reaching the + # display server forwarded_foo = uinputs.get("input-remapper Foo Device foo forwarded") forwarded = uinputs.get("input-remapper Foo Device forwarded") self.assertIsNotNone(forwarded_foo) @@ -353,9 +367,9 @@ def test_injector(self): preset = Preset() preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - ( + [ InputConfig( type=EV_KEY, code=8, @@ -366,21 +380,23 @@ def test_injector(self): code=9, origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), ), - ) + ] ), "keyboard", "k(KEY_Q).k(w)", ) ) preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig( - type=EV_ABS, - code=ABS_HAT0X, - analog_threshold=-1, - origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), - ) + [ + InputConfig( + type=EV_ABS, + code=ABS_HAT0X, + analog_threshold=-1, + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), + ) + ] ), "keyboard", "a", @@ -390,13 +406,15 @@ def test_injector(self): input_b = 10 with self.assertRaises(ValidationError): preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig( - type=EV_KEY, - code=input_b, - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=input_b, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ] ), "keyboard", "b", @@ -416,10 +434,10 @@ def test_injector(self): fixtures.foo_device_2_keyboard, [ # should execute a macro... - new_event(EV_KEY, 8, 1), # forwarded - new_event(EV_KEY, 9, 1), # triggers macro - new_event(EV_KEY, 8, 0), # releases macro - new_event(EV_KEY, 9, 0), # forwarded + InputEvent.key(8, 1), # forwarded + InputEvent.key(9, 1), # triggers macro + InputEvent.key(8, 0), # releases macro + InputEvent.key(9, 0), # forwarded ], ) @@ -428,8 +446,8 @@ def test_injector(self): fixtures.foo_device_2_gamepad, [ # gamepad stuff. trigger a combination - new_event(EV_ABS, ABS_HAT0X, -1), - new_event(EV_ABS, ABS_HAT0X, 0), + InputEvent.abs(ABS_HAT0X, -1), + InputEvent.abs(ABS_HAT0X, 0), ], ) @@ -438,9 +456,9 @@ def test_injector(self): fixtures.foo_device_2_keyboard, [ # just pass those over without modifying - new_event(EV_KEY, 10, 1), - new_event(EV_KEY, 10, 0), - new_event(3124, 3564, 6542), + InputEvent.key(10, 1), + InputEvent.key(10, 0), + InputEvent(0, 0, 3124, 3564, 6542), ], force=True, ) @@ -511,18 +529,18 @@ def test_injector(self): self.assertEqual(self.injector.get_state(), InjectorState.RUNNING) def test_is_in_capabilities(self): - key = InputCombination(get_combination_config((1, 2, 1))) + key = InputCombination(InputCombination.from_tuples((1, 2, 1))) capabilities = {1: [9, 2, 5]} self.assertTrue(is_in_capabilities(key, capabilities)) - key = InputCombination(get_combination_config((1, 2, 1), (1, 3, 1))) + key = InputCombination(InputCombination.from_tuples((1, 2, 1), (1, 3, 1))) capabilities = {1: [9, 2, 5]} # only one of the codes of the combination is required. # The goal is to make combinations= across those sub-devices possible, # that make up one hardware device self.assertTrue(is_in_capabilities(key, capabilities)) - key = InputCombination(get_combination_config((1, 2, 1), (1, 5, 1))) + key = InputCombination(InputCombination.from_tuples((1, 2, 1), (1, 5, 1))) capabilities = {1: [9, 2, 5]} self.assertTrue(is_in_capabilities(key, capabilities)) @@ -571,15 +589,15 @@ def capabilities(self, absinfo=False): preset = Preset() preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=80)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=80)]), "keyboard", "a", ) ) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=81)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=81)]), "keyboard", DISABLE_NAME, ), @@ -589,8 +607,8 @@ def capabilities(self, absinfo=False): macro = parse(macro_code, preset) preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=60)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=60)]), "keyboard", macro_code, ), @@ -599,9 +617,9 @@ def capabilities(self, absinfo=False): # going to be ignored, because EV_REL cannot be mapped, that's # mouse movements. preset.add( - get_key_mapping( + Mapping.from_combination( InputCombination( - InputConfig(type=EV_REL, code=1234, analog_threshold=3) + [InputConfig(type=EV_REL, code=1234, analog_threshold=3)] ), "keyboard", "b", diff --git a/tests/unit/test_input_config.py b/tests/unit/test_input_config.py index 4fbdbb367..608ec9333 100644 --- a/tests/unit/test_input_config.py +++ b/tests/unit/test_input_config.py @@ -44,7 +44,6 @@ ) from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.lib.fixtures import get_combination_config class TestInputConfig(unittest.TestCase): @@ -322,48 +321,99 @@ def test_is_immutable(self): class TestInputCombination(unittest.TestCase): + def test_eq(self): + a = InputCombination( + [ + InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"), + InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"), + ] + ) + b = InputCombination( + [ + InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"), + InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"), + ] + ) + self.assertEqual(a, b) + + def test_not_eq(self): + a = InputCombination( + [ + InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="2345"), + InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="bcde"), + ] + ) + b = InputCombination( + [ + InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"), + InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"), + ] + ) + self.assertNotEqual(a, b) + + def test_can_be_used_as_dict_key(self): + dict_ = { + InputCombination( + [ + InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"), + InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"), + ] + ): "foo" + } + key = InputCombination( + [ + InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"), + InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"), + ] + ) + self.assertEqual(dict_.get(key), "foo") + def test_get_permutations(self): - key_1 = InputCombination(get_combination_config((1, 3, 1))) + key_1 = InputCombination(InputCombination.from_tuples((1, 3, 1))) self.assertEqual(len(key_1.get_permutations()), 1) self.assertEqual(key_1.get_permutations()[0], key_1) - key_2 = InputCombination(get_combination_config((1, 3, 1), (1, 5, 1))) + key_2 = InputCombination(InputCombination.from_tuples((1, 3, 1), (1, 5, 1))) self.assertEqual(len(key_2.get_permutations()), 1) self.assertEqual(key_2.get_permutations()[0], key_2) key_3 = InputCombination( - get_combination_config((1, 3, 1), (1, 5, 1), (1, 7, 1)) + InputCombination.from_tuples((1, 3, 1), (1, 5, 1), (1, 7, 1)) ) self.assertEqual(len(key_3.get_permutations()), 2) self.assertEqual( key_3.get_permutations()[0], - InputCombination(get_combination_config((1, 3, 1), (1, 5, 1), (1, 7, 1))), + InputCombination( + InputCombination.from_tuples((1, 3, 1), (1, 5, 1), (1, 7, 1)) + ), ) self.assertEqual( key_3.get_permutations()[1], - InputCombination(get_combination_config((1, 5, 1), (1, 3, 1), (1, 7, 1))), + InputCombination( + InputCombination.from_tuples((1, 5, 1), (1, 3, 1), (1, 7, 1)) + ), ) def test_is_problematic(self): key_1 = InputCombination( - get_combination_config((1, KEY_LEFTSHIFT, 1), (1, 5, 1)) + InputCombination.from_tuples((1, KEY_LEFTSHIFT, 1), (1, 5, 1)) ) self.assertTrue(key_1.is_problematic()) key_2 = InputCombination( - get_combination_config((1, KEY_RIGHTALT, 1), (1, 5, 1)) + InputCombination.from_tuples((1, KEY_RIGHTALT, 1), (1, 5, 1)) ) self.assertTrue(key_2.is_problematic()) key_3 = InputCombination( - get_combination_config((1, 3, 1), (1, KEY_LEFTCTRL, 1)) + InputCombination.from_tuples((1, 3, 1), (1, KEY_LEFTCTRL, 1)) ) self.assertTrue(key_3.is_problematic()) - key_4 = InputCombination(get_combination_config((1, 3, 1))) + key_4 = InputCombination(InputCombination.from_tuples((1, 3, 1))) self.assertFalse(key_4.is_problematic()) - key_5 = InputCombination(get_combination_config((1, 3, 1), (1, 5, 1))) + key_5 = InputCombination(InputCombination.from_tuples((1, 3, 1), (1, 5, 1))) self.assertFalse(key_5.is_problematic()) def test_init(self): @@ -383,7 +433,7 @@ def test_init(self): InputCombination(({"type": 1, "code": 2}, {"type": 1, "code": 1})) InputCombination(({"type": 1, "code": 2},)) InputCombination(({"type": "1", "code": "2"},)) - InputCombination(InputConfig(type=1, code=2, analog_threshold=3)) + InputCombination([InputConfig(type=1, code=2, analog_threshold=3)]) InputCombination( ( {"type": 1, "code": 2}, @@ -393,7 +443,7 @@ def test_init(self): ) def test_to_config(self): - c1 = InputCombination(InputConfig(type=1, code=2, analog_threshold=3)) + c1 = InputCombination([InputConfig(type=1, code=2, analog_threshold=3)]) c2 = InputCombination( ( InputConfig(type=1, code=2, analog_threshold=3), @@ -410,60 +460,74 @@ def test_to_config(self): def test_beautify(self): # not an integration test, but I have all the selection_label tests here already self.assertEqual( - InputCombination(get_combination_config((EV_KEY, KEY_A, 1))).beautify(), + InputCombination( + InputCombination.from_tuples((EV_KEY, KEY_A, 1)) + ).beautify(), "a", ) self.assertEqual( - InputCombination(get_combination_config((EV_KEY, KEY_A, 1))).beautify(), + InputCombination( + InputCombination.from_tuples((EV_KEY, KEY_A, 1)) + ).beautify(), "a", ) self.assertEqual( InputCombination( - get_combination_config((EV_ABS, ABS_HAT0Y, -1)) + InputCombination.from_tuples((EV_ABS, ABS_HAT0Y, -1)) ).beautify(), "DPad-Y Up", ) self.assertEqual( - InputCombination(get_combination_config((EV_KEY, BTN_A, 1))).beautify(), + InputCombination( + InputCombination.from_tuples((EV_KEY, BTN_A, 1)) + ).beautify(), "Button A", ) self.assertEqual( - InputCombination(get_combination_config((EV_KEY, 1234, 1))).beautify(), + InputCombination( + InputCombination.from_tuples((EV_KEY, 1234, 1)) + ).beautify(), "unknown (1, 1234)", ) self.assertEqual( InputCombination( - get_combination_config((EV_ABS, ABS_HAT0X, -1)) + InputCombination.from_tuples((EV_ABS, ABS_HAT0X, -1)) ).beautify(), "DPad-X Left", ) self.assertEqual( InputCombination( - get_combination_config((EV_ABS, ABS_HAT0Y, -1)) + InputCombination.from_tuples((EV_ABS, ABS_HAT0Y, -1)) ).beautify(), "DPad-Y Up", ) self.assertEqual( - InputCombination(get_combination_config((EV_KEY, BTN_A, 1))).beautify(), + InputCombination( + InputCombination.from_tuples((EV_KEY, BTN_A, 1)) + ).beautify(), "Button A", ) self.assertEqual( - InputCombination(get_combination_config((EV_ABS, ABS_X, 1))).beautify(), + InputCombination( + InputCombination.from_tuples((EV_ABS, ABS_X, 1)) + ).beautify(), "Joystick-X Right", ) self.assertEqual( - InputCombination(get_combination_config((EV_ABS, ABS_RY, 1))).beautify(), + InputCombination( + InputCombination.from_tuples((EV_ABS, ABS_RY, 1)) + ).beautify(), "Joystick-RY Down", ) self.assertEqual( InputCombination( - get_combination_config((EV_REL, REL_HWHEEL, 1)) + InputCombination.from_tuples((EV_REL, REL_HWHEEL, 1)) ).beautify(), "Wheel Right", ) self.assertEqual( InputCombination( - get_combination_config((EV_REL, REL_WHEEL, -1)) + InputCombination.from_tuples((EV_REL, REL_WHEEL, -1)) ).beautify(), "Wheel Down", ) @@ -471,7 +535,7 @@ def test_beautify(self): # combinations self.assertEqual( InputCombination( - get_combination_config( + InputCombination.from_tuples( (EV_KEY, BTN_A, 1), (EV_KEY, BTN_B, 1), (EV_KEY, BTN_C, 1), diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 0d15f85ca..55fe079d0 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -18,14 +18,14 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . - -from tests.lib.tmp import tmp - +import evdev import os import shutil import unittest import logging +from tests.lib.tmp import tmp + from inputremapper.logger import logger, update_verbosity, log_info, ColorfulFormatter from inputremapper.configs.paths import remove @@ -53,19 +53,15 @@ def tearDown(self): path = os.path.join(tmp, "logger-test") remove(path) - def test_key_debug(self): + def test_write(self): + uinput = evdev.UInput(name="foo") path = os.path.join(tmp, "logger-test") add_filehandler(path) - logger.debug_key(((1, 2, 1),), "foo %s bar", 1234) - logger.debug_key(((1, 200, -1), (1, 5, 1)), "foo %s", (1, 2)) + logger.write((evdev.ecodes.EV_KEY, evdev.ecodes.KEY_A, 1), uinput) with open(path, "r") as f: - content = f.read().lower() - self.assertIn( - "foo 1234 bar ·················· ((1, 2, 1))", - content, - ) + content = f.read() self.assertIn( - "foo (1, 2) ···················· ((1, 200, -1), (1, 5, 1))", + f'Writing (1, 30, 1) to "foo"', content, ) diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 965d0406d..cb43f25ad 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -65,7 +65,7 @@ get_macro_argument_names, get_num_parameters, ) -from tests.lib.fixtures import new_event +from inputremapper.input_event import InputEvent from tests.lib.logger import logger from tests.lib.cleanup import quick_cleanup @@ -82,7 +82,7 @@ def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) - self.context = Context(Preset()) + self.context = Context(Preset(), source_devices={}, forward_devices={}) def tearDown(self): self.result = [] @@ -722,17 +722,17 @@ async def test_just_hold(self): macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) - await (asyncio.sleep(0.1)) + await asyncio.sleep(0.1) self.assertTrue(macro.is_holding()) self.assertEqual(len(self.result), 2) - await (asyncio.sleep(0.1)) + await asyncio.sleep(0.1) # doesn't do fancy stuff, is blocking until the release self.assertEqual(len(self.result), 2) """up""" macro.release_trigger() - await (asyncio.sleep(0.05)) + await asyncio.sleep(0.05) self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 4) @@ -745,7 +745,7 @@ async def test_dont_just_hold(self): macro = parse("key(1).hold().key(3)", self.context, DummyMapping) asyncio.ensure_future(macro.run(self.handler)) - await (asyncio.sleep(0.1)) + await asyncio.sleep(0.1) self.assertFalse(macro.is_holding()) # since press_trigger was never called it just does the macro # completely @@ -764,7 +764,7 @@ async def test_hold_down(self): """down""" macro.press_trigger() - await (asyncio.sleep(0.05)) + await asyncio.sleep(0.05) self.assertTrue(macro.is_holding()) asyncio.ensure_future(macro.run(self.handler)) @@ -777,7 +777,7 @@ async def test_hold_down(self): """up""" macro.release_trigger() - await (asyncio.sleep(0.05)) + await asyncio.sleep(0.05) self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 2) @@ -969,7 +969,7 @@ async def test_mouse(self): asyncio.ensure_future(macro_2.run(self.handler)) sleep = 0.1 - await (asyncio.sleep(sleep)) + await asyncio.sleep(sleep) self.assertTrue(macro_1.is_holding()) self.assertTrue(macro_2.is_holding()) macro_1.release_trigger() @@ -1287,9 +1287,9 @@ async def test_if_single(self): x = system_mapping.get("x") y = system_mapping.get("y") - await self.trigger_sequence(macro, new_event(EV_KEY, a, 1)) + await self.trigger_sequence(macro, InputEvent.key(a, 1)) await asyncio.sleep(0.1) - await self.release_sequence(macro, new_event(EV_KEY, a, 0)) + await self.release_sequence(macro, InputEvent.key(a, 0)) # the key that triggered the macro is released await asyncio.sleep(0.1) @@ -1313,7 +1313,7 @@ async def test_if_single_ignores_releases(self): y = system_mapping.get("y") # pressing the macro key - await self.trigger_sequence(macro, new_event(EV_KEY, a, 1)) + await self.trigger_sequence(macro, InputEvent.key(a, 1)) await asyncio.sleep(0.05) # if_single only looks out for newly pressed keys, @@ -1321,13 +1321,13 @@ async def test_if_single_ignores_releases(self): # pressed before if_single. This was decided because it is a lot # less tricky and more fluently to use if you type fast for listener in self.context.listeners: - asyncio.ensure_future(listener(new_event(EV_KEY, b, 0))) + asyncio.ensure_future(listener(InputEvent.key(b, 0))) await asyncio.sleep(0.05) self.assertListEqual(self.result, []) # releasing the actual key triggers if_single await asyncio.sleep(0.05) - await self.release_sequence(macro, new_event(EV_KEY, a, 0)) + await self.release_sequence(macro, InputEvent.key(a, 0)) await asyncio.sleep(0.05) self.assertListEqual(self.result, [(EV_KEY, x, 1), (EV_KEY, x, 0)]) self.assertFalse(macro.running) @@ -1351,11 +1351,11 @@ async def test_if_not_single(self): y = system_mapping.get("y") # press the trigger key - await self.trigger_sequence(macro, new_event(EV_KEY, a, 1)) + await self.trigger_sequence(macro, InputEvent.key(a, 1)) await asyncio.sleep(0.1) # press another key for listener in self.context.listeners: - asyncio.ensure_future(listener(new_event(EV_KEY, b, 1))) + asyncio.ensure_future(listener(InputEvent.key(b, 1))) await asyncio.sleep(0.1) self.assertListEqual(self.result, [(EV_KEY, y, 1), (EV_KEY, y, 0)]) @@ -1371,11 +1371,11 @@ async def test_if_not_single_none(self): x = system_mapping.get("x") # press trigger key - await self.trigger_sequence(macro, new_event(EV_KEY, a, 1)) + await self.trigger_sequence(macro, InputEvent.key(a, 1)) await asyncio.sleep(0.1) # press another key for listener in self.context.listeners: - asyncio.ensure_future(listener(new_event(EV_KEY, b, 1))) + asyncio.ensure_future(listener(InputEvent.key(b, 1))) await asyncio.sleep(0.1) self.assertListEqual(self.result, []) @@ -1392,7 +1392,7 @@ async def test_if_single_times_out(self): a = system_mapping.get("a") y = system_mapping.get("y") - await self.trigger_sequence(macro, new_event(EV_KEY, a, 1)) + await self.trigger_sequence(macro, InputEvent.key(a, 1)) # no timeout yet await asyncio.sleep(0.2) @@ -1413,12 +1413,12 @@ async def test_if_single_ignores_joystick(self): code_a = system_mapping.get("a") trigger = 1 - await self.trigger_sequence(macro, new_event(EV_KEY, trigger, 1)) + await self.trigger_sequence(macro, InputEvent.key(trigger, 1)) await asyncio.sleep(0.1) for listener in self.context.listeners: - asyncio.ensure_future(listener(new_event(EV_ABS, ABS_Y, 10))) + asyncio.ensure_future(listener(InputEvent.abs(ABS_Y, 10))) await asyncio.sleep(0.1) - await self.release_sequence(macro, new_event(EV_KEY, trigger, 0)) + await self.release_sequence(macro, InputEvent.key(trigger, 0)) await asyncio.sleep(0.1) self.assertFalse(macro.running) self.assertListEqual(self.result, [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)]) diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index bab43ce7d..cdd674b98 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -47,7 +47,7 @@ def test_init(self): } m = Mapping(**cfg) self.assertEqual( - m.input_combination, InputCombination(InputConfig(type=1, code=2)) + m.input_combination, InputCombination([InputConfig(type=1, code=2)]) ) self.assertEqual(m.target_uinput, "keyboard") self.assertEqual(m.output_symbol, "a") @@ -65,7 +65,7 @@ def test_init(self): def test_is_wheel_output(self): mapping = Mapping( - input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)), + input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]), target_uinput="keyboard", output_type=EV_REL, output_code=REL_Y, @@ -74,7 +74,7 @@ def test_is_wheel_output(self): self.assertFalse(mapping.is_high_res_wheel_output()) mapping = Mapping( - input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)), + input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]), target_uinput="keyboard", output_type=EV_REL, output_code=REL_WHEEL, @@ -83,7 +83,7 @@ def test_is_wheel_output(self): self.assertFalse(mapping.is_high_res_wheel_output()) mapping = Mapping( - input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)), + input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]), target_uinput="keyboard", output_type=EV_REL, output_code=REL_WHEEL_HI_RES, @@ -421,7 +421,7 @@ def test_get_bus_massage(self): def test_has_input_defined(self): m = UIMapping() self.assertFalse(m.has_input_defined()) - m.input_combination = InputCombination(InputConfig(type=EV_KEY, code=1)) + m.input_combination = InputCombination([InputConfig(type=EV_KEY, code=1)]) self.assertTrue(m.has_input_defined()) diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index 40548c445..b986d9569 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -15,7 +15,6 @@ from tests.lib.cleanup import quick_cleanup from tests.lib.tmp import tmp -from tests.lib.fixtures import get_combination_config import os import unittest @@ -211,33 +210,33 @@ def test_migrate_mappings(self): preset.load() self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=1)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=1)]), target_uinput="keyboard", output_symbol="a", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=2)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=2)]), target_uinput="gamepad", output_symbol="BTN_B", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=3)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=3)]), target_uinput="keyboard", output_symbol="BTN_1\n# Broken mapping:\n# No target can handle all specified keycodes", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=4)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=4)]), target_uinput="keyboard", output_symbol="d", ), @@ -245,12 +244,12 @@ def test_migrate_mappings(self): self.assertEqual( preset.get_mapping( InputCombination( - InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1) + [InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)] ) ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1) + [InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)] ), target_uinput="keyboard", output_symbol="b", @@ -259,14 +258,14 @@ def test_migrate_mappings(self): self.assertEqual( preset.get_mapping( InputCombination( - get_combination_config( + InputCombination.from_tuples( (EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1) ) ), ), UIMapping( input_combination=InputCombination( - get_combination_config( + InputCombination.from_tuples( (EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1) ), ), @@ -275,17 +274,17 @@ def test_migrate_mappings(self): ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=5)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=5)]), target_uinput="foo", output_symbol="e", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=6))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=6)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=6)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=6)]), target_uinput="keyboard", output_symbol="key(a, b)", ), @@ -316,41 +315,41 @@ def test_migrate_otherwise(self): preset.load() self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=1)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=1)]), target_uinput="keyboard", output_symbol="otherwise + otherwise", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=2)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=2)]), target_uinput="keyboard", output_symbol="bar($otherwise)", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=3)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=3)]), target_uinput="keyboard", output_symbol="foo(else=qux)", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=4)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=4)]), target_uinput="foo", output_symbol="qux(otherwise).bar(else=1)", ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))), + preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=5)])), UIMapping( - input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)), + input_combination=InputCombination([InputConfig(type=EV_KEY, code=5)]), target_uinput="keyboard", output_symbol="foo(otherwise1=2qux)", ), @@ -414,10 +413,12 @@ def test_migrate_left_and_right_purpose(self): # 2 mappings for wheel self.assertEqual(len(preset), 4) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_X) + [InputConfig(type=EV_ABS, code=ABS_X)] ), target_uinput="mouse", output_type=EV_REL, @@ -426,10 +427,12 @@ def test_migrate_left_and_right_purpose(self): ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_Y)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_Y) + [InputConfig(type=EV_ABS, code=ABS_Y)] ), target_uinput="mouse", output_type=EV_REL, @@ -438,10 +441,12 @@ def test_migrate_left_and_right_purpose(self): ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_RX)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_RX) + [InputConfig(type=EV_ABS, code=ABS_RX)] ), target_uinput="mouse", output_type=EV_REL, @@ -450,10 +455,12 @@ def test_migrate_left_and_right_purpose(self): ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_RY)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_RY) + [InputConfig(type=EV_ABS, code=ABS_RY)] ), target_uinput="mouse", output_type=EV_REL, @@ -490,10 +497,12 @@ def test_migrate_left_and_right_purpose2(self): # 2 mappings for wheel self.assertEqual(len(preset), 4) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_RX)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_RX) + [InputConfig(type=EV_ABS, code=ABS_RX)] ), target_uinput="mouse", output_type=EV_REL, @@ -502,10 +511,12 @@ def test_migrate_left_and_right_purpose2(self): ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_RY)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_RY) + [InputConfig(type=EV_ABS, code=ABS_RY)] ), target_uinput="mouse", output_type=EV_REL, @@ -514,10 +525,12 @@ def test_migrate_left_and_right_purpose2(self): ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_X) + [InputConfig(type=EV_ABS, code=ABS_X)] ), target_uinput="mouse", output_type=EV_REL, @@ -526,10 +539,12 @@ def test_migrate_left_and_right_purpose2(self): ), ) self.assertEqual( - preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))), + preset.get_mapping( + InputCombination([InputConfig(type=EV_ABS, code=ABS_Y)]) + ), UIMapping( input_combination=InputCombination( - InputConfig(type=EV_ABS, code=ABS_Y) + [InputConfig(type=EV_ABS, code=ABS_Y)] ), target_uinput="mouse", output_type=EV_REL, diff --git a/tests/unit/test_preset.py b/tests/unit/test_preset.py index af94179c3..2ddf0f80b 100644 --- a/tests/unit/test_preset.py +++ b/tests/unit/test_preset.py @@ -30,7 +30,6 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.input_config import InputCombination, InputConfig from tests.lib.cleanup import quick_cleanup -from tests.lib.fixtures import get_key_mapping, get_combination_config class TestPreset(unittest.TestCase): @@ -43,7 +42,7 @@ def tearDown(self): def test_is_mapped_multiple_times(self): combination = InputCombination( - get_combination_config((1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)) + InputCombination.from_tuples((1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)) ) permutations = combination.get_permutations() self.assertEqual(len(permutations), 6) @@ -64,7 +63,7 @@ def test_is_mapped_multiple_times(self): def test_has_unsaved_changes(self): self.preset.path = get_preset_path("foo", "bar2") - self.preset.add(get_key_mapping()) + self.preset.add(Mapping.from_combination()) self.assertTrue(self.preset.has_unsaved_changes()) self.preset.save() self.assertFalse(self.preset.has_unsaved_changes()) @@ -78,7 +77,7 @@ def test_has_unsaved_changes(self): self.preset.load() self.assertEqual( self.preset.get_mapping(InputCombination.empty_combination()), - get_key_mapping(), + Mapping.from_combination(), ) self.assertFalse(self.preset.has_unsaved_changes()) @@ -99,7 +98,7 @@ def test_has_unsaved_changes(self): self.preset.load() self.preset.path = get_preset_path("bar", "foo") - self.preset.remove(get_key_mapping().input_combination) + self.preset.remove(Mapping.from_combination().input_combination) # empty preset and empty file self.assertFalse(self.preset.has_unsaved_changes()) @@ -122,10 +121,14 @@ def test_save_load(self): two = InputConfig(type=EV_KEY, code=11) three = InputConfig(type=EV_KEY, code=12) - self.preset.add(get_key_mapping(InputCombination(one), "keyboard", "1")) - self.preset.add(get_key_mapping(InputCombination(two), "keyboard", "2")) self.preset.add( - get_key_mapping(InputCombination((two, three)), "keyboard", "3"), + Mapping.from_combination(InputCombination([one]), "keyboard", "1") + ) + self.preset.add( + Mapping.from_combination(InputCombination([two]), "keyboard", "2") + ) + self.preset.add( + Mapping.from_combination(InputCombination((two, three)), "keyboard", "3"), ) self.preset.path = get_preset_path("Foo Device", "test") self.preset.save() @@ -140,16 +143,16 @@ def test_save_load(self): self.assertEqual(len(loaded), 3) self.assertRaises(TypeError, loaded.get_mapping, one) self.assertEqual( - loaded.get_mapping(InputCombination(one)), - get_key_mapping(InputCombination(one), "keyboard", "1"), + loaded.get_mapping(InputCombination([one])), + Mapping.from_combination(InputCombination([one]), "keyboard", "1"), ) self.assertEqual( - loaded.get_mapping(InputCombination(two)), - get_key_mapping(InputCombination(two), "keyboard", "2"), + loaded.get_mapping(InputCombination([two])), + Mapping.from_combination(InputCombination([two]), "keyboard", "2"), ) self.assertEqual( - loaded.get_mapping(InputCombination((two, three))), - get_key_mapping(InputCombination((two, three)), "keyboard", "3"), + loaded.get_mapping(InputCombination([two, three])), + Mapping.from_combination(InputCombination([two, three]), "keyboard", "3"), ) # load missing file @@ -157,13 +160,13 @@ def test_save_load(self): self.assertRaises(FileNotFoundError, preset.load) def test_modify_mapping(self): - ev_1 = InputCombination(InputConfig(type=EV_KEY, code=1)) - ev_3 = InputCombination(InputConfig(type=EV_KEY, code=2)) + ev_1 = InputCombination([InputConfig(type=EV_KEY, code=1)]) + ev_3 = InputCombination([InputConfig(type=EV_KEY, code=2)]) # only values between -99 and 99 are allowed as mapping for EV_ABS or EV_REL - ev_4 = InputCombination(InputConfig(type=EV_ABS, code=1, analog_threshold=99)) + ev_4 = InputCombination([InputConfig(type=EV_ABS, code=1, analog_threshold=99)]) # add the first mapping - self.preset.add(get_key_mapping(ev_1, "keyboard", "a")) + self.preset.add(Mapping.from_combination(ev_1, "keyboard", "a")) self.assertTrue(self.preset.has_unsaved_changes()) self.assertEqual(len(self.preset), 1) @@ -174,19 +177,19 @@ def test_modify_mapping(self): self.assertIsNone(self.preset.get_mapping(ev_1)) self.assertEqual( self.preset.get_mapping(ev_3), - get_key_mapping(ev_3, "keyboard", "b"), + Mapping.from_combination(ev_3, "keyboard", "b"), ) self.assertEqual(len(self.preset), 1) # add 4 - self.preset.add(get_key_mapping(ev_4, "keyboard", "c")) + self.preset.add(Mapping.from_combination(ev_4, "keyboard", "c")) self.assertEqual( self.preset.get_mapping(ev_3), - get_key_mapping(ev_3, "keyboard", "b"), + Mapping.from_combination(ev_3, "keyboard", "b"), ) self.assertEqual( self.preset.get_mapping(ev_4), - get_key_mapping(ev_4, "keyboard", "c"), + Mapping.from_combination(ev_4, "keyboard", "c"), ) self.assertEqual(len(self.preset), 2) @@ -195,7 +198,7 @@ def test_modify_mapping(self): mapping.output_symbol = "d" self.assertEqual( self.preset.get_mapping(ev_4), - get_key_mapping(ev_4, "keyboard", "d"), + Mapping.from_combination(ev_4, "keyboard", "d"), ) self.assertEqual(len(self.preset), 2) @@ -206,18 +209,18 @@ def test_modify_mapping(self): self.assertEqual( self.preset.get_mapping(ev_3), - get_key_mapping(ev_3, "keyboard", "b"), + Mapping.from_combination(ev_3, "keyboard", "b"), ) self.assertEqual( self.preset.get_mapping(ev_4), - get_key_mapping(ev_4, "keyboard", "d"), + Mapping.from_combination(ev_4, "keyboard", "d"), ) self.assertEqual(len(self.preset), 2) def test_avoids_redundant_saves(self): with patch.object(self.preset, "has_unsaved_changes", lambda: False): self.preset.path = get_preset_path("foo", "bar2") - self.preset.add(get_key_mapping()) + self.preset.add(Mapping.from_combination()) self.preset.save() with open(get_preset_path("foo", "bar2"), "r") as f: @@ -234,42 +237,42 @@ def test_combinations(self): combi_2 = InputCombination((ev_2, ev_1, ev_3)) combi_3 = InputCombination((ev_1, ev_2, ev_4)) - self.preset.add(get_key_mapping(combi_1, "keyboard", "a")) + self.preset.add(Mapping.from_combination(combi_1, "keyboard", "a")) self.assertEqual( self.preset.get_mapping(combi_1), - get_key_mapping(combi_1, "keyboard", "a"), + Mapping.from_combination(combi_1, "keyboard", "a"), ) self.assertEqual( self.preset.get_mapping(combi_2), - get_key_mapping(combi_1, "keyboard", "a"), + Mapping.from_combination(combi_1, "keyboard", "a"), ) # since combi_1 and combi_2 are equivalent, this raises a KeyError self.assertRaises( KeyError, self.preset.add, - get_key_mapping(combi_2, "keyboard", "b"), + Mapping.from_combination(combi_2, "keyboard", "b"), ) self.assertEqual( self.preset.get_mapping(combi_1), - get_key_mapping(combi_1, "keyboard", "a"), + Mapping.from_combination(combi_1, "keyboard", "a"), ) self.assertEqual( self.preset.get_mapping(combi_2), - get_key_mapping(combi_1, "keyboard", "a"), + Mapping.from_combination(combi_1, "keyboard", "a"), ) - self.preset.add(get_key_mapping(combi_3, "keyboard", "c")) + self.preset.add(Mapping.from_combination(combi_3, "keyboard", "c")) self.assertEqual( self.preset.get_mapping(combi_1), - get_key_mapping(combi_1, "keyboard", "a"), + Mapping.from_combination(combi_1, "keyboard", "a"), ) self.assertEqual( self.preset.get_mapping(combi_2), - get_key_mapping(combi_1, "keyboard", "a"), + Mapping.from_combination(combi_1, "keyboard", "a"), ) self.assertEqual( self.preset.get_mapping(combi_3), - get_key_mapping(combi_3, "keyboard", "c"), + Mapping.from_combination(combi_3, "keyboard", "c"), ) mapping = self.preset.get_mapping(combi_1) @@ -279,69 +282,69 @@ def test_combinations(self): self.assertEqual( self.preset.get_mapping(combi_1), - get_key_mapping(combi_1, "keyboard", "c"), + Mapping.from_combination(combi_1, "keyboard", "c"), ) self.assertEqual( self.preset.get_mapping(combi_2), - get_key_mapping(combi_1, "keyboard", "c"), + Mapping.from_combination(combi_1, "keyboard", "c"), ) self.assertEqual( self.preset.get_mapping(combi_3), - get_key_mapping(combi_3, "keyboard", "c"), + Mapping.from_combination(combi_3, "keyboard", "c"), ) def test_remove(self): # does nothing - ev_1 = InputCombination(InputConfig(type=EV_KEY, code=40)) - ev_2 = InputCombination(InputConfig(type=EV_KEY, code=30)) - ev_3 = InputCombination(InputConfig(type=EV_KEY, code=20)) - ev_4 = InputCombination(InputConfig(type=EV_KEY, code=10)) + ev_1 = InputCombination([InputConfig(type=EV_KEY, code=40)]) + ev_2 = InputCombination([InputConfig(type=EV_KEY, code=30)]) + ev_3 = InputCombination([InputConfig(type=EV_KEY, code=20)]) + ev_4 = InputCombination([InputConfig(type=EV_KEY, code=10)]) self.assertRaises(TypeError, self.preset.remove, (EV_KEY, 10, 1)) self.preset.remove(ev_1) self.assertFalse(self.preset.has_unsaved_changes()) self.assertEqual(len(self.preset), 0) - self.preset.add(get_key_mapping(combination=ev_1)) + self.preset.add(Mapping.from_combination(input_combination=ev_1)) self.assertEqual(len(self.preset), 1) self.preset.remove(ev_1) self.assertEqual(len(self.preset), 0) - self.preset.add(get_key_mapping(ev_4, "keyboard", "KEY_KP1")) + self.preset.add(Mapping.from_combination(ev_4, "keyboard", "KEY_KP1")) self.assertTrue(self.preset.has_unsaved_changes()) - self.preset.add(get_key_mapping(ev_3, "keyboard", "KEY_KP2")) - self.preset.add(get_key_mapping(ev_2, "keyboard", "KEY_KP3")) + self.preset.add(Mapping.from_combination(ev_3, "keyboard", "KEY_KP2")) + self.preset.add(Mapping.from_combination(ev_2, "keyboard", "KEY_KP3")) self.assertEqual(len(self.preset), 3) self.preset.remove(ev_3) self.assertEqual(len(self.preset), 2) self.assertEqual( self.preset.get_mapping(ev_4), - get_key_mapping(ev_4, "keyboard", "KEY_KP1"), + Mapping.from_combination(ev_4, "keyboard", "KEY_KP1"), ) self.assertIsNone(self.preset.get_mapping(ev_3)) self.assertEqual( self.preset.get_mapping(ev_2), - get_key_mapping(ev_2, "keyboard", "KEY_KP3"), + Mapping.from_combination(ev_2, "keyboard", "KEY_KP3"), ) def test_empty(self): self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=10)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=10)]), "keyboard", "1", ), ) self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=11)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=11)]), "keyboard", "2", ), ) self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=12)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=12)]), "keyboard", "3", ), @@ -358,22 +361,22 @@ def test_empty(self): def test_clear(self): self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=10)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=10)]), "keyboard", "1", ), ) self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=11)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=11)]), "keyboard", "2", ), ) self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=12)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=12)]), "keyboard", "3", ), @@ -391,16 +394,16 @@ def test_clear(self): def test_dangerously_mapped_btn_left(self): # btn left is mapped self.preset.add( - get_key_mapping( - InputCombination(InputConfig.btn_left()), + Mapping.from_combination( + InputCombination([InputConfig.btn_left()]), "keyboard", "1", ) ) self.assertTrue(self.preset.dangerously_mapped_btn_left()) self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=41)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=41)]), "keyboard", "2", ) @@ -409,8 +412,8 @@ def test_dangerously_mapped_btn_left(self): # another mapping maps to btn_left self.preset.add( - get_key_mapping( - InputCombination(InputConfig(type=EV_KEY, code=42)), + Mapping.from_combination( + InputCombination([InputConfig(type=EV_KEY, code=42)]), "mouse", "btn_left", ) @@ -418,7 +421,7 @@ def test_dangerously_mapped_btn_left(self): self.assertFalse(self.preset.dangerously_mapped_btn_left()) mapping = self.preset.get_mapping( - InputCombination(InputConfig(type=EV_KEY, code=42)) + InputCombination([InputConfig(type=EV_KEY, code=42)]) ) mapping.output_symbol = "BTN_Left" self.assertFalse(self.preset.dangerously_mapped_btn_left()) @@ -428,7 +431,7 @@ def test_dangerously_mapped_btn_left(self): self.assertTrue(self.preset.dangerously_mapped_btn_left()) # btn_left is not mapped - self.preset.remove(InputCombination(InputConfig.btn_left())) + self.preset.remove(InputCombination([InputConfig.btn_left()])) self.assertFalse(self.preset.dangerously_mapped_btn_left()) def test_save_load_with_invalid_mappings(self): @@ -443,7 +446,9 @@ def test_save_load_with_invalid_mappings(self): m.target_uinput = "keyboard" self.assertTrue(ui_preset.is_valid()) - m2 = UIMapping(input_combination=InputCombination(InputConfig(type=1, code=2))) + m2 = UIMapping( + input_combination=InputCombination([InputConfig(type=1, code=2)]) + ) ui_preset.add(m2) self.assertFalse(ui_preset.is_valid()) ui_preset.save() diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index 09f03284e..97a299cc8 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -18,12 +18,14 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +import asyncio import os import json import multiprocessing import time import unittest from typing import List, Optional +from unittest import mock from unittest.mock import patch, MagicMock from evdev.ecodes import ( @@ -33,7 +35,6 @@ KEY_COMMA, BTN_TOOL_DOUBLETAP, KEY_A, - EV_REL, REL_WHEEL, REL_X, ABS_X, @@ -50,7 +51,8 @@ from inputremapper.gui.messages.message_data import CombinationRecorded from inputremapper.gui.messages.message_types import MessageType from inputremapper.gui.reader_client import ReaderClient -from inputremapper.gui.reader_service import ReaderService +from inputremapper.gui.reader_service import ReaderService, ContextDummy +from inputremapper.input_event import InputEvent from tests.lib.fixtures import new_event from tests.lib.cleanup import quick_cleanup from tests.lib.constants import ( @@ -60,7 +62,8 @@ MIN_ABS, ) from tests.lib.pipes import push_event, push_events -from tests.lib.fixtures import fixtures, get_combination_config +from tests.lib.fixtures import fixtures +from tests.lib.stuff import spy CODE_1 = 100 CODE_2 = 101 @@ -86,7 +89,7 @@ def wait(func, timeout=1.0): break -class TestReader(unittest.TestCase): +class TestReaderAsyncio(unittest.IsolatedAsyncioTestCase): def setUp(self): self.reader_service = None self.groups = _Groups() @@ -100,8 +103,76 @@ def tearDown(self): except (BrokenPipeError, OSError): pass - if self.reader_service is not None: - self.reader_service.join() + async def create_reader_service(self, groups: Optional[_Groups] = None): + # this will cause pending events to be copied over to the reader-service + # process + if not groups: + groups = self.groups + + self.reader_service = ReaderService(groups) + asyncio.ensure_future(self.reader_service.run()) + + async def test_should_forward_to_dummy(self): + # It forwards to a ForwardDummy, because the gui process + # 1. can't inject and + # 2. is not even supposed to inject anything + # thanks to not using multiprocessing as opposed to the other tests, we can + # access this stuff + context = None + original_create_event_pipeline = ReaderService._create_event_pipeline + + def remember_context(*args, **kwargs): + nonlocal context + context = original_create_event_pipeline(*args, **kwargs) + return context + + with mock.patch( + "inputremapper.gui.reader_service.ReaderService._create_event_pipeline", + remember_context, + ): + await self.create_reader_service() + + listener = Listener() + self.message_broker.subscribe(MessageType.combination_recorded, listener) + + self.reader_client.set_group(self.groups.find(key="Foo Device 2")) + self.reader_client.start_recorder() + + await asyncio.sleep(0.1) + self.assertIsInstance(context, ContextDummy) + + with spy( + context.forward_dummy, + "write", + ) as write_spy: + events = [InputEvent.rel(REL_X, -1)] + push_events(fixtures.foo_device_2_mouse, events) + await asyncio.sleep(0.1) + self.reader_client._read() + self.assertEqual(0, len(listener.calls)) + + # we want `write` to be called on the forward_dummy, because we want + # those events to just disappear. + self.assertEqual(write_spy.call_count, len(events)) + self.assertEqual([call[0] for call in write_spy.call_args_list], events) + + +class TestReaderMultiprocessing(unittest.TestCase): + def setUp(self): + self.reader_service_process = None + self.groups = _Groups() + self.message_broker = MessageBroker() + self.reader_client = ReaderClient(self.message_broker, self.groups) + + def tearDown(self): + quick_cleanup() + try: + self.reader_client.terminate() + except (BrokenPipeError, OSError): + pass + + if self.reader_service_process is not None: + self.reader_service_process.join() def create_reader_service(self, groups: Optional[_Groups] = None): # this will cause pending events to be copied over to the reader-service @@ -111,10 +182,15 @@ def create_reader_service(self, groups: Optional[_Groups] = None): def start_reader_service(): reader_service = ReaderService(groups) - reader_service.run() + # this is a new process, so create a new event loop, or something + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(reader_service.run()) - self.reader_service = multiprocessing.Process(target=start_reader_service) - self.reader_service.start() + self.reader_service_process = multiprocessing.Process( + target=start_reader_service + ) + self.reader_service_process.start() time.sleep(0.1) def test_reading(self): @@ -126,13 +202,13 @@ def test_reading(self): self.reader_client.set_group(self.groups.find(key="Foo Device 2")) self.reader_client.start_recorder() - push_events(fixtures.foo_device_2_gamepad, [new_event(EV_ABS, ABS_HAT0X, 1)]) + push_events(fixtures.foo_device_2_gamepad, [InputEvent.abs(ABS_HAT0X, 1)]) # we need to sleep because we have two different fixtures, # which will lead to race conditions time.sleep(0.1) # relative axis events should be released automagically after 0.3s - push_events(fixtures.foo_device_2_mouse, [new_event(EV_REL, REL_X, 5)]) + push_events(fixtures.foo_device_2_mouse, [InputEvent.rel(REL_X, 5)]) time.sleep(0.1) # read all pending events. Having a glib mainloop would be better, # as it would call read automatically periodically @@ -141,17 +217,19 @@ def test_reading(self): [ CombinationRecorded( InputCombination( - InputConfig( - type=3, - code=16, - analog_threshold=1, - origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), - ) + [ + InputConfig( + type=3, + code=16, + analog_threshold=1, + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), + ) + ] ) ), CombinationRecorded( InputCombination( - ( + [ InputConfig( type=3, code=16, @@ -164,7 +242,7 @@ def test_reading(self): analog_threshold=1, origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), ), - ) + ] ) ), ], @@ -173,7 +251,7 @@ def test_reading(self): # release the hat switch should emit the recording finished event # as both the hat and relative axis are released by now - push_events(fixtures.foo_device_2_gamepad, [new_event(EV_ABS, ABS_HAT0X, 0)]) + push_events(fixtures.foo_device_2_gamepad, [InputEvent.abs(ABS_HAT0X, 0)]) time.sleep(0.3) self.reader_client._read() self.assertEqual([Signal(MessageType.recording_finished)], l2.calls) @@ -188,7 +266,7 @@ def test_should_release_relative_axis(self): self.reader_client.set_group(self.groups.find(key="Foo Device 2")) self.reader_client.start_recorder() - push_events(fixtures.foo_device_2_mouse, [new_event(EV_REL, REL_X, -5)]) + push_events(fixtures.foo_device_2_mouse, [InputEvent.rel(REL_X, -5)]) time.sleep(0.1) self.reader_client._read() @@ -196,12 +274,14 @@ def test_should_release_relative_axis(self): [ CombinationRecorded( InputCombination( - InputConfig( - type=2, - code=0, - analog_threshold=-1, - origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), - ) + [ + InputConfig( + type=2, + code=0, + analog_threshold=-1, + origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), + ) + ] ) ) ], @@ -220,7 +300,7 @@ def test_should_not_trigger_at_low_speed_for_rel_axis(self): self.reader_client.set_group(self.groups.find(key="Foo Device 2")) self.reader_client.start_recorder() - push_events(fixtures.foo_device_2_mouse, [new_event(EV_REL, REL_X, -1)]) + push_events(fixtures.foo_device_2_mouse, [InputEvent.rel(REL_X, -1)]) time.sleep(0.1) self.reader_client._read() self.assertEqual(0, len(l1.calls)) @@ -234,7 +314,7 @@ def test_should_trigger_wheel_at_low_speed(self): push_events( fixtures.foo_device_2_mouse, - [new_event(EV_REL, REL_WHEEL, -1), new_event(EV_REL, REL_HWHEEL, 1)], + [InputEvent.rel(REL_WHEEL, -1), InputEvent.rel(REL_HWHEEL, 1)], ) time.sleep(0.1) self.reader_client._read() @@ -243,17 +323,19 @@ def test_should_trigger_wheel_at_low_speed(self): [ CombinationRecorded( InputCombination( - InputConfig( - type=2, - code=8, - analog_threshold=-1, - origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), - ) + [ + InputConfig( + type=2, + code=8, + analog_threshold=-1, + origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), + ) + ] ) ), CombinationRecorded( InputCombination( - ( + [ InputConfig( type=2, code=8, @@ -266,7 +348,7 @@ def test_should_trigger_wheel_at_low_speed(self): analog_threshold=1, origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), ), - ) + ] ) ), ], @@ -280,11 +362,11 @@ def test_wont_emit_the_same_combination_twice(self): self.reader_client.set_group(self.groups.find(key="Foo Device 2")) self.reader_client.start_recorder() - push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, KEY_A, 1)]) + push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(KEY_A, 1)]) time.sleep(0.1) self.reader_client._read() # the duplicate event should be ignored - push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, KEY_A, 1)]) + push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(KEY_A, 1)]) time.sleep(0.1) self.reader_client._read() @@ -292,12 +374,14 @@ def test_wont_emit_the_same_combination_twice(self): [ CombinationRecorded( InputCombination( - InputConfig( - type=1, - code=30, - analog_threshold=1, - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), - ) + [ + InputConfig( + type=1, + code=30, + analog_threshold=1, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ] ) ) ], @@ -316,7 +400,7 @@ def test_should_read_absolut_axis(self): # over 30% should trigger push_events( fixtures.foo_device_2_gamepad, - [new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.4))], + [InputEvent.abs(ABS_X, int(MAX_ABS * 0.4))], ) time.sleep(0.1) self.reader_client._read() @@ -324,12 +408,14 @@ def test_should_read_absolut_axis(self): [ CombinationRecorded( InputCombination( - InputConfig( - type=3, - code=0, - analog_threshold=1, - origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), - ) + [ + InputConfig( + type=3, + code=0, + analog_threshold=1, + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), + ) + ] ) ) ], @@ -340,7 +426,7 @@ def test_should_read_absolut_axis(self): # less the 30% should release push_events( fixtures.foo_device_2_gamepad, - [new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.2))], + [InputEvent.abs(ABS_X, int(MAX_ABS * 0.2))], ) time.sleep(0.1) self.reader_client._read() @@ -348,12 +434,14 @@ def test_should_read_absolut_axis(self): [ CombinationRecorded( InputCombination( - InputConfig( - type=3, - code=0, - analog_threshold=1, - origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), - ) + [ + InputConfig( + type=3, + code=0, + analog_threshold=1, + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), + ) + ] ) ) ], @@ -368,19 +456,19 @@ def test_should_change_direction(self): self.reader_client.set_group(self.groups.find(key="Foo Device 2")) self.reader_client.start_recorder() - push_event(fixtures.foo_device_2_keyboard, new_event(EV_KEY, KEY_A, 1)) + push_event(fixtures.foo_device_2_keyboard, InputEvent.key(KEY_A, 1)) time.sleep(0.1) push_event( - fixtures.foo_device_2_gamepad, new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.4)) + fixtures.foo_device_2_gamepad, InputEvent.abs(ABS_X, int(MAX_ABS * 0.4)) ) time.sleep(0.1) - push_event(fixtures.foo_device_2_keyboard, new_event(EV_KEY, KEY_COMMA, 1)) + push_event(fixtures.foo_device_2_keyboard, InputEvent.key(KEY_COMMA, 1)) time.sleep(0.1) push_events( fixtures.foo_device_2_gamepad, [ - new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.1)), - new_event(EV_ABS, ABS_X, int(MIN_ABS * 0.4)), + InputEvent.abs(ABS_X, int(MAX_ABS * 0.1)), + InputEvent.abs(ABS_X, int(MIN_ABS * 0.4)), ], ) time.sleep(0.1) @@ -389,16 +477,18 @@ def test_should_change_direction(self): [ CombinationRecorded( InputCombination( - InputConfig( - type=EV_KEY, - code=KEY_A, - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=KEY_A, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ] ) ), CombinationRecorded( InputCombination( - ( + [ InputConfig( type=EV_KEY, code=KEY_A, @@ -410,12 +500,12 @@ def test_should_change_direction(self): analog_threshold=1, origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), ), - ) + ] ) ), CombinationRecorded( InputCombination( - ( + [ InputConfig( type=EV_KEY, code=KEY_A, @@ -432,12 +522,12 @@ def test_should_change_direction(self): code=KEY_COMMA, origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), ), - ) + ] ) ), CombinationRecorded( InputCombination( - ( + [ InputConfig( type=EV_KEY, code=KEY_A, @@ -454,7 +544,7 @@ def test_should_change_direction(self): code=KEY_COMMA, origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), ), - ) + ] ) ), ], @@ -468,7 +558,7 @@ def test_change_device(self): push_events( fixtures.foo_device_2_keyboard, [ - new_event(EV_KEY, 1, 1), + InputEvent.key(1, 1), ] * 10, ) @@ -476,8 +566,8 @@ def test_change_device(self): push_events( fixtures.bar_device, [ - new_event(EV_KEY, 2, 1), - new_event(EV_KEY, 2, 0), + InputEvent.key(2, 1), + InputEvent.key(2, 0), ] * 3, ) @@ -490,11 +580,13 @@ def test_change_device(self): self.assertEqual( l1.calls[0].combination, InputCombination( - InputConfig( - type=EV_KEY, - code=1, - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=1, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ] ), ) @@ -507,17 +599,19 @@ def test_change_device(self): self.assertEqual(len(l1.calls), 1) self.reader_client.start_recorder() - push_events(fixtures.bar_device, [new_event(EV_KEY, 2, 1)]) + push_events(fixtures.bar_device, [InputEvent.key(2, 1)]) time.sleep(0.1) self.reader_client._read() self.assertEqual( l1.calls[1].combination, InputCombination( - InputConfig( - type=EV_KEY, - code=2, - origin_hash=fixtures.bar_device.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=2, + origin_hash=fixtures.bar_device.get_device_hash(), + ) + ] ), ) @@ -564,7 +658,7 @@ def refresh(): self.assertEqual( l1.calls[-1].combination, InputCombination( - ( + [ InputConfig( type=EV_KEY, code=CODE_1, @@ -581,7 +675,7 @@ def refresh(): analog_threshold=-1, origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), ), - ) + ] ), ) @@ -592,9 +686,9 @@ def test_blacklisted_events(self): push_events( fixtures.foo_device_2_mouse, [ - new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1), - new_event(EV_KEY, BTN_LEFT, 1), - new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1), + InputEvent.key(BTN_TOOL_DOUBLETAP, 1), + InputEvent.key(BTN_LEFT, 1), + InputEvent.key(BTN_TOOL_DOUBLETAP, 1), ], force=True, ) @@ -606,11 +700,13 @@ def test_blacklisted_events(self): self.assertEqual( l1.calls[-1].combination, InputCombination( - InputConfig( - type=EV_KEY, - code=BTN_LEFT, - origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=BTN_LEFT, + origin_hash=fixtures.foo_device_2_mouse.get_device_hash(), + ) + ] ), ) @@ -620,7 +716,7 @@ def test_ignore_value_2(self): # this is not a combination, because (EV_KEY CODE_3, 2) is ignored push_events( fixtures.foo_device_2_gamepad, - [new_event(EV_ABS, ABS_HAT0X, 1), new_event(EV_KEY, CODE_3, 2)], + [InputEvent.abs(ABS_HAT0X, 1), InputEvent.key(CODE_3, 2)], force=True, ) self.create_reader_service() @@ -631,12 +727,14 @@ def test_ignore_value_2(self): self.assertEqual( l1.calls[-1].combination, InputCombination( - InputConfig( - type=EV_ABS, - code=ABS_HAT0X, - analog_threshold=1, - origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), - ) + [ + InputConfig( + type=EV_ABS, + code=ABS_HAT0X, + analog_threshold=1, + origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(), + ) + ] ), ) @@ -659,11 +757,13 @@ def test_reading_ignore_up(self): self.assertEqual( l1.calls[-1].combination, InputCombination( - InputConfig( - type=EV_KEY, - code=CODE_2, - origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), - ) + [ + InputConfig( + type=EV_KEY, + code=CODE_2, + origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(), + ) + ] ), ) @@ -674,9 +774,9 @@ def test_wrong_device(self): push_events( fixtures.foo_device_2_keyboard, [ - new_event(EV_KEY, CODE_1, 1), - new_event(EV_KEY, CODE_2, 1), - new_event(EV_KEY, CODE_3, 1), + InputEvent.key(CODE_1, 1), + InputEvent.key(CODE_2, 1), + InputEvent.key(CODE_3, 1), ], ) self.create_reader_service() @@ -696,9 +796,9 @@ def test_inputremapper_devices(self): push_events( fixtures.input_remapper_bar_device, [ - new_event(EV_KEY, CODE_1, 1), - new_event(EV_KEY, CODE_2, 1), - new_event(EV_KEY, CODE_3, 1), + InputEvent.key(CODE_1, 1), + InputEvent.key(CODE_2, 1), + InputEvent.key(CODE_3, 1), ], ) self.create_reader_service() @@ -712,7 +812,7 @@ def test_terminate(self): self.create_reader_service() self.reader_client.set_group(self.groups.find(key="Foo Device 2")) - push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, CODE_3, 1)]) + push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(CODE_3, 1)]) time.sleep(START_READING_DELAY + EVENT_READ_TIMEOUT) self.assertTrue(self.reader_client._results_pipe.poll()) @@ -721,7 +821,7 @@ def test_terminate(self): self.assertFalse(self.reader_client._results_pipe.poll()) # no new events arrive after terminating - push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, CODE_3, 1)]) + push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(CODE_3, 1)]) time.sleep(EVENT_READ_TIMEOUT * 3) self.assertFalse(self.reader_client._results_pipe.poll()) @@ -862,48 +962,48 @@ def test_reader_service_times_out(self): # that exposes user-input forever with patch.object(ReaderService, "_maximum_lifetime", 1): self.create_reader_service() - self.assertTrue(self.reader_service.is_alive()) + self.assertTrue(self.reader_service_process.is_alive()) time.sleep(0.5) - self.assertTrue(self.reader_service.is_alive()) + self.assertTrue(self.reader_service_process.is_alive()) time.sleep(1) - self.assertFalse(self.reader_service.is_alive()) + self.assertFalse(self.reader_service_process.is_alive()) def test_reader_service_waits_for_client_to_finish(self): # if the client is currently reading, it waits a bit longer until the # client finishes reading with patch.object(ReaderService, "_maximum_lifetime", 1): self.create_reader_service() - self.assertTrue(self.reader_service.is_alive()) + self.assertTrue(self.reader_service_process.is_alive()) self.reader_client.set_group(self.groups.find(key="Foo Device 2")) self.reader_client.start_recorder() time.sleep(2) # still alive, without start_recorder it should have already exited - self.assertTrue(self.reader_service.is_alive()) + self.assertTrue(self.reader_service_process.is_alive()) self.reader_client.stop_recorder() time.sleep(1) - self.assertFalse(self.reader_service.is_alive()) + self.assertFalse(self.reader_service_process.is_alive()) def test_reader_service_wont_wait_forever(self): # if the client is reading forever, stop it after another timeout with patch.object(ReaderService, "_maximum_lifetime", 1): with patch.object(ReaderService, "_timeout_tolerance", 1): self.create_reader_service() - self.assertTrue(self.reader_service.is_alive()) + self.assertTrue(self.reader_service_process.is_alive()) self.reader_client.set_group(self.groups.find(key="Foo Device 2")) self.reader_client.start_recorder() time.sleep(1.5) # still alive, without start_recorder it should have already exited - self.assertTrue(self.reader_service.is_alive()) + self.assertTrue(self.reader_service_process.is_alive()) time.sleep(1) # now it stopped, even though the reader is still reading - self.assertFalse(self.reader_service.is_alive()) + self.assertFalse(self.reader_service_process.is_alive()) if __name__ == "__main__": diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index d9c0b733d..5a1b4fcff 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -17,10 +17,8 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from inputremapper.utils import get_device_hash +import asyncio -from inputremapper.gui.messages.message_broker import MessageBroker -from tests.lib.fixtures import new_event from tests.lib.cleanup import cleanup, quick_cleanup from tests.lib.constants import EVENT_READ_TIMEOUT, START_READING_DELAY from tests.lib.logger import logger @@ -39,6 +37,9 @@ from inputremapper.groups import groups, _Groups from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.reader_service import ReaderService +from inputremapper.input_event import InputEvent +from inputremapper.utils import get_device_hash +from inputremapper.gui.messages.message_broker import MessageBroker class TestTest(unittest.TestCase): @@ -95,7 +96,8 @@ def start_reader_service(): # there is no point in using the global groups object # because the reader-service runs in a different process reader_service = ReaderService(_Groups()) - reader_service.run() + loop = asyncio.new_event_loop() + loop.run_until_complete(reader_service.run()) self.reader_service = multiprocessing.Process(target=start_reader_service) self.reader_service.start() @@ -113,7 +115,7 @@ def wait_for_results(): reader_client.start_recorder() time.sleep(START_READING_DELAY) - event = new_event(EV_KEY, 102, 1) + event = InputEvent.key(102, 1) push_events(fixtures.foo_device_2_keyboard, [event]) wait_for_results() self.assertTrue(reader_client._results_pipe.poll()) @@ -123,7 +125,7 @@ def wait_for_results(): # can push more events to the reader-service that is inside a separate # process, which end up being sent to the reader - event = new_event(EV_KEY, 102, 0) + event = InputEvent.key(102, 0) logger.info("push_events") push_events(fixtures.foo_device_2_keyboard, [event]) wait_for_results() diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py new file mode 100644 index 000000000..cb231f1cc --- /dev/null +++ b/tests/unit/test_util.py @@ -0,0 +1,41 @@ +#!/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 unittest + +from evdev._ecodes import EV_ABS, ABS_X, BTN_WEST, BTN_Y, EV_KEY, KEY_A + +from inputremapper.utils import get_evdev_constant_name + + +class TestUtil(unittest.TestCase): + def test_get_evdev_constant_name(self): + # BTN_WEST and BTN_Y both are code 308. I don't care which one is chosen + # in the return value, but it should return one of them without crashing. + self.assertEqual(get_evdev_constant_name(EV_KEY, BTN_Y), "BTN_WEST") + self.assertEqual(get_evdev_constant_name(EV_KEY, BTN_WEST), "BTN_WEST") + + self.assertEqual(get_evdev_constant_name(123, KEY_A), "unknown") + self.assertEqual(get_evdev_constant_name(EV_KEY, 9999), "unknown") + + self.assertEqual(get_evdev_constant_name(EV_KEY, KEY_A), "KEY_A") + + self.assertEqual(get_evdev_constant_name(EV_ABS, ABS_X), "ABS_X")