Merge lp:~elopio/autopilot/no_uinput_side-effects2 into lp:autopilot

Proposed by Leo Arias
Status: Superseded
Proposed branch: lp:~elopio/autopilot/no_uinput_side-effects2
Merge into: lp:autopilot
Diff against target: 1143 lines (+731/-202)
4 files modified
autopilot/input/_uinput.py (+258/-192)
autopilot/tests/functional/test_input_stack.py (+6/-5)
autopilot/tests/unit/test_input.py (+465/-5)
debian/control (+2/-0)
To merge this branch: bzr merge lp:~elopio/autopilot/no_uinput_side-effects2
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
Autopilot Hackers Pending
Review via email: mp+202467@code.launchpad.net

This proposal supersedes a proposal from 2014-01-21.

This proposal has been superseded by a proposal from 2014-01-21.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:425
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~elopio/autopilot/no_uinput_side-effects2/+merge/202467/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/autopilot-ci/419/
Executed test runs:
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-trusty-amd64-ci/145/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-trusty-armhf-ci/145/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-trusty/2495/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-trusty-amd64/2497/console

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/autopilot-ci/419/rebuild

review: Needs Fixing (continuous-integration)
426. By Leo Arias

Create the Touch device with a dummy resolution.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:426
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~elopio/autopilot/no_uinput_side-effects2/+merge/202467/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/autopilot-ci/420/
Executed test runs:
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-trusty-amd64-ci/146/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-trusty-armhf-ci/146/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-trusty/2497/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-trusty-amd64/2499/console

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/autopilot-ci/420/rebuild

review: Needs Fixing (continuous-integration)
427. By Leo Arias

Fixed pep8.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
428. By Leo Arias

Make sure that the keyboard mock will be used.

429. By Leo Arias

updated the pressed keys list.

430. By Leo Arias

Typo.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
431. By Leo Arias

    updated the pressed keys list.

432. By Leo Arias

Fixed the release all.

433. By Leo Arias

Added a unit test for the previous bug.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
434. By Leo Arias

Added the failing test case.

435. By Leo Arias

Fixed the test with multiple fingers.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/input/_uinput.py'
2--- autopilot/input/_uinput.py 2013-11-07 05:53:36 +0000
3+++ autopilot/input/_uinput.py 2014-01-21 22:20:18 +0000
4@@ -1,7 +1,7 @@
5 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
6 #
7 # Autopilot Functional Test Tool
8-# Copyright (C) 2012-2013 Canonical
9+# Copyright (C) 2012, 2013, 2014 Canonical
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13@@ -20,10 +20,10 @@
14
15 """UInput device drivers."""
16
17+from autopilot import utilities
18 from autopilot.input import Keyboard as KeyboardBase
19 from autopilot.input import Touch as TouchBase
20 from autopilot.input._common import get_center_point
21-from autopilot.utilities import sleep
22 import autopilot.platform
23
24 import logging
25@@ -33,11 +33,6 @@
26
27 logger = logging.getLogger(__name__)
28
29-PRESS = 1
30-RELEASE = 0
31-
32-_PRESSED_KEYS = []
33-
34
35 def _get_devnode_path():
36 """Provide a fallback uinput node for devices which don't support udev"""
37@@ -47,13 +42,76 @@
38 return devnode
39
40
41+class _UInputKeyboardDevice(object):
42+ """Wrapper for the UInput Keyboard to execute its primitives."""
43+
44+ def __init__(self, device_class=UInput):
45+ super(_UInputKeyboardDevice, self).__init__()
46+ self._device = device_class(devnode=_get_devnode_path())
47+ self._pressed_keys_ecodes = []
48+
49+ def press(self, key):
50+ """Press one key button.
51+
52+ It ignores case, so, for example, 'a' and 'A' are mapped to the same
53+ key.
54+
55+ """
56+ ecode = self._get_ecode_for_key(key)
57+ logger.debug('Pressing %s (%r).', key, ecode)
58+ self._emit_press_event(ecode)
59+ self._pressed_keys_ecodes.append(ecode)
60+
61+ def _get_ecode_for_key(self, key):
62+ key_name = key if key.startswith('KEY_') else 'KEY_' + key
63+ key_name = key_name.upper()
64+ ecode = e.ecodes.get(key_name, None)
65+ if ecode is None:
66+ raise ValueError('Unknown key name: %s.' % key)
67+ return ecode
68+
69+ def _emit_press_event(self, ecode):
70+ press_value = 1
71+ self._emit(ecode, press_value)
72+
73+ def _emit(self, ecode, value):
74+ self._device.write(e.EV_KEY, ecode, value)
75+ self._device.syn()
76+
77+ def release(self, key):
78+ """Release one key button.
79+
80+ It ignores case, so, for example, 'a' and 'A' are mapped to the same
81+ key.
82+
83+ """
84+ ecode = self._get_ecode_for_key(key)
85+ if ecode in self._pressed_keys_ecodes:
86+ logger.debug('Releasing %s (%r).', key, ecode)
87+ self._emit_release_event(ecode)
88+ self._pressed_keys_ecodes.remove(ecode)
89+ else:
90+ raise ValueError('Key %r not pressed.' % key)
91+
92+ def _emit_release_event(self, ecode):
93+ release_value = 0
94+ self._emit(ecode, release_value)
95+
96+ def release_pressed_keys(self):
97+ """Release all the keys that are currently pressed."""
98+ for ecode in self._pressed_keys_ecodes:
99+ self._emit_release_event(ecode)
100+ self._pressed_keys_ecodes = []
101+
102+
103 class Keyboard(KeyboardBase):
104
105- _device = UInput(devnode=_get_devnode_path())
106+ _device = None
107
108- def _emit(self, event, value):
109- Keyboard._device.write(e.EV_KEY, event, value)
110- Keyboard._device.syn()
111+ def __init__(self, device_class=_UInputKeyboardDevice):
112+ super(Keyboard, self).__init__()
113+ if Keyboard._device is None:
114+ Keyboard._device = device_class()
115
116 def _sanitise_keys(self, keys):
117 if keys == '+':
118@@ -76,11 +134,9 @@
119 raise TypeError("'keys' argument must be a string.")
120
121 for key in self._sanitise_keys(keys):
122- for event in Keyboard._get_events_for_key(key):
123- logger.debug("Pressing %s (%r)", key, event)
124- _PRESSED_KEYS.append(event)
125- self._emit(event, PRESS)
126- sleep(delay)
127+ for key_button in self._get_key_buttons(key):
128+ self._device.press(key_button)
129+ utilities.sleep(delay)
130
131 def release(self, keys, delay=0.1):
132 """Send key release events only.
133@@ -99,12 +155,9 @@
134 raise TypeError("'keys' argument must be a string.")
135
136 for key in reversed(self._sanitise_keys(keys)):
137- for event in Keyboard._get_events_for_key(key):
138- logger.debug("Releasing %s (%r)", key, event)
139- if event in _PRESSED_KEYS:
140- _PRESSED_KEYS.remove(event)
141- self._emit(event, RELEASE)
142- sleep(delay)
143+ for key_button in reversed(self._get_key_buttons(key)):
144+ self._device.release(key_button)
145+ utilities.sleep(delay)
146
147 def press_and_release(self, keys, delay=0.1):
148 """Press and release all items in 'keys'.
149@@ -145,98 +198,33 @@
150 any keys that were pressed and not released.
151
152 """
153- global _PRESSED_KEYS
154- if len(_PRESSED_KEYS) == 0:
155- return
156-
157- def _release(event):
158- Keyboard._device.write(e.EV_KEY, event, RELEASE)
159- Keyboard._device.syn()
160- for event in _PRESSED_KEYS:
161- logger.warning("Releasing key %r as part of cleanup call.", event)
162- _release(event)
163- _PRESSED_KEYS = []
164-
165- @staticmethod
166- def _get_events_for_key(key):
167- """Return a list of events required to generate 'key' as an input.
168-
169- Multiple keys will be returned when the key specified requires more
170+ cls._device.release_pressed_keys()
171+
172+ def _get_key_buttons(self, key):
173+ """Return a list of the key buttons required to press.
174+
175+ Multiple buttons will be returned when the key specified requires more
176 than one keypress to generate (for example, upper-case letters).
177
178 """
179- events = []
180+ key_buttons = []
181 if key.isupper() or key in _SHIFTED_KEYS:
182- events.append(e.KEY_LEFTSHIFT)
183- keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
184- evt = getattr(e, 'KEY_' + keyname.upper(), None)
185- if evt is None:
186- raise ValueError("Unknown key name: '%s'" % key)
187- events.append(evt)
188- return events
189-
190-
191-last_tracking_id = 0
192-
193-
194-def get_next_tracking_id():
195- global last_tracking_id
196- last_tracking_id += 1
197- return last_tracking_id
198-
199-
200+ key_buttons.append('KEY_LEFTSHIFT')
201+ key_name = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
202+ key_buttons.append(key_name)
203+ return key_buttons
204+
205+
206+@utilities.deprecated('Touch')
207 def create_touch_device(res_x=None, res_y=None):
208 """Create and return a UInput touch device.
209
210 If res_x and res_y are not specified, they will be queried from the system.
211
212 """
213-
214- if res_x is None or res_y is None:
215- from autopilot.display import Display
216- display = Display.create()
217- # TODO: This calculation needs to become part of the display module:
218- l = r = t = b = 0
219- for screen in range(display.get_num_screens()):
220- geometry = display.get_screen_geometry(screen)
221- if geometry[0] < l:
222- l = geometry[0]
223- if geometry[1] < t:
224- t = geometry[1]
225- if geometry[0] + geometry[2] > r:
226- r = geometry[0] + geometry[2]
227- if geometry[1] + geometry[3] > b:
228- b = geometry[1] + geometry[3]
229- res_x = r - l
230- res_y = b - t
231-
232- # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no
233- # idea why...
234- touch_tool = e.BTN_TOOL_FINGER
235- if autopilot.platform.model() == 'Desktop':
236- touch_tool = e.BTN_TOUCH
237-
238- cap_mt = {
239- e.EV_ABS: [
240- (e.ABS_X, (0, res_x, 0, 0)),
241- (e.ABS_Y, (0, res_y, 0, 0)),
242- (e.ABS_PRESSURE, (0, 65535, 0, 0)),
243- (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
244- (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
245- (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
246- (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
247- (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
248- (e.ABS_MT_SLOT, (0, 9, 0, 0)),
249- ],
250- e.EV_KEY: [
251- touch_tool,
252- ]
253- }
254-
255- return UInput(cap_mt, name='autopilot-finger', version=0x2,
256- devnode=_get_devnode_path())
257-
258-_touch_device = create_touch_device()
259+ return UInput(events=_get_touch_events(), name='autopilot-finger',
260+ version=0x2, devnode=_get_devnode_path())
261+
262
263 # Multiouch notes:
264 # ----------------
265@@ -281,58 +269,177 @@
266 # about this is that the SLOT refers to a finger number, and the TRACKING_ID
267 # identifies a unique touch for the duration of it's existance.
268
269-_touch_fingers_in_use = []
270-
271-
272-def _get_touch_finger():
273- """Claim a touch finger id for use.
274-
275- :raises: RuntimeError if no more fingers are available.
276-
277- """
278- global _touch_fingers_in_use
279-
280- for i in range(9):
281- if i not in _touch_fingers_in_use:
282- _touch_fingers_in_use.append(i)
283- return i
284- raise RuntimeError("All available fingers have been used already.")
285-
286-
287-def _release_touch_finger(finger_num):
288- """Relase a previously-claimed finger id.
289-
290- :raises: RuntimeError if the finger given was never claimed, or was already
291- released.
292-
293- """
294- global _touch_fingers_in_use
295-
296- if finger_num not in _touch_fingers_in_use:
297- raise RuntimeError(
298- "Finger %d was never claimed, or has already been released." %
299- (finger_num))
300- _touch_fingers_in_use.remove(finger_num)
301- assert(finger_num not in _touch_fingers_in_use)
302+
303+def _get_touch_events(res_x, res_y):
304+ if res_x is None or res_y is None:
305+ res_x, res_y = _get_system_resolution()
306+
307+ touch_tool = _get_touch_tool()
308+
309+ events = {
310+ e.EV_ABS: [
311+ (e.ABS_X, (0, res_x, 0, 0)),
312+ (e.ABS_Y, (0, res_y, 0, 0)),
313+ (e.ABS_PRESSURE, (0, 65535, 0, 0)),
314+ (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
315+ (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
316+ (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
317+ (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
318+ (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
319+ (e.ABS_MT_SLOT, (0, 9, 0, 0)),
320+ ],
321+ e.EV_KEY: [
322+ touch_tool,
323+ ]
324+ }
325+ return events
326+
327+
328+def _get_system_resolution():
329+ from autopilot.display import Display
330+ display = Display.create()
331+ # TODO: This calculation needs to become part of the display module:
332+ l = r = t = b = 0
333+ for screen in range(display.get_num_screens()):
334+ geometry = display.get_screen_geometry(screen)
335+ if geometry[0] < l:
336+ l = geometry[0]
337+ if geometry[1] < t:
338+ t = geometry[1]
339+ if geometry[0] + geometry[2] > r:
340+ r = geometry[0] + geometry[2]
341+ if geometry[1] + geometry[3] > b:
342+ b = geometry[1] + geometry[3]
343+ res_x = r - l
344+ res_y = b - t
345+ return res_x, res_y
346+
347+
348+def _get_touch_tool():
349+ # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have
350+ # no idea why...
351+ if autopilot.platform.model() == 'Desktop':
352+ touch_tool = e.BTN_TOUCH
353+ else:
354+ touch_tool = e.BTN_TOOL_FINGER
355+ return touch_tool
356+
357+
358+class _UInputTouchDevice(object):
359+ """Wrapper for the UInput Touch to execute its primitives.
360+
361+ If res_x and res_y are not specified, they will be queried from the system.
362+
363+ """
364+
365+ _touch_fingers_in_use = []
366+ _max_number_of_fingers = 9
367+ _last_tracking_id = 0
368+
369+ def __init__(self, res_x=None, res_y=None, device_class=UInput):
370+ super(_UInputTouchDevice, self).__init__()
371+ self._device = device_class(
372+ events=_get_touch_events(res_x, res_y), name='autopilot-finger',
373+ version=0x2, devnode=_get_devnode_path())
374+ self._touch_finger_slot = None
375+
376+ @property
377+ def pressed(self):
378+ return self._touch_finger_slot is not None
379+
380+ def finger_down(self, x, y):
381+ """Internal: moves finger "finger" down on the touchscreen.
382+
383+ :param x: The finger will be moved to this x coordinate.
384+ :param y: The finger will be moved to this y coordinate.
385+
386+ """
387+ if self.pressed:
388+ raise RuntimeError("Cannot press finger: it's already pressed.")
389+ self._touch_finger_slot = self._get_free_touch_finger_slot()
390+
391+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
392+ self._device.write(
393+ e.EV_ABS, e.ABS_MT_TRACKING_ID, self._get_next_tracking_id())
394+ press_value = 1
395+ self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, press_value)
396+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
397+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
398+ self._device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
399+ self._device.syn()
400+
401+ def _get_free_touch_finger_slot(self):
402+ """Return the id of a free touch finger.
403+
404+ :raises: RuntimeError if no more fingers are available.
405+
406+ """
407+ for i in range(_UInputTouchDevice._max_number_of_fingers):
408+ if i not in _UInputTouchDevice._touch_fingers_in_use:
409+ _UInputTouchDevice._touch_fingers_in_use.append(i)
410+ return i
411+ raise RuntimeError('All available fingers have been used already.')
412+
413+ def _get_next_tracking_id(self):
414+ _UInputTouchDevice._last_tracking_id += 1
415+ return _UInputTouchDevice._last_tracking_id
416+
417+ def finger_move(self, x, y):
418+ """Internal: moves finger "finger" on the touchscreen to pos (x,y)
419+
420+ NOTE: The finger has to be down for this to have any effect.
421+
422+ """
423+ if not self.pressed:
424+ raise RuntimeError('Attempting to move without finger being down.')
425+ else:
426+ self._device.write(
427+ e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
428+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
429+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
430+ self._device.syn()
431+
432+ def finger_up(self):
433+ """Internal: moves finger "finger" up from the touchscreen"""
434+ if not self.pressed:
435+ raise RuntimeError("Cannot release finger: it's not pressed.")
436+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
437+ lift_tracking_id = -1
438+ self._device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, lift_tracking_id)
439+ release_value = 0
440+ self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, release_value)
441+ self._device.syn()
442+ self._release_touch_finger()
443+
444+ def _release_touch_finger(self):
445+ """Release the touch finger."""
446+ if (self._touch_finger_slot not in
447+ _UInputTouchDevice._touch_fingers_in_use):
448+ raise RuntimeError(
449+ "Finger %d was never claimed, or has already been released." %
450+ self._touch_finger_slot)
451+ _UInputTouchDevice._touch_fingers_in_use.remove(
452+ self._touch_finger_slot)
453+ self._touch_finger_slot = None
454
455
456 class Touch(TouchBase):
457 """Low level interface to generate single finger touch events."""
458
459- def __init__(self):
460+ def __init__(self, device_class=_UInputTouchDevice):
461 super(Touch, self).__init__()
462- self._touch_finger = None
463+ Touch._device = device_class()
464
465 @property
466 def pressed(self):
467- return self._touch_finger is not None
468+ return self._device.pressed
469
470 def tap(self, x, y):
471 """Click (or 'tap') at given x and y coordinates."""
472 logger.debug("Tapping at: %d,%d", x, y)
473- self._finger_down(x, y)
474- sleep(0.1)
475- self._finger_up()
476+ self._device.finger_down(x, y)
477+ utilities.sleep(0.1)
478+ self._device.finger_up()
479
480 def tap_object(self, object):
481 """Click (or 'tap') a given object"""
482@@ -344,12 +451,12 @@
483 """Press and hold a given object or at the given coordinates
484 Call release() when the object has been pressed long enough"""
485 logger.debug("Pressing at: %d,%d", x, y)
486- self._finger_down(x, y)
487+ self._device.finger_down(x, y)
488
489 def release(self):
490 """Release a previously pressed finger"""
491 logger.debug("Releasing")
492- self._finger_up()
493+ self._device.finger_up()
494
495 def move(self, x, y):
496 """Moves the pointing "finger" to pos(x,y).
497@@ -357,14 +464,12 @@
498 NOTE: The finger has to be down for this to have any effect.
499
500 """
501- if self._touch_finger is None:
502- raise RuntimeError("Attempting to move without finger being down.")
503- self._finger_move(x, y)
504+ self._device.finger_move(x, y)
505
506 def drag(self, x1, y1, x2, y2):
507 """Perform a drag gesture from (x1,y1) to (x2,y2)"""
508 logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)
509- self._finger_down(x1, y1)
510+ self._device.finger_down(x1, y1)
511
512 # Let's drag in 100 steps for now...
513 dx = 1.0 * (x2 - x1) / 100
514@@ -372,52 +477,13 @@
515 cur_x = x1 + dx
516 cur_y = y1 + dy
517 for i in range(0, 100):
518- self._finger_move(int(cur_x), int(cur_y))
519- sleep(0.002)
520+ self._device.finger_move(int(cur_x), int(cur_y))
521+ utilities.sleep(0.002)
522 cur_x += dx
523 cur_y += dy
524 # Make sure we actually end up at target
525- self._finger_move(x2, y2)
526- self._finger_up()
527-
528- def _finger_down(self, x, y):
529- """Internal: moves finger "finger" down on the touchscreen.
530-
531- :param x: The finger will be moved to this x coordinate.
532- :param y: The finger will be moved to this y coordinate.
533-
534- """
535- if self._touch_finger is not None:
536- raise RuntimeError("Cannot press finger: it's already pressed.")
537- self._touch_finger = _get_touch_finger()
538-
539- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
540- _touch_device.write(
541- e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())
542- _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)
543- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
544- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
545- _touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
546- _touch_device.syn()
547-
548- def _finger_move(self, x, y):
549- """Internal: moves finger "finger" on the touchscreen to pos (x,y)
550- NOTE: The finger has to be down for this to have any effect."""
551- if self._touch_finger is not None:
552- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
553- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
554- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
555- _touch_device.syn()
556-
557- def _finger_up(self):
558- """Internal: moves finger "finger" up from the touchscreen"""
559- if self._touch_finger is None:
560- raise RuntimeError("Cannot release finger: it's not pressed.")
561- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
562- _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
563- _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)
564- _touch_device.syn()
565- self._touch_finger = _release_touch_finger(self._touch_finger)
566+ self._device.finger_move(x2, y2)
567+ self._device.finger_up()
568
569
570 # veebers: there should be a better way to handle this.
571
572=== modified file 'autopilot/tests/functional/test_input_stack.py'
573--- autopilot/tests/functional/test_input_stack.py 2013-12-16 00:20:40 +0000
574+++ autopilot/tests/functional/test_input_stack.py 2014-01-21 22:20:18 +0000
575@@ -1,7 +1,7 @@
576 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
577 #
578 # Autopilot Functional Test Tool
579-# Copyright (C) 2012-2013 Canonical
580+# Copyright (C) 2012, 2013, 2014 Canonical
581 #
582 # This program is free software: you can redistribute it and/or modify
583 # it under the terms of the GNU General Public License as published by
584@@ -150,8 +150,8 @@
585 from autopilot.input._X11 import _PRESSED_KEYS
586 return _PRESSED_KEYS
587 elif self.backend == 'UInput':
588- from autopilot.input._uinput import _PRESSED_KEYS
589- return _PRESSED_KEYS
590+ from autopilot.input import _uinput
591+ return _uinput.Keyboard._device._pressed_keys_ecodes
592 else:
593 self.fail("Don't know how to get pressed keys list for backend "
594 + self.backend
595@@ -551,8 +551,9 @@
596 test_result = FakeTestCase("test_press_key").run()
597
598 self.assertThat(test_result.wasSuccessful(), Equals(True))
599- from autopilot.input._uinput import _PRESSED_KEYS
600- self.assertThat(_PRESSED_KEYS, Equals([]))
601+ from autopilot.input import _uinput
602+ self.assertThat(
603+ _uinput.Keyboard._device._pressed_keys_ecodes, Equals([]))
604
605 @patch('autopilot.input._X11.fake_input', new=lambda *args: None, )
606 def test_mouse_button_released(self):
607
608=== modified file 'autopilot/tests/unit/test_input.py'
609--- autopilot/tests/unit/test_input.py 2013-12-10 03:10:11 +0000
610+++ autopilot/tests/unit/test_input.py 2014-01-21 22:20:18 +0000
611@@ -1,7 +1,7 @@
612 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
613 #
614 # Autopilot Functional Test Tool
615-# Copyright (C) 2013 Canonical
616+# Copyright (C) 2013, 2014 Canonical
617 #
618 # This program is free software: you can redistribute it and/or modify
619 # it under the terms of the GNU General Public License as published by
620@@ -17,11 +17,15 @@
621 # along with this program. If not, see <http://www.gnu.org/licenses/>.
622 #
623
624-from mock import patch
625+import mock
626+import testscenarios
627+from evdev import ecodes, uinput
628 from testtools import TestCase
629 from testtools.matchers import raises
630
631+from autopilot.input import _uinput
632 from autopilot.input._common import get_center_point
633+from autopilot import utilities
634
635
636 class Empty(object):
637@@ -84,7 +88,7 @@
638 raises(expected_exception)
639 )
640
641- @patch('autopilot.input._common.logger')
642+ @mock.patch('autopilot.input._common.logger')
643 def test_get_center_point_logs_with_globalRect(self, mock_logger):
644 obj = make_fake_object(globalRect=True)
645 x, y = get_center_point(obj)
646@@ -100,7 +104,7 @@
647 self.assertEqual(123, x)
648 self.assertEqual(345, y)
649
650- @patch('autopilot.input._common.logger')
651+ @mock.patch('autopilot.input._common.logger')
652 def test_get_center_point_logs_with_center_points(self, mock_logger):
653 obj = make_fake_object(center=True)
654 x, y = get_center_point(obj)
655@@ -116,7 +120,7 @@
656 self.assertEqual(110, x)
657 self.assertEqual(120, y)
658
659- @patch('autopilot.input._common.logger')
660+ @mock.patch('autopilot.input._common.logger')
661 def test_get_center_point_logs_with_xywh(self, mock_logger):
662 obj = make_fake_object(xywh=True)
663 x, y = get_center_point(obj)
664@@ -151,3 +155,459 @@
665
666 self.assertEqual(123, x)
667 self.assertEqual(345, y)
668+
669+
670+class UInputKeyboardDeviceTestCase(TestCase):
671+ """Test the integration with evdev.UInput for the keyboard."""
672+
673+ _PRESS_VALUE = 1
674+ _RELEASE_VALUE = 0
675+
676+ def setUp(self):
677+ super(UInputKeyboardDeviceTestCase, self).setUp()
678+ self.keyboard = _uinput._UInputKeyboardDevice(device_class=mock.Mock)
679+ self.keyboard._device.mock_add_spec(uinput.UInput, spec_set=True)
680+
681+ def test_press_key_should_emit_write_and_syn(self):
682+ self.keyboard.press('KEY_A')
683+ self._assert_key_press_emitted_write_and_syn('KEY_A')
684+
685+ def _assert_key_press_emitted_write_and_syn(self, key):
686+ self._assert_emitted_write_and_syn(key, self._PRESS_VALUE)
687+
688+ def _assert_emitted_write_and_syn(self, key, value):
689+ key_ecode = ecodes.ecodes.get(key)
690+ expected_calls = [
691+ mock.call.write(ecodes.EV_KEY, key_ecode, value),
692+ mock.call.syn()
693+ ]
694+
695+ self.assertEqual(expected_calls, self.keyboard._device.mock_calls)
696+
697+ def test_press_key_should_append_leading_string(self):
698+ self.keyboard.press('A')
699+ self._assert_key_press_emitted_write_and_syn('KEY_A')
700+
701+ def test_press_key_should_ignore_case(self):
702+ self.keyboard.press('a')
703+ self._assert_key_press_emitted_write_and_syn('KEY_A')
704+
705+ def test_press_unexisting_key_should_raise_error(self):
706+ error = self.assertRaises(
707+ ValueError, self.keyboard.press, 'unexisting')
708+
709+ self.assertEqual('Unknown key name: unexisting.', str(error))
710+
711+ def test_release_not_pressed_key_should_raise_error(self):
712+ error = self.assertRaises(
713+ ValueError, self.keyboard.release, 'A')
714+
715+ self.assertEqual("Key 'A' not pressed.", str(error))
716+
717+ def test_release_key_should_emit_write_and_syn(self):
718+ self._press_key_and_reset_mock('KEY_A')
719+
720+ self.keyboard.release('KEY_A')
721+ self._assert_key_release_emitted_write_and_syn('KEY_A')
722+
723+ def _press_key_and_reset_mock(self, key):
724+ self.keyboard.press(key)
725+ self.keyboard._device.reset_mock()
726+
727+ def _assert_key_release_emitted_write_and_syn(self, key):
728+ self._assert_emitted_write_and_syn(key, self._RELEASE_VALUE)
729+
730+ def test_release_key_should_append_leading_string(self):
731+ self._press_key_and_reset_mock('KEY_A')
732+
733+ self.keyboard.release('A')
734+ self._assert_key_release_emitted_write_and_syn('KEY_A')
735+
736+ def test_release_key_should_ignore_case(self):
737+ self._press_key_and_reset_mock('KEY_A')
738+
739+ self.keyboard.release('a')
740+ self._assert_key_release_emitted_write_and_syn('KEY_A')
741+
742+ def test_release_unexisting_key_should_raise_error(self):
743+ error = self.assertRaises(
744+ ValueError, self.keyboard.release, 'unexisting')
745+
746+ self.assertEqual('Unknown key name: unexisting.', str(error))
747+
748+ def test_release_pressed_keys_without_pressed_keys_should_do_nothing(self):
749+ self.keyboard.release_pressed_keys()
750+ self.assertEqual([], self.keyboard._device.mock_calls)
751+
752+ def test_release_pressed_keys_with_pressed_keys(self):
753+ expected_calls = [
754+ mock.call.write(
755+ ecodes.EV_KEY, ecodes.ecodes.get('KEY_A'),
756+ self._RELEASE_VALUE),
757+ mock.call.syn(),
758+ mock.call.write(
759+ ecodes.EV_KEY, ecodes.ecodes.get('KEY_B'),
760+ self._RELEASE_VALUE),
761+ mock.call.syn()
762+ ]
763+
764+ self._press_key_and_reset_mock('KEY_A')
765+ self._press_key_and_reset_mock('KEY_B')
766+
767+ self.keyboard.release_pressed_keys()
768+
769+ self.assertEqual(expected_calls, self.keyboard._device.mock_calls)
770+
771+ def test_release_pressed_keys_already_released(self):
772+ expected_calls = []
773+ self.keyboard.press('KEY_A')
774+ self.keyboard.release_pressed_keys()
775+ self.keyboard._device.reset_mock()
776+
777+ self.keyboard.release_pressed_keys()
778+ self.assertEqual(expected_calls, self.keyboard._device.mock_calls)
779+
780+
781+class UInputKeyboardTestCase(testscenarios.TestWithScenarios, TestCase):
782+ """Test UInput Keyboard helper for autopilot tests."""
783+
784+ scenarios = [
785+ ('single key', dict(keys='a', expected_calls_args=['a'])),
786+ ('upper-case letter', dict(
787+ keys='A', expected_calls_args=['KEY_LEFTSHIFT', 'A'])),
788+ ('key combination', dict(
789+ keys='a+b', expected_calls_args=['a', 'b']))
790+ ]
791+
792+ def setUp(self):
793+ super(UInputKeyboardTestCase, self).setUp()
794+ # Return to the original device after the test.
795+ self.addCleanup(self._set_keyboard_device, _uinput.Keyboard._device)
796+ _uinput.Keyboard._device = None
797+ self.keyboard = _uinput.Keyboard(device_class=mock.Mock)
798+ self.keyboard._device.mock_add_spec(
799+ _uinput._UInputKeyboardDevice, spec_set=True)
800+ # Mock the sleeps so we don't have to spend time actually sleeping.
801+ self.addCleanup(utilities.sleep.disable_mock)
802+ utilities.sleep.enable_mock()
803+
804+ self.keyboard._device.reset_mock()
805+
806+ def _set_keyboard_device(self, device):
807+ _uinput.Keyboard._device = device
808+
809+ def test_press(self):
810+ expected_calls = [
811+ mock.call.press(arg) for arg in self.expected_calls_args]
812+ self.keyboard.press(self.keys)
813+
814+ self.assertEqual(expected_calls, self.keyboard._device.mock_calls)
815+
816+ def test_release(self):
817+ self.keyboard.press(self.keys)
818+ self.keyboard._device.reset_mock()
819+
820+ expected_calls = [
821+ mock.call.release(arg) for arg in
822+ reversed(self.expected_calls_args)]
823+ self.keyboard.release(self.keys)
824+
825+ self.assertEqual(
826+ expected_calls, self.keyboard._device.mock_calls)
827+
828+ def test_press_and_release(self):
829+ expected_press_calls = [
830+ mock.call.press(arg) for arg in self.expected_calls_args]
831+ expected_release_calls = [
832+ mock.call.release(arg) for arg in
833+ reversed(self.expected_calls_args)]
834+
835+ self.keyboard.press_and_release(self.keys)
836+
837+ self.assertEqual(
838+ expected_press_calls + expected_release_calls,
839+ self.keyboard._device.mock_calls)
840+
841+
842+class UInputTouchDeviceTestCase(TestCase):
843+ """Test the integration with evdev.UInput for the touch device."""
844+
845+ def setUp(self):
846+ super(UInputTouchDeviceTestCase, self).setUp()
847+ self._number_of_slots = 9
848+
849+ # Return to the original fingers after the test.
850+ self.addCleanup(
851+ self._set_fingers_in_use,
852+ _uinput._UInputTouchDevice._touch_fingers_in_use,
853+ _uinput._UInputTouchDevice._last_tracking_id)
854+
855+ # Always start the tests without fingers in use.
856+ _uinput._UInputTouchDevice._touch_fingers_in_use = []
857+ _uinput._UInputTouchDevice._last_tracking_id = 0
858+
859+ def _set_fingers_in_use(self, touch_fingers_in_use, last_tracking_id):
860+ _uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
861+ _uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
862+
863+ def test_finger_down_should_use_free_slot(self):
864+ for slot in range(self._number_of_slots):
865+ touch = self._get_touch_device()
866+
867+ touch.finger_down(0, 0)
868+
869+ self._assert_finger_down_emitted_write_and_syn(
870+ touch, slot=slot, tracking_id=mock.ANY, x=0, y=0)
871+
872+ def _get_touch_device(self):
873+ dummy_x_resolution = 100
874+ dummy_y_resolution = 100
875+ touch = _uinput._UInputTouchDevice(
876+ res_x=dummy_x_resolution, res_y=dummy_y_resolution,
877+ device_class=mock.Mock)
878+ touch._device.mock_add_spec(uinput.UInput, spec_set=True)
879+ return touch
880+
881+ def _assert_finger_down_emitted_write_and_syn(
882+ self, touch, slot, tracking_id, x, y):
883+ press_value = 1
884+ expected_calls = [
885+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
886+ mock.call.write(
887+ ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, tracking_id),
888+ mock.call.write(
889+ ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, press_value),
890+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
891+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
892+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_PRESSURE, 400),
893+ mock.call.syn()
894+ ]
895+ self.assertEqual(expected_calls, touch._device.mock_calls)
896+
897+ def test_finger_down_without_free_slots_should_raise_error(self):
898+ # Claim all the available slots.
899+ for slot in range(self._number_of_slots):
900+ touch = self._get_touch_device()
901+ touch.finger_down(0, 0)
902+
903+ touch = self._get_touch_device()
904+
905+ # Try to use one more.
906+ error = self.assertRaises(RuntimeError, touch.finger_down, 11, 11)
907+ self.assertEqual(
908+ 'All available fingers have been used already.', str(error))
909+
910+ def test_finger_down_should_use_unique_tracking_id(self):
911+ for number in range(self._number_of_slots):
912+ touch = self._get_touch_device()
913+ touch.finger_down(0, 0)
914+
915+ self._assert_finger_down_emitted_write_and_syn(
916+ touch, slot=mock.ANY, tracking_id=number + 1, x=0, y=0)
917+
918+ def test_finger_down_should_not_reuse_tracking_ids(self):
919+ # Claim and release all the available slots once.
920+ for number in range(self._number_of_slots):
921+ touch = self._get_touch_device()
922+ touch.finger_down(0, 0)
923+ touch.finger_up()
924+
925+ touch = self._get_touch_device()
926+
927+ touch.finger_down(12, 12)
928+ self._assert_finger_down_emitted_write_and_syn(
929+ touch, slot=mock.ANY, tracking_id=number + 2, x=12, y=12)
930+
931+ def test_finger_down_with_finger_pressed_should_raise_error(self):
932+ touch = self._get_touch_device()
933+ touch.finger_down(0, 0)
934+
935+ error = self.assertRaises(RuntimeError, touch.finger_down, 0, 0)
936+ self.assertEqual(
937+ "Cannot press finger: it's already pressed.", str(error))
938+
939+ def test_finger_move_without_finger_pressed_should_raise_error(self):
940+ touch = self._get_touch_device()
941+
942+ error = self.assertRaises(RuntimeError, touch.finger_move, 10, 10)
943+ self.assertEqual(
944+ 'Attempting to move without finger being down.', str(error))
945+
946+ def test_finger_move_should_use_assigned_slot(self):
947+ for slot in range(self._number_of_slots):
948+ touch = self._get_touch_device()
949+ touch.finger_down(0, 0)
950+ touch._device.reset_mock()
951+
952+ touch.finger_move(10, 10)
953+
954+ self._assert_finger_move_emitted_write_and_syn(
955+ touch, slot=slot, x=10, y=10)
956+
957+ def _assert_finger_move_emitted_write_and_syn(self, touch, slot, x, y):
958+ expected_calls = [
959+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
960+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
961+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
962+ mock.call.syn()
963+ ]
964+ self.assertEqual(expected_calls, touch._device.mock_calls)
965+
966+ def test_finger_move_should_reuse_assigned_slot(self):
967+ first_slot = 0
968+ touch = self._get_touch_device()
969+ touch.finger_down(1, 1)
970+ touch._device.reset_mock()
971+
972+ touch.finger_move(13, 13)
973+ self._assert_finger_move_emitted_write_and_syn(
974+ touch, slot=first_slot, x=13, y=13)
975+ touch._device.reset_mock()
976+
977+ touch.finger_move(14, 14)
978+ self._assert_finger_move_emitted_write_and_syn(
979+ touch, slot=first_slot, x=14, y=14)
980+
981+ def test_finger_up_without_finger_pressed_should_raise_error(self):
982+ touch = self._get_touch_device()
983+
984+ error = self.assertRaises(RuntimeError, touch.finger_up)
985+ self.assertEqual(
986+ "Cannot release finger: it's not pressed.", str(error))
987+
988+ def test_finger_up_should_use_assigned_slot(self):
989+ fingers = []
990+ for slot in range(self._number_of_slots):
991+ touch = self._get_touch_device()
992+ touch.finger_down(0, 0)
993+ touch._device.reset_mock()
994+ fingers.append(touch)
995+
996+ for slot, touch in enumerate(fingers):
997+ touch.finger_up()
998+
999+ self._assert_finger_up_emitted_write_and_syn(touch, slot=slot)
1000+
1001+ def _assert_finger_up_emitted_write_and_syn(self, touch, slot):
1002+ lift_tracking_id = -1
1003+ release_value = 0
1004+ expected_calls = [
1005+ mock.call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
1006+ mock.call.write(
1007+ ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, lift_tracking_id),
1008+ mock.call.write(
1009+ ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, release_value),
1010+ mock.call.syn()
1011+ ]
1012+ self.assertEqual(expected_calls, touch._device.mock_calls)
1013+
1014+ def test_finger_up_should_release_slot(self):
1015+ fingers = []
1016+ # Claim all the available slots.
1017+ for slot in range(self._number_of_slots):
1018+ touch = self._get_touch_device()
1019+ touch.finger_down(0, 0)
1020+ fingers.append(touch)
1021+
1022+ slot_to_reuse = 3
1023+ fingers[slot_to_reuse].finger_up()
1024+
1025+ touch = self._get_touch_device()
1026+
1027+ # Try to use one more.
1028+ touch.finger_down(15, 15)
1029+ self._assert_finger_down_emitted_write_and_syn(
1030+ touch, slot=slot_to_reuse, tracking_id=mock.ANY, x=15, y=15)
1031+
1032+ def test_pressed_with_finger_down(self):
1033+ touch = self._get_touch_device()
1034+ touch.finger_down(0, 0)
1035+
1036+ self.assertTrue(touch.pressed)
1037+
1038+ def test_pressed_without_finger_down(self):
1039+ touch = self._get_touch_device()
1040+ self.assertFalse(touch.pressed)
1041+
1042+ def test_pressed_after_finger_up(self):
1043+ touch = self._get_touch_device()
1044+ touch.finger_down(0, 0)
1045+ touch.finger_up()
1046+
1047+ self.assertFalse(touch.pressed)
1048+
1049+ def test_pressed_with_other_finger_down(self):
1050+ other_touch = self._get_touch_device()
1051+ other_touch.finger_down(0, 0)
1052+
1053+ touch = self._get_touch_device()
1054+ self.assertFalse(touch.pressed)
1055+
1056+
1057+class UInputTouchTestCase(TestCase):
1058+ """Test UInput Touch helper for autopilot tests."""
1059+
1060+ def setUp(self):
1061+ super(UInputTouchTestCase, self).setUp()
1062+ self.touch = _uinput.Touch(device_class=mock.Mock)
1063+ self.touch._device.mock_add_spec(
1064+ _uinput._UInputTouchDevice, spec_set=True)
1065+ # Mock the sleeps so we don't have to spend time actually sleeping.
1066+ self.addCleanup(utilities.sleep.disable_mock)
1067+ utilities.sleep.enable_mock()
1068+
1069+ def test_tap(self):
1070+ expected_calls = [
1071+ mock.call.finger_down(0, 0),
1072+ mock.call.finger_up()
1073+ ]
1074+
1075+ self.touch.tap(0, 0)
1076+ self.assertEqual(expected_calls, self.touch._device.mock_calls)
1077+
1078+ def test_tap_object(self):
1079+ object_ = type('Dummy', (object,), {'globalRect': (0, 0, 10, 10)})
1080+ expected_calls = [
1081+ mock.call.finger_down(5, 5),
1082+ mock.call.finger_up()
1083+ ]
1084+
1085+ self.touch.tap_object(object_)
1086+ self.assertEqual(expected_calls, self.touch._device.mock_calls)
1087+
1088+ def test_press(self):
1089+ expected_calls = [mock.call.finger_down(0, 0)]
1090+
1091+ self.touch.press(0, 0)
1092+ self.assertEqual(expected_calls, self.touch._device.mock_calls)
1093+
1094+ def test_release(self):
1095+ expected_calls = [mock.call.finger_up()]
1096+
1097+ self.touch.release()
1098+ self.assertEqual(expected_calls, self.touch._device.mock_calls)
1099+
1100+ def test_move(self):
1101+ expected_calls = [mock.call.finger_move(10, 10)]
1102+
1103+ self.touch.move(10, 10)
1104+ self.assertEqual(expected_calls, self.touch._device.mock_calls)
1105+
1106+
1107+class MultipleUInputTouchBackend(_uinput._UInputTouchDevice):
1108+
1109+ def __init__(self, res_x=100, res_y=100, device_class=mock.Mock):
1110+ super(MultipleUInputTouchBackend, self).__init__(
1111+ res_x, res_y, device_class)
1112+
1113+
1114+class MultipleUInputTouchTestCase(TestCase):
1115+
1116+ def test_with_multiple_touch(self):
1117+ finger1 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
1118+ finger2 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
1119+
1120+ finger1.press(0, 0)
1121+ self.addCleanup(finger1.release)
1122+
1123+ self.assertFalse(finger2.pressed)
1124
1125=== modified file 'debian/control'
1126--- debian/control 2014-01-21 13:02:00 +0000
1127+++ debian/control 2014-01-21 22:20:18 +0000
1128@@ -16,6 +16,7 @@
1129 python-dbus,
1130 python-debian,
1131 python-dev,
1132+ python-evdev,
1133 python-fixtures,
1134 python-gi,
1135 python-junitxml,
1136@@ -30,6 +31,7 @@
1137 python-xlib,
1138 python3-all-dev (>= 3.3),
1139 python3-dbus,
1140+ python3-evdev,
1141 python3-fixtures,
1142 python3-gi,
1143 python3-junitxml,

Subscribers

People subscribed via source and target branches