no .terminate anymore, proper multiprocessing coverage, some minor cleanup in injector

This commit is contained in:
sezanzeb 2020-12-01 23:53:32 +01:00
parent f4ca07490d
commit 17445558fb
14 changed files with 185 additions and 89 deletions

View File

@ -1,4 +1,5 @@
[run] [run]
concurrency = multiprocessing
branch = True branch = True
source = /usr/lib/python3.8/site-packages/keymapper source = keymapper
concurrency = multiprocessing
debug = multiproc

View File

@ -59,7 +59,8 @@ groups
##### Git/pip ##### Git/pip
```bash ```bash
sudo pip install git+https://github.com/sezanzeb/key-mapper.git git clone https://github.com/sezanzeb/key-mapper.git
cd key-mapper && sudo python3 setup.py install
``` ```
##### Manjaro/Arch ##### Manjaro/Arch
@ -93,12 +94,12 @@ sudo dpkg -i python3-key-mapper_0.1.0-1_all.deb
- [x] support timed macros, maybe using some sort of syntax - [x] support timed macros, maybe using some sort of syntax
- [x] add to the AUR, provide .deb file - [x] add to the AUR, provide .deb file
- [ ] automatically load presets when devices get plugged in after login - [ ] automatically load presets when devices get plugged in after login
- [ ] support gamepads as keyboard and mouse combi - [ ] support gamepads as keyboard and mouse combi (partially done)
## Tests ## Tests
```bash ```bash
pylint keymapper --extension-pkg-whitelist=evdev pylint keymapper --extension-pkg-whitelist=evdev
sudo pip install . && coverage run tests/test.py sudo pip install -e . && coverage run tests/test.py
coverage combine && coverage report -m coverage combine && coverage report -m
``` ```

View File

@ -27,7 +27,7 @@ import json
import shutil import shutil
import copy import copy
from keymapper.paths import CONFIG, touch from keymapper.paths import CONFIG, USER, touch
from keymapper.logger import logger from keymapper.logger import logger

View File

@ -138,8 +138,14 @@ class Daemon(service.Object):
return True return True
@dbus.service.method('keymapper.Interface') @dbus.service.method('keymapper.Interface', in_signature='b')
def stop(self): def stop(self, terminate=False):
"""Stop all mapping injections.""" """Stop all injections and end the service.
Raises dbus.exceptions.DBusException in your main process.
"""
for injector in self.injectors.values(): for injector in self.injectors.values():
injector.stop_injecting() injector.stop_injecting()
if terminate:
exit(0)

View File

@ -39,9 +39,7 @@ from keymapper.dev.macros import parse
DEV_NAME = 'key-mapper' DEV_NAME = 'key-mapper'
DEVICE_CREATED = 1 CLOSE = 0
FAILED = 2
DEVICE_SKIPPED = 3
def is_numlock_on(): def is_numlock_on():
@ -67,8 +65,8 @@ def toggle_numlock():
logger.debug('numlockx not found, trying to inject a keycode') logger.debug('numlockx not found, trying to inject a keycode')
# and this doesn't always work. # and this doesn't always work.
device = evdev.UInput( device = evdev.UInput(
name=f'key-mapper numlock-control', name=f'{DEV_NAME} numlock-control',
phys='key-mapper', phys=DEV_NAME,
) )
device.write(EV_KEY, evdev.ecodes.KEY_NUMLOCK, 1) device.write(EV_KEY, evdev.ecodes.KEY_NUMLOCK, 1)
device.syn() device.syn()
@ -110,10 +108,7 @@ class KeycodeInjector:
self.device = device self.device = device
self.mapping = mapping self.mapping = mapping
self._process = None self._process = None
self._msg_pipe = multiprocessing.Pipe()
def __del__(self):
if self._process is not None:
self._process.terminate()
def start_injecting(self): def start_injecting(self):
"""Start injecting keycodes.""" """Start injecting keycodes."""
@ -121,11 +116,16 @@ class KeycodeInjector:
self._process.start() self._process.start()
def _prepare_device(self, path): def _prepare_device(self, path):
"""Try to grab the device, return if not needed/possible.""" """Try to grab the device, return if not needed/possible.
Also return if ABS events are changed to REL mouse movements,
because the capabilities of the returned device are changed
so this cannot be checked later anymore.
"""
device = evdev.InputDevice(path) device = evdev.InputDevice(path)
if device is None: if device is None:
return None return None, False
capabilities = device.capabilities(absinfo=False) capabilities = device.capabilities(absinfo=False)
@ -136,15 +136,15 @@ class KeycodeInjector:
needed = True needed = True
break break
can_do_abs = evdev.ecodes.ABS_X in capabilities.get(EV_ABS, []) map_ABS = self.map_ABS(device)
if self.map_abs_to_rel() and can_do_abs: if map_ABS:
needed = True needed = True
if not needed: if not needed:
# skipping reading and checking on events from those devices # skipping reading and checking on events from those devices
# may be beneficial for performance. # may be beneficial for performance.
logger.debug('No need to grab %s', path) logger.debug('No need to grab %s', path)
return None return None, False
attempts = 0 attempts = 0
while True: while True:
@ -164,22 +164,25 @@ class KeycodeInjector:
if attempts >= 4: if attempts >= 4:
logger.error('Cannot grab %s, it is possibly in use', path) logger.error('Cannot grab %s, it is possibly in use', path)
return None return None, False
time.sleep(0.15) time.sleep(0.15)
return device return device, map_ABS
def map_abs_to_rel(self): def map_ABS(self, device):
# TODO offer configuration via the UI if a gamepad is elected # TODO offer configuration via the UI if a gamepad is elected
return True capabilities = device.capabilities(absinfo=False)
return evdev.ecodes.ABS_X in capabilities.get(EV_ABS, [])
def _modify_capabilities(self, input_device): def _modify_capabilities(self, input_device, map_ABS):
"""Adds all keycode into a copy of a devices capabilities. """Adds all keycode into a copy of a devices capabilities.
Prameters Prameters
--------- ---------
input_device : evdev.InputDevice input_device : evdev.InputDevice
map_ABS : bool
if ABS capabilities should be removed in favor of REL
""" """
ecodes = evdev.ecodes ecodes = evdev.ecodes
@ -196,9 +199,8 @@ class KeycodeInjector:
if keycode is not None: if keycode is not None:
capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET) capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET)
if self.map_abs_to_rel(): if map_ABS:
if capabilities.get(ecodes.EV_ABS): del capabilities[ecodes.EV_ABS]
del capabilities[ecodes.EV_ABS]
capabilities[ecodes.EV_REL] = [ capabilities[ecodes.EV_REL] = [
evdev.ecodes.REL_X, evdev.ecodes.REL_X,
evdev.ecodes.REL_Y, evdev.ecodes.REL_Y,
@ -214,23 +216,37 @@ class KeycodeInjector:
return capabilities return capabilities
async def _msg_listener(self, loop):
"""Wait for messages from the main process to do special stuff."""
while True:
frame_available = asyncio.Event()
loop.add_reader(self._msg_pipe[0].fileno(), frame_available.set)
await frame_available.wait()
frame_available.clear()
msg = self._msg_pipe[0].recv()
if msg == CLOSE:
logger.debug('Received close signal')
# stop the event loop and cause the process to reach its end
# cleanly. Using .terminate prevents coverage from working.
loop.stop()
return
def _start_injecting(self): def _start_injecting(self):
"""The injection worker that keeps injecting until terminated. """The injection worker that keeps injecting until terminated.
Stuff is non-blocking by using asyncio in order to do multiple things Stuff is non-blocking by using asyncio in order to do multiple things
somewhat concurrently. somewhat concurrently.
""" """
# TODO do select.select insted of async_read_loop
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
coroutines = [] coroutines = []
logger.info('Starting injecting the mapping for %s', self.device) logger.info('Starting injecting the mapping for %s', self.device)
paths = get_devices()[self.device]['paths'] paths = get_devices()[self.device]['paths']
devices = [self._prepare_device(path) for path in paths]
# Watch over each one of the potentially multiple devices per hardware # Watch over each one of the potentially multiple devices per hardware
for input_device in devices: for path in paths:
input_device, map_ABS = self._prepare_device(path)
if input_device is None: if input_device is None:
continue continue
@ -238,17 +254,19 @@ class KeycodeInjector:
# EV_ABS capability, EV_REL won't move the mouse pointer anymore. # EV_ABS capability, EV_REL won't move the mouse pointer anymore.
# so don't merge all InputDevices into one UInput device. # so don't merge all InputDevices into one UInput device.
uinput = evdev.UInput( uinput = evdev.UInput(
name=f'key-mapper {self.device}', name=f'{DEV_NAME} {self.device}',
phys='key-mapper', phys=DEV_NAME,
events=self._modify_capabilities(input_device) events=self._modify_capabilities(input_device, map_ABS)
) )
# TODO separate file
# keycode injection # keycode injection
coroutine = self._keycode_loop(input_device, uinput) coroutine = self._keycode_loop(input_device, uinput, map_ABS)
coroutines.append(coroutine) coroutines.append(coroutine)
# TODO separate file
# mouse movement injection # mouse movement injection
if self.map_abs_to_rel(): if map_ABS:
self.abs_x = 0 self.abs_x = 0
self.abs_y = 0 self.abs_y = 0
# events only take ints, so a movement of 0.3 needs to add # events only take ints, so a movement of 0.3 needs to add
@ -262,7 +280,13 @@ class KeycodeInjector:
logger.error('Did not grab any device') logger.error('Did not grab any device')
return return
loop.run_until_complete(asyncio.gather(*coroutines)) coroutines.append(self._msg_listener(loop))
try:
loop.run_until_complete(asyncio.gather(*coroutines))
except RuntimeError:
# stopped event loop most likely
pass
if len(coroutines) > 0: if len(coroutines) > 0:
logger.debug('asyncio coroutines ended') logger.debug('asyncio coroutines ended')
@ -336,7 +360,7 @@ class KeycodeInjector:
rel_x rel_x
) )
async def _keycode_loop(self, device, keymapper_device): async def _keycode_loop(self, device, keymapper_device, map_ABS):
"""Inject keycodes for one of the virtual devices. """Inject keycodes for one of the virtual devices.
Parameters Parameters
@ -345,6 +369,8 @@ class KeycodeInjector:
where to read keycodes from where to read keycodes from
keymapper_device : evdev.UInput keymapper_device : evdev.UInput
where to write keycodes to where to write keycodes to
map_ABS : bool
the value of map_ABS() for the original device
""" """
# Parse all macros beforehand # Parse all macros beforehand
logger.debug('Parsing macros') logger.debug('Parsing macros')
@ -363,7 +389,7 @@ class KeycodeInjector:
) )
async for event in device.async_read_loop(): async for event in device.async_read_loop():
if self.map_abs_to_rel() and event.type == EV_ABS: if map_ABS and event.type == EV_ABS:
if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]: if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]:
continue continue
if event.code == evdev.ecodes.ABS_X: if event.code == evdev.ecodes.ABS_X:
@ -438,5 +464,4 @@ class KeycodeInjector:
def stop_injecting(self): def stop_injecting(self):
"""Stop injecting keycodes.""" """Stop injecting keycodes."""
logger.info('Stopping injecting keycodes for device "%s"', self.device) logger.info('Stopping injecting keycodes for device "%s"', self.device)
if self._process is not None and self._process.is_alive(): self._msg_pipe[1].send(CLOSE)
self._process.terminate()

View File

@ -31,6 +31,9 @@ from keymapper.getdevices import get_devices, refresh_devices
from keymapper.state import KEYCODE_OFFSET from keymapper.state import KEYCODE_OFFSET
CLOSE = 1
class _KeycodeReader: class _KeycodeReader:
"""Keeps reading keycodes in the background for the UI to use. """Keeps reading keycodes in the background for the UI to use.
@ -47,15 +50,9 @@ class _KeycodeReader:
self.stop_reading() self.stop_reading()
def stop_reading(self): def stop_reading(self):
# TODO something like this for the injector?
if self._process is not None:
logger.debug('Terminating reader process')
self._process.terminate()
self._process = None
if self._pipe is not None: if self._pipe is not None:
logger.debug('Closing reader pipe') logger.debug('Sending close msg to reader')
self._pipe[0].close() self._pipe[0].send(CLOSE)
self._pipe = None self._pipe = None
def clear(self): def clear(self):
@ -77,8 +74,7 @@ class _KeycodeReader:
self.virtual_devices = [] self.virtual_devices = []
for name, group in get_devices(include_keymapper=True).items(): for name, group in get_devices().items():
# also find stuff like "key-mapper {device}"
if device_name not in name: if device_name not in name:
continue continue
@ -99,35 +95,46 @@ class _KeycodeReader:
) )
pipe = multiprocessing.Pipe() pipe = multiprocessing.Pipe()
self._process = multiprocessing.Process(
target=self._read_worker,
args=(pipe[1],)
)
self._process.start()
self._pipe = pipe self._pipe = pipe
self._process = multiprocessing.Process(target=self._read_worker)
self._process.start()
def _consume_event(self, event, pipe): def _consume_event(self, event):
"""Write the event code into the pipe if it is a key-down press.""" """Write the event code into the pipe if it is a key-down press."""
# value: 1 for down, 0 for up, 2 for hold. # value: 1 for down, 0 for up, 2 for hold.
if self._pipe[1].closed:
logger.debug('Pipe closed, reader stops.')
exit(0)
if event.type == evdev.ecodes.EV_KEY and event.value == 1: if event.type == evdev.ecodes.EV_KEY and event.value == 1:
logger.spam( logger.spam(
'got code:%s value:%s', 'got code:%s value:%s',
event.code + KEYCODE_OFFSET, event.value event.code + KEYCODE_OFFSET, event.value
) )
pipe.send(event.code + KEYCODE_OFFSET) self._pipe[1].send(event.code + KEYCODE_OFFSET)
def _read_worker(self, pipe): def _read_worker(self):
"""Process that reads keycodes and buffers them into a pipe.""" """Process that reads keycodes and buffers them into a pipe."""
# using a process that blocks instead of read_one made it easier # using a process that blocks instead of read_one made it easier
# to debug via the logs, because the UI was not polling properly # to debug via the logs, because the UI was not polling properly
# at some point which caused logs for events not to be written. # at some point which caused logs for events not to be written.
rlist = {device.fd: device for device in self.virtual_devices} rlist = {device.fd: device for device in self.virtual_devices}
rlist[self._pipe[1]] = self._pipe[1]
while True: while True:
ready = select.select(rlist, [], [])[0] ready = select.select(rlist, [], [])[0]
for fd in ready: for fd in ready:
readable = rlist[fd]
if isinstance(readable, multiprocessing.connection.Connection):
msg = readable.recv()
if msg == CLOSE:
logger.debug('Reader stopped')
return
continue
try: try:
for event in rlist[fd].read(): for event in rlist[fd].read():
self._consume_event(event, pipe) self._consume_event(event)
except OSError: except OSError:
logger.debug( logger.debug(
'Device "%s" disappeared from the reader', 'Device "%s" disappeared from the reader',

View File

@ -69,6 +69,9 @@ def get_selected_row_bg():
return color.to_string() return color.to_string()
# TODO show if the preset is being injected
class Window: class Window:
"""User Interface.""" """User Interface."""
def __init__(self): def __init__(self):

View File

@ -62,6 +62,13 @@ class Formatter(logging.Formatter):
SPAM: 34, SPAM: 34,
logging.INFO: 32, logging.INFO: 32,
}.get(record.levelno, 0) }.get(record.levelno, 0)
# if this runs in a separate process, write down the pid
# to debug exit codes and such
pid = ''
if os.getpid() != logger.main_pid:
pid = f'pid {os.getpid()}, '
if debug: if debug:
self._style._fmt = ( # noqa self._style._fmt = ( # noqa
'\033[1m' # bold '\033[1m' # bold
@ -69,7 +76,7 @@ class Formatter(logging.Formatter):
f'%(levelname)s' f'%(levelname)s'
'\033[0m' # end style '\033[0m' # end style
f'\033[{color}m' # color f'\033[{color}m' # color
': %(filename)s, line %(lineno)d, %(message)s' f': {pid}%(filename)s, line %(lineno)d, %(message)s'
'\033[0m' # end style '\033[0m' # end style
) )
else: else:
@ -85,6 +92,7 @@ handler.setFormatter(Formatter())
logger.addHandler(handler) logger.addHandler(handler)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
logging.getLogger('asyncio').setLevel(logging.WARNING) logging.getLogger('asyncio').setLevel(logging.WARNING)
logger.main_pid = os.getpid()
def is_debug(): def is_debug():

13
scripts/install.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# in case setup.py does nothing instead of something.
# call via `./scripts/build.sh`
# try both ways of installation
sudo pip3 install .
sudo python3 setup.py install
# copy crucial files
sudo cp bin/* /usr/bin/ -r
sudo mkdir /usr/share/key-mapper
sudo cp data/* /usr/share/key-mapper -r

View File

@ -158,10 +158,18 @@ def patch_select():
import select import select
def new_select(rlist, *args): def new_select(rlist, *args):
return ([ ret = []
device for device in rlist for thing in rlist:
if len(pending_events.get(device, [])) > 0 if hasattr(thing, 'poll') and thing.poll():
],) # the reader receives msgs through pipes. If there is one
# ready, provide the pipe
ret.append(thing)
continue
if len(pending_events.get(thing, [])) > 0:
ret.append(thing)
return [ret, [], []]
select.select = new_select select.select = new_select

View File

@ -20,13 +20,9 @@
import os import os
import sys
import multiprocessing import multiprocessing
import unittest import unittest
import time import time
from unittest.mock import patch
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
import dbus import dbus
import evdev import evdev
@ -61,12 +57,10 @@ class TestDBusDaemon(unittest.TestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
cls.interface.stop() try:
time.sleep(0.1) cls.interface.stop(True)
cls.process.terminate() except dbus.exceptions.DBusException:
time.sleep(0.1) pass
os.system('pkill -f key-mapper-service')
time.sleep(0.1)
def test_can_connect(self): def test_can_connect(self):
self.assertIsInstance(self.interface, dbus.Interface) self.assertIsInstance(self.interface, dbus.Interface)

View File

@ -58,8 +58,9 @@ class TestInjector(unittest.TestCase):
self.injector.stop_injecting() self.injector.stop_injecting()
self.injector = None self.injector = None
evdev.InputDevice.grab = self.grab evdev.InputDevice.grab = self.grab
if pending_events.get('device 2') is not None: keys = list(pending_events.keys())
del pending_events['device 2'] for key in keys:
del pending_events[key]
clear_write_history() clear_write_history()
def test_modify_capabilities(self): def test_modify_capabilities(self):
@ -80,7 +81,11 @@ class TestInjector(unittest.TestCase):
maps_to = system_mapping['a'] - KEYCODE_OFFSET maps_to = system_mapping['a'] - KEYCODE_OFFSET
self.injector = KeycodeInjector('foo', mapping) self.injector = KeycodeInjector('foo', mapping)
capabilities = self.injector._modify_capabilities(FakeDevice()) fake_device = FakeDevice()
capabilities = self.injector._modify_capabilities(
fake_device,
map_ABS=False
)
self.assertIn(EV_KEY, capabilities) self.assertIn(EV_KEY, capabilities)
keys = capabilities[EV_KEY] keys = capabilities[EV_KEY]
@ -97,17 +102,30 @@ class TestInjector(unittest.TestCase):
path = '/dev/input/event10' path = '/dev/input/event10'
# this test needs to pass around all other constraints of # this test needs to pass around all other constraints of
# _prepare_device # _prepare_device
device = self.injector._prepare_device(path) device, map_ABS = self.injector._prepare_device(path)
self.assertFalse(map_ABS)
self.assertEqual(self.failed, 2) self.assertEqual(self.failed, 2)
# success on the third try # success on the third try
device.name = fixtures[path]['name'] device.name = fixtures[path]['name']
def test_gamepad_capabilities(self):
self.injector = KeycodeInjector('gamepad', custom_mapping)
path = '/dev/input/event30'
device, map_ABS = self.injector._prepare_device(path)
self.assertTrue(map_ABS)
capabilities = self.injector._modify_capabilities(device, map_ABS)
self.assertNotIn(evdev.ecodes.EV_ABS, capabilities)
self.assertIn(evdev.ecodes.EV_REL, capabilities)
def test_skip_unused_device(self): def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping # skips a device because its capabilities are not used in the mapping
custom_mapping.change(10, 'a') custom_mapping.change(10, 'a')
self.injector = KeycodeInjector('device 1', custom_mapping) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event11' path = '/dev/input/event11'
device = self.injector._prepare_device(path) device, map_ABS = self.injector._prepare_device(path)
self.assertFalse(map_ABS)
self.assertEqual(self.failed, 0) self.assertEqual(self.failed, 0)
self.assertIsNone(device) self.assertIsNone(device)
@ -115,7 +133,7 @@ class TestInjector(unittest.TestCase):
# skips a device because its capabilities are not used in the mapping # skips a device because its capabilities are not used in the mapping
self.injector = KeycodeInjector('device 1', custom_mapping) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event11' path = '/dev/input/event11'
device = self.injector._prepare_device(path) device, _ = self.injector._prepare_device(path)
# make sure the test uses a fixture without interesting capabilities # make sure the test uses a fixture without interesting capabilities
capabilities = evdev.InputDevice(path).capabilities() capabilities = evdev.InputDevice(path).capabilities()
@ -145,7 +163,7 @@ class TestInjector(unittest.TestCase):
def test_abs_to_rel(self): def test_abs_to_rel(self):
# maps gamepad joystick events to mouse events # maps gamepad joystick events to mouse events
# TODO enable this somewhere so that map_abs_to_rel returns true # TODO enable this somewhere so that map_ABS returns true
# in the .json file of the mapping. # in the .json file of the mapping.
config.set('gamepad.non_linearity', 1) config.set('gamepad.non_linearity', 1)
pointer_speed = 80 pointer_speed = 80
@ -182,6 +200,11 @@ class TestInjector(unittest.TestCase):
event = uinput_write_history_pipe[0].recv() event = uinput_write_history_pipe[0].recv()
history.append((event.type, event.code, event.value)) history.append((event.type, event.code, event.value))
if history[0][0] == EV_ABS:
raise AssertionError(
'The injector probably just forwarded them unchanged'
)
# movement is written at 60hz and it takes `divisor` steps to # movement is written at 60hz and it takes `divisor` steps to
# move 1px. take it times 2 for both x and y events. # move 1px. take it times 2 for both x and y events.
self.assertGreater(len(history), 60 * sleep * 0.9 * 2 / divisor) self.assertGreater(len(history), 60 * sleep * 0.9 * 2 / divisor)

View File

@ -50,8 +50,10 @@ def gtk_iteration():
Gtk.main_iteration() Gtk.main_iteration()
def launch(argv=None, bin_path='/bin/key-mapper-gtk'): def launch(argv=None):
"""Start key-mapper-gtk with the command line argument array argv.""" """Start key-mapper-gtk with the command line argument array argv."""
bin_path = os.path.join(os.getcwd(), 'bin', 'key-mapper-gtk')
if not argv: if not argv:
argv = ['-d'] argv = ['-d']

View File

@ -52,6 +52,10 @@ class TestReader(unittest.TestCase):
Event(evdev.events.EV_KEY, CODE_3, 1) Event(evdev.events.EV_KEY, CODE_3, 1)
] ]
keycode_reader.start_reading('device 1') keycode_reader.start_reading('device 1')
# sending anything arbitrary does not stop the pipe
keycode_reader._pipe[0].send(1234)
time.sleep(EVENT_READ_TIMEOUT * 5) time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), CODE_3 + 8) self.assertEqual(keycode_reader.read(), CODE_3 + 8)
self.assertIsNone(keycode_reader.read()) self.assertIsNone(keycode_reader.read())
@ -67,8 +71,10 @@ class TestReader(unittest.TestCase):
self.assertIsNone(keycode_reader.read()) self.assertIsNone(keycode_reader.read())
def test_keymapper_devices(self): def test_keymapper_devices(self):
# In order to show pressed keycodes on the ui while the device is # Don't read from keymapper devices, their keycodes are not
# grabbed, read from that as well. # representative for the original key. As long as this is not
# intentionally programmed it won't even do that. But it was at some
# point.
pending_events['key-mapper device 2'] = [ pending_events['key-mapper device 2'] = [
Event(evdev.events.EV_KEY, CODE_1, 1), Event(evdev.events.EV_KEY, CODE_1, 1),
Event(evdev.events.EV_KEY, CODE_2, 1), Event(evdev.events.EV_KEY, CODE_2, 1),
@ -76,7 +82,6 @@ class TestReader(unittest.TestCase):
] ]
keycode_reader.start_reading('device 2') keycode_reader.start_reading('device 2')
time.sleep(EVENT_READ_TIMEOUT * 5) time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), CODE_3 + 8)
self.assertIsNone(keycode_reader.read()) self.assertIsNone(keycode_reader.read())
def test_clear(self): def test_clear(self):