Merge lp:~elopio/autopilot/fix1257055-slow_drag into lp:autopilot

Proposed by Leo Arias
Status: Superseded
Proposed branch: lp:~elopio/autopilot/fix1257055-slow_drag
Merge into: lp:autopilot
Diff against target: 1650 lines (+1161/-233)
7 files modified
autopilot/input/_X11.py (+3/-3)
autopilot/input/__init__.py (+4/-4)
autopilot/input/_common.py (+12/-2)
autopilot/input/_uinput.py (+352/-216)
autopilot/tests/functional/test_input_stack.py (+6/-5)
autopilot/tests/unit/test_input.py (+782/-3)
debian/control (+2/-0)
To merge this branch: bzr merge lp:~elopio/autopilot/fix1257055-slow_drag
Reviewer Review Type Date Requested Status
Thomi Richards (community) Needs Fixing
PS Jenkins bot continuous-integration Needs Fixing
Leo Arias Pending
Review via email: mp+202205@code.launchpad.net

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

This proposal has been superseded by a proposal from 2014-02-11.

Commit message

Added Mouse, Touch and Pointer drags with rate.

To post a comment you must log in.
421. By Leo Arias

Updated the copyright years.

422. By Leo Arias

Added the rate to the Pointer drag.

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

Renamed MockTouch to MockUinputTouch.

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

Merged with trunk.

425. By Leo Arias

Added python-evdev as a build-dep.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :

Hi,

I'm not sure if this MP is still relevant. If it's not, please delete the proposal to merge. If it is, then:

39 - def drag(self, x1, y1, x2, y2):
40 + def drag(self, x1, y1, x2, y2, rate=10):

If you're adding (or removing, or changing) a parameter to a public method, please document it in the docstrings. What exactly is 'rate' measured in anyway?

82 - def drag(self, x1, y1, x2, y2):
83 + def drag(self, x1, y1, x2, y2, rate=10):

You introduce a new parameter, and then don't use it at all in the method.

In general this MP makes me feel rather uneasy. You're introducing a new parameter, and some code changes, and I can't see any justification for it.

If this is really needed, I think it might be worth moving the algorithm to a common location, and making both the X11 and uinput backends be able to use it. That way, you can test it without needing to mock out anything.

Anyway, please talk to me on IRC about this. We need to either clean it up and merge it, or remove it from the review queue.

review: Needs Fixing
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :

Hi,

In order to make our review queue a little more sane, I'm setting this to WIP. If/when you need a new review, please set it back to 'Needs Review', and (optionally) ping someone on the AP team.

Thanks.

426. By Leo Arias

Merged with prerequisite branch.

427. By Leo Arias

Fixed the imports.

428. By Leo Arias

Added comments for the rate parameter.

429. By Leo Arias

Updated the tests.

430. By Leo Arias

Removed unused import.

431. By Leo Arias

Removed extra line.

432. By Leo Arias

s/should/must

433. By Leo Arias

Added a test with time_between_events.

434. By Leo Arias

We can't use the real Mouse, so switch to touch backend for now.

435. By Leo Arias

Updated the fake.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/input/_X11.py'
2--- autopilot/input/_X11.py 2013-11-07 05:53:36 +0000
3+++ autopilot/input/_X11.py 2014-02-11 06:39:03 +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@@ -455,7 +455,7 @@
14 x, y = coord["root_x"], coord["root_y"]
15 return x, y
16
17- def drag(self, x1, y1, x2, y2):
18+ def drag(self, x1, y1, x2, y2, rate=10):
19 """Performs a press, move and release.
20
21 This is to keep a common API between Mouse and Finger as long as
22@@ -464,7 +464,7 @@
23 """
24 self.move(x1, y1)
25 self.press()
26- self.move(x2, y2)
27+ self.move(x2, y2, rate=rate)
28 self.release()
29
30 @classmethod
31
32=== modified file 'autopilot/input/__init__.py'
33--- autopilot/input/__init__.py 2013-09-20 19:01:27 +0000
34+++ autopilot/input/__init__.py 2014-02-11 06:39:03 +0000
35@@ -369,7 +369,7 @@
36 """
37 raise NotImplementedError("You cannot use this class directly.")
38
39- def drag(self, x1, y1, x2, y2):
40+ def drag(self, x1, y1, x2, y2, rate=10):
41 """Performs a press, move and release.
42
43 This is to keep a common API between Mouse and Finger as long as
44@@ -466,7 +466,7 @@
45 """Release a previously pressed finger"""
46 raise NotImplementedError("You cannot use this class directly.")
47
48- def drag(self, x1, y1, x2, y2):
49+ def drag(self, x1, y1, x2, y2, rate=10):
50 """Perform a drag gesture from (x1,y1) to (x2,y2)"""
51 raise NotImplementedError("You cannot use this class directly.")
52
53@@ -641,9 +641,9 @@
54 else:
55 return (self._x, self._y)
56
57- def drag(self, x1, y1, x2, y2):
58+ def drag(self, x1, y1, x2, y2, rate=10):
59 """Performs a press, move and release."""
60- self._device.drag(x1, y1, x2, y2)
61+ self._device.drag(x1, y1, x2, y2, rate=rate)
62 if isinstance(self._device, Touch):
63 self._x = x2
64 self._y = y2
65
66=== modified file 'autopilot/input/_common.py'
67--- autopilot/input/_common.py 2013-12-10 03:10:11 +0000
68+++ autopilot/input/_common.py 2014-02-11 06:39:03 +0000
69@@ -26,8 +26,18 @@
70
71
72 def get_center_point(object_proxy):
73- """Get the center point of an object, searching for several different ways
74- of determining exactly where the center is.
75+ """Get the center point of an object.
76+
77+ It searches for several different ways of determining exactly where the
78+ center is.
79+
80+ :raises ValueError: if `object_proxy` has the globalRect attribute but it
81+ is not of the correct type.
82+ :raises ValueError: if `object_proxy` doesn't have the globalRect
83+ attribute, it has the x and y attributes instead, but they are not of
84+ the correct type.
85+ :raises ValueError: if `object_proxy` doesn't have any recognised position
86+ attributes.
87
88 """
89 try:
90
91=== modified file 'autopilot/input/_uinput.py'
92--- autopilot/input/_uinput.py 2013-11-07 05:53:36 +0000
93+++ autopilot/input/_uinput.py 2014-02-11 06:39:03 +0000
94@@ -1,7 +1,7 @@
95 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
96 #
97 # Autopilot Functional Test Tool
98-# Copyright (C) 2012-2013 Canonical
99+# Copyright (C) 2012, 2013, 2014 Canonical
100 #
101 # This program is free software: you can redistribute it and/or modify
102 # it under the terms of the GNU General Public License as published by
103@@ -17,27 +17,23 @@
104 # along with this program. If not, see <http://www.gnu.org/licenses/>.
105 #
106
107-
108 """UInput device drivers."""
109
110+import logging
111+import os.path
112+
113+import six
114+from evdev import UInput, ecodes as e
115+
116+import autopilot.platform
117 from autopilot.input import Keyboard as KeyboardBase
118 from autopilot.input import Touch as TouchBase
119 from autopilot.input._common import get_center_point
120-from autopilot.utilities import sleep
121-import autopilot.platform
122+from autopilot.utilities import deprecated, sleep
123
124-import logging
125-from evdev import UInput, ecodes as e
126-import os.path
127-import six
128
129 logger = logging.getLogger(__name__)
130
131-PRESS = 1
132-RELEASE = 0
133-
134-_PRESSED_KEYS = []
135-
136
137 def _get_devnode_path():
138 """Provide a fallback uinput node for devices which don't support udev"""
139@@ -47,13 +43,78 @@
140 return devnode
141
142
143+class _UInputKeyboardDevice(object):
144+ """Wrapper for the UInput Keyboard to execute its primitives."""
145+
146+ def __init__(self, device_class=UInput):
147+ super(_UInputKeyboardDevice, self).__init__()
148+ self._device = device_class(devnode=_get_devnode_path())
149+ self._pressed_keys_ecodes = []
150+
151+ def press(self, key):
152+ """Press one key button.
153+
154+ It ignores case, so, for example, 'a' and 'A' are mapped to the same
155+ key.
156+
157+ """
158+ ecode = self._get_ecode_for_key(key)
159+ logger.debug('Pressing %s (%r).', key, ecode)
160+ self._emit_press_event(ecode)
161+ self._pressed_keys_ecodes.append(ecode)
162+
163+ def _get_ecode_for_key(self, key):
164+ key_name = key if key.startswith('KEY_') else 'KEY_' + key
165+ key_name = key_name.upper()
166+ ecode = e.ecodes.get(key_name, None)
167+ if ecode is None:
168+ raise ValueError('Unknown key name: %s.' % key)
169+ return ecode
170+
171+ def _emit_press_event(self, ecode):
172+ press_value = 1
173+ self._emit(ecode, press_value)
174+
175+ def _emit(self, ecode, value):
176+ self._device.write(e.EV_KEY, ecode, value)
177+ self._device.syn()
178+
179+ def release(self, key):
180+ """Release one key button.
181+
182+ It ignores case, so, for example, 'a' and 'A' are mapped to the same
183+ key.
184+
185+ :raises ValueError: if ``key`` is not pressed.
186+
187+ """
188+ ecode = self._get_ecode_for_key(key)
189+ if ecode in self._pressed_keys_ecodes:
190+ logger.debug('Releasing %s (%r).', key, ecode)
191+ self._emit_release_event(ecode)
192+ self._pressed_keys_ecodes.remove(ecode)
193+ else:
194+ raise ValueError('Key %r not pressed.' % key)
195+
196+ def _emit_release_event(self, ecode):
197+ release_value = 0
198+ self._emit(ecode, release_value)
199+
200+ def release_pressed_keys(self):
201+ """Release all the keys that are currently pressed."""
202+ for ecode in self._pressed_keys_ecodes:
203+ self._emit_release_event(ecode)
204+ self._pressed_keys_ecodes = []
205+
206+
207 class Keyboard(KeyboardBase):
208
209- _device = UInput(devnode=_get_devnode_path())
210+ _device = None
211
212- def _emit(self, event, value):
213- Keyboard._device.write(e.EV_KEY, event, value)
214- Keyboard._device.syn()
215+ def __init__(self, device_class=_UInputKeyboardDevice):
216+ super(Keyboard, self).__init__()
217+ if Keyboard._device is None:
218+ Keyboard._device = device_class()
219
220 def _sanitise_keys(self, keys):
221 if keys == '+':
222@@ -71,15 +132,15 @@
223
224 presses the 'Alt' and 'F2' keys.
225
226+ :raises TypeError: if ``keys`` is not a string.
227+
228 """
229 if not isinstance(keys, six.string_types):
230 raise TypeError("'keys' argument must be a string.")
231
232 for key in self._sanitise_keys(keys):
233- for event in Keyboard._get_events_for_key(key):
234- logger.debug("Pressing %s (%r)", key, event)
235- _PRESSED_KEYS.append(event)
236- self._emit(event, PRESS)
237+ for key_button in self._get_key_buttons(key):
238+ self._device.press(key_button)
239 sleep(delay)
240
241 def release(self, keys, delay=0.1):
242@@ -94,16 +155,16 @@
243
244 Keys are released in the reverse order in which they are specified.
245
246+ :raises TypeError: if ``keys`` is not a string.
247+ :raises ValueError: if one of the keys to be released is not pressed.
248+
249 """
250 if not isinstance(keys, six.string_types):
251 raise TypeError("'keys' argument must be a string.")
252
253 for key in reversed(self._sanitise_keys(keys)):
254- for event in Keyboard._get_events_for_key(key):
255- logger.debug("Releasing %s (%r)", key, event)
256- if event in _PRESSED_KEYS:
257- _PRESSED_KEYS.remove(event)
258- self._emit(event, RELEASE)
259+ for key_button in reversed(self._get_key_buttons(key)):
260+ self._device.release(key_button)
261 sleep(delay)
262
263 def press_and_release(self, keys, delay=0.1):
264@@ -118,6 +179,8 @@
265
266 presses both the 'Alt' and 'F2' keys, and then releases both keys.
267
268+ :raises TypeError: if ``keys`` is not a string.
269+
270 """
271 logger.debug("Pressing and Releasing: %s", keys)
272 self.press(keys, delay)
273@@ -129,6 +192,8 @@
274 Only 'normal' keys can be typed with this method. Control characters
275 (such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').
276
277+ :raises TypeError: if ``keys`` is not a string.
278+
279 """
280 if not isinstance(string, six.string_types):
281 raise TypeError("'keys' argument must be a string.")
282@@ -145,98 +210,35 @@
283 any keys that were pressed and not released.
284
285 """
286- global _PRESSED_KEYS
287- if len(_PRESSED_KEYS) == 0:
288- return
289-
290- def _release(event):
291- Keyboard._device.write(e.EV_KEY, event, RELEASE)
292- Keyboard._device.syn()
293- for event in _PRESSED_KEYS:
294- logger.warning("Releasing key %r as part of cleanup call.", event)
295- _release(event)
296- _PRESSED_KEYS = []
297-
298- @staticmethod
299- def _get_events_for_key(key):
300- """Return a list of events required to generate 'key' as an input.
301-
302- Multiple keys will be returned when the key specified requires more
303+ if cls._device is not None:
304+ cls._device.release_pressed_keys()
305+
306+ def _get_key_buttons(self, key):
307+ """Return a list of the key buttons required to press.
308+
309+ Multiple buttons will be returned when the key specified requires more
310 than one keypress to generate (for example, upper-case letters).
311
312 """
313- events = []
314+ key_buttons = []
315 if key.isupper() or key in _SHIFTED_KEYS:
316- events.append(e.KEY_LEFTSHIFT)
317- keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
318- evt = getattr(e, 'KEY_' + keyname.upper(), None)
319- if evt is None:
320- raise ValueError("Unknown key name: '%s'" % key)
321- events.append(evt)
322- return events
323-
324-
325-last_tracking_id = 0
326-
327-
328-def get_next_tracking_id():
329- global last_tracking_id
330- last_tracking_id += 1
331- return last_tracking_id
332-
333-
334+ key_buttons.append('KEY_LEFTSHIFT')
335+ key_name = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
336+ key_buttons.append(key_name)
337+ return key_buttons
338+
339+
340+@deprecated('the Touch class to instantiate a device object')
341 def create_touch_device(res_x=None, res_y=None):
342 """Create and return a UInput touch device.
343
344 If res_x and res_y are not specified, they will be queried from the system.
345
346 """
347-
348- if res_x is None or res_y is None:
349- from autopilot.display import Display
350- display = Display.create()
351- # TODO: This calculation needs to become part of the display module:
352- l = r = t = b = 0
353- for screen in range(display.get_num_screens()):
354- geometry = display.get_screen_geometry(screen)
355- if geometry[0] < l:
356- l = geometry[0]
357- if geometry[1] < t:
358- t = geometry[1]
359- if geometry[0] + geometry[2] > r:
360- r = geometry[0] + geometry[2]
361- if geometry[1] + geometry[3] > b:
362- b = geometry[1] + geometry[3]
363- res_x = r - l
364- res_y = b - t
365-
366- # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no
367- # idea why...
368- touch_tool = e.BTN_TOOL_FINGER
369- if autopilot.platform.model() == 'Desktop':
370- touch_tool = e.BTN_TOUCH
371-
372- cap_mt = {
373- e.EV_ABS: [
374- (e.ABS_X, (0, res_x, 0, 0)),
375- (e.ABS_Y, (0, res_y, 0, 0)),
376- (e.ABS_PRESSURE, (0, 65535, 0, 0)),
377- (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
378- (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
379- (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
380- (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
381- (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
382- (e.ABS_MT_SLOT, (0, 9, 0, 0)),
383- ],
384- e.EV_KEY: [
385- touch_tool,
386- ]
387- }
388-
389- return UInput(cap_mt, name='autopilot-finger', version=0x2,
390- devnode=_get_devnode_path())
391-
392-_touch_device = create_touch_device()
393+ return UInput(events=_get_touch_events(res_x, res_y),
394+ name='autopilot-finger',
395+ version=0x2, devnode=_get_devnode_path())
396+
397
398 # Multiouch notes:
399 # ----------------
400@@ -281,143 +283,277 @@
401 # about this is that the SLOT refers to a finger number, and the TRACKING_ID
402 # identifies a unique touch for the duration of it's existance.
403
404-_touch_fingers_in_use = []
405-
406-
407-def _get_touch_finger():
408- """Claim a touch finger id for use.
409-
410- :raises: RuntimeError if no more fingers are available.
411-
412- """
413- global _touch_fingers_in_use
414-
415- for i in range(9):
416- if i not in _touch_fingers_in_use:
417- _touch_fingers_in_use.append(i)
418- return i
419- raise RuntimeError("All available fingers have been used already.")
420-
421-
422-def _release_touch_finger(finger_num):
423- """Relase a previously-claimed finger id.
424-
425- :raises: RuntimeError if the finger given was never claimed, or was already
426- released.
427-
428- """
429- global _touch_fingers_in_use
430-
431- if finger_num not in _touch_fingers_in_use:
432- raise RuntimeError(
433- "Finger %d was never claimed, or has already been released." %
434- (finger_num))
435- _touch_fingers_in_use.remove(finger_num)
436- assert(finger_num not in _touch_fingers_in_use)
437+
438+def _get_touch_events(res_x=None, res_y=None):
439+ if res_x is None or res_y is None:
440+ res_x, res_y = _get_system_resolution()
441+
442+ touch_tool = _get_touch_tool()
443+
444+ events = {
445+ e.EV_ABS: [
446+ (e.ABS_X, (0, res_x, 0, 0)),
447+ (e.ABS_Y, (0, res_y, 0, 0)),
448+ (e.ABS_PRESSURE, (0, 65535, 0, 0)),
449+ (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
450+ (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
451+ (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
452+ (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
453+ (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
454+ (e.ABS_MT_SLOT, (0, 9, 0, 0)),
455+ ],
456+ e.EV_KEY: [
457+ touch_tool,
458+ ]
459+ }
460+ return events
461+
462+
463+def _get_system_resolution():
464+ from autopilot.display import Display
465+ display = Display.create()
466+ # TODO: This calculation needs to become part of the display module:
467+ l = r = t = b = 0
468+ for screen in range(display.get_num_screens()):
469+ geometry = display.get_screen_geometry(screen)
470+ if geometry[0] < l:
471+ l = geometry[0]
472+ if geometry[1] < t:
473+ t = geometry[1]
474+ if geometry[0] + geometry[2] > r:
475+ r = geometry[0] + geometry[2]
476+ if geometry[1] + geometry[3] > b:
477+ b = geometry[1] + geometry[3]
478+ res_x = r - l
479+ res_y = b - t
480+ return res_x, res_y
481+
482+
483+def _get_touch_tool():
484+ # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have
485+ # no idea why...
486+ if autopilot.platform.model() == 'Desktop':
487+ touch_tool = e.BTN_TOUCH
488+ else:
489+ touch_tool = e.BTN_TOOL_FINGER
490+ return touch_tool
491+
492+
493+class _UInputTouchDevice(object):
494+ """Wrapper for the UInput Touch to execute its primitives."""
495+
496+ _device = None
497+ _touch_fingers_in_use = []
498+ _last_tracking_id = 0
499+
500+ def __init__(self, res_x=None, res_y=None, device_class=UInput):
501+ """Class constructor.
502+
503+ If res_x and res_y are not specified, they will be queried from the
504+ system.
505+
506+ """
507+ super(_UInputTouchDevice, self).__init__()
508+ if _UInputTouchDevice._device is None:
509+ _UInputTouchDevice._device = device_class(
510+ events=_get_touch_events(res_x, res_y),
511+ name='autopilot-finger',
512+ version=0x2, devnode=_get_devnode_path())
513+ self._touch_finger_slot = None
514+
515+ @property
516+ def pressed(self):
517+ return self._touch_finger_slot is not None
518+
519+ def finger_down(self, x, y):
520+ """Internal: moves finger "finger" down on the touchscreen.
521+
522+ :param x: The finger will be moved to this x coordinate.
523+ :param y: The finger will be moved to this y coordinate.
524+
525+ :raises RuntimeError: if the finger is already pressed.
526+ :raises RuntimeError: if no more touch slots are available.
527+
528+ """
529+ if self.pressed:
530+ raise RuntimeError("Cannot press finger: it's already pressed.")
531+ self._touch_finger_slot = self._get_free_touch_finger_slot()
532+
533+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
534+ self._device.write(
535+ e.EV_ABS, e.ABS_MT_TRACKING_ID, self._get_next_tracking_id())
536+ press_value = 1
537+ self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, press_value)
538+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
539+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
540+ self._device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
541+ self._device.syn()
542+
543+ def _get_free_touch_finger_slot(self):
544+ """Return the id of a free touch finger.
545+
546+ :raises RuntimeError: if no more touch slots are available.
547+
548+ """
549+ max_number_of_fingers = 9
550+ for i in range(max_number_of_fingers):
551+ if i not in _UInputTouchDevice._touch_fingers_in_use:
552+ _UInputTouchDevice._touch_fingers_in_use.append(i)
553+ return i
554+ raise RuntimeError('All available fingers have been used already.')
555+
556+ def _get_next_tracking_id(self):
557+ _UInputTouchDevice._last_tracking_id += 1
558+ return _UInputTouchDevice._last_tracking_id
559+
560+ def finger_move(self, x, y):
561+ """Internal: moves finger "finger" on the touchscreen to pos (x,y)
562+
563+ NOTE: The finger has to be down for this to have any effect.
564+
565+ :raises RuntimeError: if the finger is not pressed.
566+
567+ """
568+ if not self.pressed:
569+ raise RuntimeError('Attempting to move without finger being down.')
570+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
571+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
572+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
573+ self._device.syn()
574+
575+ def finger_up(self):
576+ """Internal: moves finger "finger" up from the touchscreen
577+
578+ :raises RuntimeError: if the finger is not pressed.
579+
580+ """
581+ if not self.pressed:
582+ raise RuntimeError("Cannot release finger: it's not pressed.")
583+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
584+ lift_tracking_id = -1
585+ self._device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, lift_tracking_id)
586+ release_value = 0
587+ self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, release_value)
588+ self._device.syn()
589+ self._release_touch_finger()
590+
591+ def _release_touch_finger(self):
592+ """Release the touch finger.
593+
594+ :raises RuntimeError: if the finger was not claimed before or was
595+ already released.
596+
597+ """
598+ if (self._touch_finger_slot not in
599+ _UInputTouchDevice._touch_fingers_in_use):
600+ raise RuntimeError(
601+ "Finger %d was never claimed, or has already been released." %
602+ self._touch_finger_slot)
603+ _UInputTouchDevice._touch_fingers_in_use.remove(
604+ self._touch_finger_slot)
605+ self._touch_finger_slot = None
606
607
608 class Touch(TouchBase):
609 """Low level interface to generate single finger touch events."""
610
611- def __init__(self):
612+ def __init__(self, device_class=_UInputTouchDevice):
613 super(Touch, self).__init__()
614- self._touch_finger = None
615+ self._device = device_class()
616
617 @property
618 def pressed(self):
619- return self._touch_finger is not None
620+ return self._device.pressed
621
622 def tap(self, x, y):
623- """Click (or 'tap') at given x and y coordinates."""
624+ """Click (or 'tap') at given x and y coordinates.
625+
626+ :raises RuntimeError: if the finger is already pressed.
627+ :raises RuntimeError: if no more finger slots are available.
628+
629+ """
630 logger.debug("Tapping at: %d,%d", x, y)
631- self._finger_down(x, y)
632+ self._device.finger_down(x, y)
633 sleep(0.1)
634- self._finger_up()
635-
636- def tap_object(self, object):
637- """Click (or 'tap') a given object"""
638+ self._device.finger_up()
639+
640+ def tap_object(self, object_):
641+ """Click (or 'tap') a given object.
642+
643+ :raises RuntimeError: if the finger is already pressed.
644+ :raises RuntimeError: if no more finger slots are available.
645+ :raises ValueError: if `object_` doesn't have any recognised position
646+ attributes or if they are not of the correct type.
647+
648+ """
649 logger.debug("Tapping object: %r", object)
650- x, y = get_center_point(object)
651+ x, y = get_center_point(object_)
652 self.tap(x, y)
653
654 def press(self, x, y):
655- """Press and hold a given object or at the given coordinates
656- Call release() when the object has been pressed long enough"""
657+ """Press and hold a given object or at the given coordinates.
658+
659+ Call release() when the object has been pressed long enough.
660+
661+ :raises RuntimeError: if the finger is already pressed.
662+ :raises RuntimeError: if no more finger slots are available.
663+
664+ """
665 logger.debug("Pressing at: %d,%d", x, y)
666- self._finger_down(x, y)
667+ self._device.finger_down(x, y)
668
669 def release(self):
670- """Release a previously pressed finger"""
671+ """Release a previously pressed finger.
672+
673+ :raises RuntimeError: if the touch is not pressed.
674+
675+ """
676 logger.debug("Releasing")
677- self._finger_up()
678+ self._device.finger_up()
679
680 def move(self, x, y):
681 """Moves the pointing "finger" to pos(x,y).
682
683 NOTE: The finger has to be down for this to have any effect.
684
685- """
686- if self._touch_finger is None:
687- raise RuntimeError("Attempting to move without finger being down.")
688- self._finger_move(x, y)
689-
690- def drag(self, x1, y1, x2, y2):
691- """Perform a drag gesture from (x1,y1) to (x2,y2)"""
692+ :raises RuntimeError: if the finger is not pressed.
693+
694+ """
695+ self._device.finger_move(x, y)
696+
697+ def drag(self, x1, y1, x2, y2, rate=10):
698+ """Perform a drag gesture from (x1,y1) to (x2,y2).
699+
700+ :raises RuntimeError: if the finger is already pressed.
701+ :raises RuntimeError: if no more finger slots are available.
702+
703+ """
704 logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)
705- self._finger_down(x1, y1)
706-
707- # Let's drag in 100 steps for now...
708- dx = 1.0 * (x2 - x1) / 100
709- dy = 1.0 * (y2 - y1) / 100
710- cur_x = x1 + dx
711- cur_y = y1 + dy
712- for i in range(0, 100):
713- self._finger_move(int(cur_x), int(cur_y))
714+ self._device.finger_down(x1, y1)
715+
716+ current_x, current_y = x1, y1
717+ while current_x != x2 or current_y != y2:
718+ dx = abs(x2 - current_x)
719+ dy = abs(y2 - current_y)
720+
721+ intx = float(dx) / max(dx, dy)
722+ inty = float(dy) / max(dx, dy)
723+
724+ step_x = min(rate * intx, dx)
725+ step_y = min(rate * inty, dy)
726+
727+ if x2 < current_x:
728+ step_x *= -1
729+ if y2 < current_y:
730+ step_y *= -1
731+
732+ current_x += step_x
733+ current_y += step_y
734+ self.device._finger_move(current_x, current_y)
735+
736 sleep(0.002)
737- cur_x += dx
738- cur_y += dy
739- # Make sure we actually end up at target
740- self._finger_move(x2, y2)
741- self._finger_up()
742-
743- def _finger_down(self, x, y):
744- """Internal: moves finger "finger" down on the touchscreen.
745-
746- :param x: The finger will be moved to this x coordinate.
747- :param y: The finger will be moved to this y coordinate.
748-
749- """
750- if self._touch_finger is not None:
751- raise RuntimeError("Cannot press finger: it's already pressed.")
752- self._touch_finger = _get_touch_finger()
753-
754- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
755- _touch_device.write(
756- e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())
757- _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)
758- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
759- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
760- _touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
761- _touch_device.syn()
762-
763- def _finger_move(self, x, y):
764- """Internal: moves finger "finger" on the touchscreen to pos (x,y)
765- NOTE: The finger has to be down for this to have any effect."""
766- if self._touch_finger is not None:
767- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
768- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
769- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
770- _touch_device.syn()
771-
772- def _finger_up(self):
773- """Internal: moves finger "finger" up from the touchscreen"""
774- if self._touch_finger is None:
775- raise RuntimeError("Cannot release finger: it's not pressed.")
776- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
777- _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
778- _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)
779- _touch_device.syn()
780- self._touch_finger = _release_touch_finger(self._touch_finger)
781+
782+ self.device._finger_up()
783
784
785 # veebers: there should be a better way to handle this.
786
787=== modified file 'autopilot/tests/functional/test_input_stack.py'
788--- autopilot/tests/functional/test_input_stack.py 2013-12-16 00:20:40 +0000
789+++ autopilot/tests/functional/test_input_stack.py 2014-02-11 06:39:03 +0000
790@@ -1,7 +1,7 @@
791 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
792 #
793 # Autopilot Functional Test Tool
794-# Copyright (C) 2012-2013 Canonical
795+# Copyright (C) 2012, 2013, 2014 Canonical
796 #
797 # This program is free software: you can redistribute it and/or modify
798 # it under the terms of the GNU General Public License as published by
799@@ -150,8 +150,8 @@
800 from autopilot.input._X11 import _PRESSED_KEYS
801 return _PRESSED_KEYS
802 elif self.backend == 'UInput':
803- from autopilot.input._uinput import _PRESSED_KEYS
804- return _PRESSED_KEYS
805+ from autopilot.input import _uinput
806+ return _uinput.Keyboard._device._pressed_keys_ecodes
807 else:
808 self.fail("Don't know how to get pressed keys list for backend "
809 + self.backend
810@@ -551,8 +551,9 @@
811 test_result = FakeTestCase("test_press_key").run()
812
813 self.assertThat(test_result.wasSuccessful(), Equals(True))
814- from autopilot.input._uinput import _PRESSED_KEYS
815- self.assertThat(_PRESSED_KEYS, Equals([]))
816+ from autopilot.input import _uinput
817+ self.assertThat(
818+ _uinput.Keyboard._device._pressed_keys_ecodes, Equals([]))
819
820 @patch('autopilot.input._X11.fake_input', new=lambda *args: None, )
821 def test_mouse_button_released(self):
822
823=== modified file 'autopilot/tests/unit/test_input.py'
824--- autopilot/tests/unit/test_input.py 2013-12-10 03:10:11 +0000
825+++ autopilot/tests/unit/test_input.py 2014-02-11 06:39:03 +0000
826@@ -1,7 +1,7 @@
827 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
828 #
829 # Autopilot Functional Test Tool
830-# Copyright (C) 2013 Canonical
831+# Copyright (C) 2013, 2014 Canonical
832 #
833 # This program is free software: you can redistribute it and/or modify
834 # it under the terms of the GNU General Public License as published by
835@@ -17,10 +17,18 @@
836 # along with this program. If not, see <http://www.gnu.org/licenses/>.
837 #
838
839-from mock import patch
840+import testscenarios
841+
842+from mock import call, Mock, patch
843+from evdev import ecodes, uinput
844+from mock import ANY, call, patch, Mock
845+from six import StringIO
846 from testtools import TestCase
847-from testtools.matchers import raises
848+from testtools.matchers import Contains, raises
849
850+import autopilot.input
851+from autopilot import utilities
852+from autopilot.input import _uinput, _X11
853 from autopilot.input._common import get_center_point
854
855
856@@ -151,3 +159,774 @@
857
858 self.assertEqual(123, x)
859 self.assertEqual(345, y)
860+
861+
862+class PartialMock(object):
863+ """Mock some of the methods of an object, and record their calls."""
864+
865+ def __init__(self, real_object, *args):
866+ super(PartialMock, self).__init__()
867+ self._mock_manager = Mock()
868+ self._real_object = real_object
869+ self.patched_attributes = args
870+
871+ def __getattr__(self, name):
872+ """Forward all the calls to the real object."""
873+ return self._real_object.__getattribute__(name)
874+
875+ @property
876+ def mock_calls(self):
877+ """Return the calls recorded for the mocked attributes."""
878+ return self._mock_manager.mock_calls
879+
880+ def __enter__(self):
881+ self._start_patchers()
882+ return self
883+
884+ def _start_patchers(self):
885+ self._patchers = []
886+ for attribute in self.patched_attributes:
887+ patcher = patch.object(self._real_object, attribute)
888+ self._patchers.append(patcher)
889+
890+ self._mock_manager.attach_mock(patcher.start(), attribute)
891+
892+ def __exit__(self, exc_type, exc_val, exc_tb):
893+ self._stop_patchers()
894+
895+ def _stop_patchers(self):
896+ for patcher in self._patchers:
897+ patcher.stop()
898+
899+
900+class MockX11Mouse(PartialMock):
901+ """Mock for the X11 Mouse Touch.
902+
903+ It records the calls to press, release and move, but doesn't perform them.
904+
905+ """
906+
907+ def __init__(self):
908+ super(MockX11Mouse, self).__init__(
909+ _X11.Mouse(), 'press', 'release', 'move')
910+
911+ def get_move_call_args_list(self):
912+ return self._mock_manager.move.call_args_list
913+
914+
915+class X11MouseTestCase(TestCase):
916+
917+ def test_drag_should_call_move_with_rate(self):
918+ expected_first_move_call = call(0, 0)
919+ expected_second_move_call = call(100, 100, rate=1)
920+ with MockX11Mouse() as mock_mouse:
921+ mock_mouse.drag(0, 0, 100, 100, rate=1)
922+
923+ self.assertEqual(
924+ [expected_first_move_call, expected_second_move_call],
925+ mock_mouse.get_move_call_args_list())
926+
927+ def test_drag_with_default_rate(self):
928+ expected_first_move_call = call(0, 0)
929+ expected_second_move_call = call(100, 100, rate=10)
930+ with MockX11Mouse() as mock_mouse:
931+ mock_mouse.drag(0, 0, 100, 100)
932+
933+ self.assertEqual(
934+ [expected_first_move_call, expected_second_move_call],
935+ mock_mouse.get_move_call_args_list())
936+
937+
938+class MockUinputTouch(PartialMock):
939+ """Mock for the uinput Touch.
940+
941+ It records the calls to _finger_down, _finger_up and _finger_move, but
942+ doesn't perform them.
943+
944+ """
945+
946+ def __init__(self):
947+ super(MockUinputTouch, self).__init__(
948+ _uinput.Touch(), '_finger_down', '_finger_up', '_finger_move')
949+
950+ def get_finger_move_call_args_list(self):
951+ return self._mock_manager._finger_move.call_args_list
952+
953+
954+class UinputTouchTestCase(TestCase):
955+
956+ def test_drag_finger_actions(self):
957+ expected_finger_calls = [
958+ call._finger_down(0, 0),
959+ call._finger_move(10, 10),
960+ call._finger_up()
961+ ]
962+ with MockUinputTouch() as mock_touch:
963+ mock_touch.drag(0, 0, 10, 10)
964+ self.assertEqual(mock_touch.mock_calls, expected_finger_calls)
965+
966+ def test_drag_should_call_move_with_rate(self):
967+ expected_move_calls = [call(5, 5), call(10, 10), call(15, 15)]
968+ with MockUinputTouch() as mock_touch:
969+ mock_touch.drag(0, 0, 15, 15, rate=5)
970+
971+ self.assertEqual(
972+ expected_move_calls, mock_touch.get_finger_move_call_args_list())
973+
974+ def test_drag_with_default_rate(self):
975+ expected_move_calls = [call(10, 10), call(20, 20)]
976+ with MockUinputTouch() as mock_touch:
977+ mock_touch.drag(0, 0, 20, 20)
978+
979+ self.assertEqual(
980+ expected_move_calls, mock_touch.get_finger_move_call_args_list())
981+
982+ def test_drag_to_same_place_should_not_move(self):
983+ expected_finger_calls = [
984+ call._finger_down(0, 0),
985+ call._finger_up()
986+ ]
987+ with MockUinputTouch() as mock_touch:
988+ mock_touch.drag(0, 0, 0, 0)
989+ self.assertEqual(mock_touch.mock_calls, expected_finger_calls)
990+
991+
992+class UInputTestCase(TestCase):
993+ """Tests for the global methods of the uinput module."""
994+
995+ def test_create_touch_device_must_print_deprecation_message(self):
996+ with patch('sys.stderr', new=StringIO()) as stderr:
997+ with patch('autopilot.input._uinput.UInput'):
998+ _uinput.create_touch_device('dummy', 'dummy')
999+ self.assertThat(
1000+ stderr.getvalue(),
1001+ Contains(
1002+ "This function is deprecated. Please use 'the Touch class to "
1003+ "instantiate a device object' instead."
1004+ )
1005+ )
1006+
1007+
1008+class UInputKeyboardDeviceTestCase(TestCase):
1009+ """Test the integration with evdev.UInput for the keyboard."""
1010+
1011+ _PRESS_VALUE = 1
1012+ _RELEASE_VALUE = 0
1013+
1014+ def get_keyboard_with_mocked_backend(self):
1015+ keyboard = _uinput._UInputKeyboardDevice(device_class=Mock)
1016+ keyboard._device.mock_add_spec(uinput.UInput, spec_set=True)
1017+ return keyboard
1018+
1019+ def assert_key_press_emitted_write_and_syn(self, keyboard, key):
1020+ self.assert_emitted_write_and_syn(keyboard, key, self._PRESS_VALUE)
1021+
1022+ def assert_key_release_emitted_write_and_syn(self, keyboard, key):
1023+ self.assert_emitted_write_and_syn(keyboard, key, self._RELEASE_VALUE)
1024+
1025+ def assert_emitted_write_and_syn(self, keyboard, key, value):
1026+ key_ecode = ecodes.ecodes.get(key)
1027+ expected_calls = [
1028+ call.write(ecodes.EV_KEY, key_ecode, value),
1029+ call.syn()
1030+ ]
1031+
1032+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
1033+
1034+ def press_key_and_reset_mock(self, keyboard, key):
1035+ keyboard.press(key)
1036+ keyboard._device.reset_mock()
1037+
1038+ def test_press_key_must_emit_write_and_syn(self):
1039+ keyboard = self.get_keyboard_with_mocked_backend()
1040+ keyboard.press('KEY_A')
1041+ self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
1042+
1043+ def test_press_key_must_append_leading_string(self):
1044+ keyboard = self.get_keyboard_with_mocked_backend()
1045+ keyboard.press('A')
1046+ self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
1047+
1048+ def test_press_key_must_ignore_case(self):
1049+ keyboard = self.get_keyboard_with_mocked_backend()
1050+ keyboard.press('a')
1051+ self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
1052+
1053+ def test_press_unexisting_key_must_raise_error(self):
1054+ keyboard = self.get_keyboard_with_mocked_backend()
1055+ error = self.assertRaises(
1056+ ValueError, keyboard.press, 'unexisting')
1057+
1058+ self.assertEqual('Unknown key name: unexisting.', str(error))
1059+
1060+ def test_release_not_pressed_key_must_raise_error(self):
1061+ keyboard = self.get_keyboard_with_mocked_backend()
1062+ error = self.assertRaises(
1063+ ValueError, keyboard.release, 'A')
1064+
1065+ self.assertEqual("Key 'A' not pressed.", str(error))
1066+
1067+ def test_release_key_must_emit_write_and_syn(self):
1068+ keyboard = self.get_keyboard_with_mocked_backend()
1069+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
1070+
1071+ keyboard.release('KEY_A')
1072+ self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
1073+
1074+ def test_release_key_must_append_leading_string(self):
1075+ keyboard = self.get_keyboard_with_mocked_backend()
1076+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
1077+
1078+ keyboard.release('A')
1079+ self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
1080+
1081+ def test_release_key_must_ignore_case(self):
1082+ keyboard = self.get_keyboard_with_mocked_backend()
1083+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
1084+
1085+ keyboard.release('a')
1086+ self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
1087+
1088+ def test_release_unexisting_key_must_raise_error(self):
1089+ keyboard = self.get_keyboard_with_mocked_backend()
1090+ error = self.assertRaises(
1091+ ValueError, keyboard.release, 'unexisting')
1092+
1093+ self.assertEqual('Unknown key name: unexisting.', str(error))
1094+
1095+ def test_release_pressed_keys_without_pressed_keys_must_do_nothing(self):
1096+ keyboard = self.get_keyboard_with_mocked_backend()
1097+ keyboard.release_pressed_keys()
1098+ self.assertEqual([], keyboard._device.mock_calls)
1099+
1100+ def test_release_pressed_keys_with_pressed_keys(self):
1101+ expected_calls = [
1102+ call.write(
1103+ ecodes.EV_KEY, ecodes.ecodes.get('KEY_A'),
1104+ self._RELEASE_VALUE),
1105+ call.syn(),
1106+ call.write(
1107+ ecodes.EV_KEY, ecodes.ecodes.get('KEY_B'),
1108+ self._RELEASE_VALUE),
1109+ call.syn()
1110+ ]
1111+
1112+ keyboard = self.get_keyboard_with_mocked_backend()
1113+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
1114+ self.press_key_and_reset_mock(keyboard, 'KEY_B')
1115+
1116+ keyboard.release_pressed_keys()
1117+
1118+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
1119+
1120+ def test_release_pressed_keys_already_released(self):
1121+ expected_calls = []
1122+ keyboard = self.get_keyboard_with_mocked_backend()
1123+ keyboard.press('KEY_A')
1124+ keyboard.release_pressed_keys()
1125+ keyboard._device.reset_mock()
1126+
1127+ keyboard.release_pressed_keys()
1128+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
1129+
1130+
1131+class UInputKeyboardTestCase(testscenarios.TestWithScenarios, TestCase):
1132+ """Test UInput Keyboard helper for autopilot tests."""
1133+
1134+ scenarios = [
1135+ ('single key', dict(keys='a', expected_calls_args=['a'])),
1136+ ('upper-case letter', dict(
1137+ keys='A', expected_calls_args=['KEY_LEFTSHIFT', 'A'])),
1138+ ('key combination', dict(
1139+ keys='a+b', expected_calls_args=['a', 'b']))
1140+ ]
1141+
1142+ def setUp(self):
1143+ super(UInputKeyboardTestCase, self).setUp()
1144+ # Return to the original device after the test.
1145+ self.addCleanup(self.set_keyboard_device, _uinput.Keyboard._device)
1146+ # Mock the sleeps so we don't have to spend time actually sleeping.
1147+ self.addCleanup(utilities.sleep.disable_mock)
1148+ utilities.sleep.enable_mock()
1149+
1150+ def set_keyboard_device(self, device):
1151+ _uinput.Keyboard._device = device
1152+
1153+ def get_keyboard_with_mocked_backend(self):
1154+ _uinput.Keyboard._device = None
1155+ keyboard = _uinput.Keyboard(device_class=Mock)
1156+ keyboard._device.mock_add_spec(
1157+ _uinput._UInputKeyboardDevice, spec_set=True)
1158+ return keyboard
1159+
1160+ def test_press_must_put_press_device_keys(self):
1161+ expected_calls = [
1162+ call.press(arg) for arg in self.expected_calls_args]
1163+ keyboard = self.get_keyboard_with_mocked_backend()
1164+ keyboard.press(self.keys)
1165+
1166+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
1167+
1168+ def test_release_must_release_device_keys(self):
1169+ keyboard = self.get_keyboard_with_mocked_backend()
1170+ keyboard.press(self.keys)
1171+ keyboard._device.reset_mock()
1172+
1173+ expected_calls = [
1174+ call.release(arg) for arg in
1175+ reversed(self.expected_calls_args)]
1176+ keyboard.release(self.keys)
1177+
1178+ self.assertEqual(
1179+ expected_calls, keyboard._device.mock_calls)
1180+
1181+ def test_press_and_release_must_press_device_keys(self):
1182+ expected_press_calls = [
1183+ call.press(arg) for arg in self.expected_calls_args]
1184+ ignored_calls = [
1185+ ANY for arg in self.expected_calls_args]
1186+
1187+ keyboard = self.get_keyboard_with_mocked_backend()
1188+ keyboard.press_and_release(self.keys)
1189+
1190+ self.assertEqual(
1191+ expected_press_calls + ignored_calls,
1192+ keyboard._device.mock_calls)
1193+
1194+ def test_press_and_release_must_release_device_keys_in_reverse_order(
1195+ self):
1196+ ignored_calls = [
1197+ ANY for arg in self.expected_calls_args]
1198+ expected_release_calls = [
1199+ call.release(arg) for arg in
1200+ reversed(self.expected_calls_args)]
1201+
1202+ keyboard = self.get_keyboard_with_mocked_backend()
1203+ keyboard.press_and_release(self.keys)
1204+
1205+ self.assertEqual(
1206+ ignored_calls + expected_release_calls,
1207+ keyboard._device.mock_calls)
1208+
1209+ def test_on_test_end_without_device_must_do_nothing(self):
1210+ _uinput.Keyboard._device = None
1211+ # This will fail if it calls anything from the device, as it's None.
1212+ _uinput.Keyboard.on_test_end(self)
1213+
1214+ def test_on_test_end_with_device_must_release_pressed_keys(self):
1215+ keyboard = self.get_keyboard_with_mocked_backend()
1216+ _uinput.Keyboard.on_test_end(self)
1217+ self.assertEqual(
1218+ [call.release_pressed_keys()], keyboard._device.mock_calls)
1219+
1220+
1221+class TouchEventsTestCase(TestCase):
1222+
1223+ def assert_expected_ev_abs(self, res_x, res_y, actual_ev_abs):
1224+ expected_ev_abs = [
1225+ (ecodes.ABS_X, (0, res_x, 0, 0)),
1226+ (ecodes.ABS_Y, (0, res_y, 0, 0)),
1227+ (ecodes.ABS_PRESSURE, (0, 65535, 0, 0)),
1228+ (ecodes.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
1229+ (ecodes.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
1230+ (ecodes.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
1231+ (ecodes.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
1232+ (ecodes.ABS_MT_PRESSURE, (0, 255, 0, 0)),
1233+ (ecodes.ABS_MT_SLOT, (0, 9, 0, 0))
1234+ ]
1235+ self.assertEqual(expected_ev_abs, actual_ev_abs)
1236+
1237+ def test_get_touch_events_without_args_must_use_system_resolution(self):
1238+ with patch.object(
1239+ _uinput, '_get_system_resolution', spec_set=True,
1240+ autospec=True) as mock_system_resolution:
1241+ mock_system_resolution.return_value = (
1242+ 'system_res_x', 'system_res_y')
1243+ events = _uinput._get_touch_events()
1244+
1245+ ev_abs = events.get(ecodes.EV_ABS)
1246+ self.assert_expected_ev_abs('system_res_x', 'system_res_y', ev_abs)
1247+
1248+ def test_get_touch_events_with_args_must_use_given_resulution(self):
1249+ events = _uinput._get_touch_events('given_res_x', 'given_res_y')
1250+ ev_abs = events.get(ecodes.EV_ABS)
1251+ self.assert_expected_ev_abs('given_res_x', 'given_res_y', ev_abs)
1252+
1253+
1254+class UInputTouchDeviceTestCase(TestCase):
1255+ """Test the integration with evdev.UInput for the touch device."""
1256+
1257+ def setUp(self):
1258+ super(UInputTouchDeviceTestCase, self).setUp()
1259+ self._number_of_slots = 9
1260+
1261+ # Return to the original device after the test.
1262+ self.addCleanup(
1263+ self.set_mouse_device,
1264+ _uinput._UInputTouchDevice._device,
1265+ _uinput._UInputTouchDevice._touch_fingers_in_use,
1266+ _uinput._UInputTouchDevice._last_tracking_id)
1267+
1268+ # Always start the tests without fingers in use.
1269+ _uinput._UInputTouchDevice._touch_fingers_in_use = []
1270+ _uinput._UInputTouchDevice._last_tracking_id = 0
1271+
1272+ def set_mouse_device(
1273+ self, device, touch_fingers_in_use, last_tracking_id):
1274+ _uinput._UInputTouchDevice._device = device
1275+ _uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
1276+ _uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
1277+
1278+ def get_touch_with_mocked_backend(self):
1279+ dummy_x_resolution = 100
1280+ dummy_y_resolution = 100
1281+
1282+ _uinput._UInputTouchDevice._device = None
1283+ touch = _uinput._UInputTouchDevice(
1284+ res_x=dummy_x_resolution, res_y=dummy_y_resolution,
1285+ device_class=Mock)
1286+ touch._device.mock_add_spec(uinput.UInput, spec_set=True)
1287+ return touch
1288+
1289+ def assert_finger_down_emitted_write_and_syn(
1290+ self, touch, slot, tracking_id, x, y):
1291+ press_value = 1
1292+ expected_calls = [
1293+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
1294+ call.write(
1295+ ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, tracking_id),
1296+ call.write(
1297+ ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, press_value),
1298+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
1299+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
1300+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_PRESSURE, 400),
1301+ call.syn()
1302+ ]
1303+ self.assertEqual(expected_calls, touch._device.mock_calls)
1304+
1305+ def assert_finger_move_emitted_write_and_syn(self, touch, slot, x, y):
1306+ expected_calls = [
1307+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
1308+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
1309+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
1310+ call.syn()
1311+ ]
1312+ self.assertEqual(expected_calls, touch._device.mock_calls)
1313+
1314+ def assert_finger_up_emitted_write_and_syn(self, touch, slot):
1315+ lift_tracking_id = -1
1316+ release_value = 0
1317+ expected_calls = [
1318+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
1319+ call.write(
1320+ ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, lift_tracking_id),
1321+ call.write(
1322+ ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, release_value),
1323+ call.syn()
1324+ ]
1325+ self.assertEqual(expected_calls, touch._device.mock_calls)
1326+
1327+ def test_finger_down_must_use_free_slot(self):
1328+ for slot in range(self._number_of_slots):
1329+ touch = self.get_touch_with_mocked_backend()
1330+
1331+ touch.finger_down(0, 0)
1332+
1333+ self.assert_finger_down_emitted_write_and_syn(
1334+ touch, slot=slot, tracking_id=ANY, x=0, y=0)
1335+
1336+ def test_finger_down_without_free_slots_must_raise_error(self):
1337+ # Claim all the available slots.
1338+ for slot in range(self._number_of_slots):
1339+ touch = self.get_touch_with_mocked_backend()
1340+ touch.finger_down(0, 0)
1341+
1342+ touch = self.get_touch_with_mocked_backend()
1343+
1344+ # Try to use one more.
1345+ error = self.assertRaises(RuntimeError, touch.finger_down, 11, 11)
1346+ self.assertEqual(
1347+ 'All available fingers have been used already.', str(error))
1348+
1349+ def test_finger_down_must_use_unique_tracking_id(self):
1350+ for number in range(self._number_of_slots):
1351+ touch = self.get_touch_with_mocked_backend()
1352+ touch.finger_down(0, 0)
1353+
1354+ self.assert_finger_down_emitted_write_and_syn(
1355+ touch, slot=ANY, tracking_id=number + 1, x=0, y=0)
1356+
1357+ def test_finger_down_must_not_reuse_tracking_ids(self):
1358+ # Claim and release all the available slots once.
1359+ for number in range(self._number_of_slots):
1360+ touch = self.get_touch_with_mocked_backend()
1361+ touch.finger_down(0, 0)
1362+ touch.finger_up()
1363+
1364+ touch = self.get_touch_with_mocked_backend()
1365+
1366+ touch.finger_down(12, 12)
1367+ self.assert_finger_down_emitted_write_and_syn(
1368+ touch, slot=ANY, tracking_id=number + 2, x=12, y=12)
1369+
1370+ def test_finger_down_with_finger_pressed_must_raise_error(self):
1371+ touch = self.get_touch_with_mocked_backend()
1372+ touch.finger_down(0, 0)
1373+
1374+ error = self.assertRaises(RuntimeError, touch.finger_down, 0, 0)
1375+ self.assertEqual(
1376+ "Cannot press finger: it's already pressed.", str(error))
1377+
1378+ def test_finger_move_without_finger_pressed_must_raise_error(self):
1379+ touch = self.get_touch_with_mocked_backend()
1380+
1381+ error = self.assertRaises(RuntimeError, touch.finger_move, 10, 10)
1382+ self.assertEqual(
1383+ 'Attempting to move without finger being down.', str(error))
1384+
1385+ def test_finger_move_must_use_assigned_slot(self):
1386+ for slot in range(self._number_of_slots):
1387+ touch = self.get_touch_with_mocked_backend()
1388+ touch.finger_down(0, 0)
1389+ touch._device.reset_mock()
1390+
1391+ touch.finger_move(10, 10)
1392+
1393+ self.assert_finger_move_emitted_write_and_syn(
1394+ touch, slot=slot, x=10, y=10)
1395+
1396+ def test_finger_move_must_reuse_assigned_slot(self):
1397+ first_slot = 0
1398+ touch = self.get_touch_with_mocked_backend()
1399+ touch.finger_down(1, 1)
1400+ touch._device.reset_mock()
1401+
1402+ touch.finger_move(13, 13)
1403+ self.assert_finger_move_emitted_write_and_syn(
1404+ touch, slot=first_slot, x=13, y=13)
1405+ touch._device.reset_mock()
1406+
1407+ touch.finger_move(14, 14)
1408+ self.assert_finger_move_emitted_write_and_syn(
1409+ touch, slot=first_slot, x=14, y=14)
1410+
1411+ def test_finger_up_without_finger_pressed_must_raise_error(self):
1412+ touch = self.get_touch_with_mocked_backend()
1413+
1414+ error = self.assertRaises(RuntimeError, touch.finger_up)
1415+ self.assertEqual(
1416+ "Cannot release finger: it's not pressed.", str(error))
1417+
1418+ def test_finger_up_must_use_assigned_slot(self):
1419+ fingers = []
1420+ for slot in range(self._number_of_slots):
1421+ touch = self.get_touch_with_mocked_backend()
1422+ touch.finger_down(0, 0)
1423+ touch._device.reset_mock()
1424+ fingers.append(touch)
1425+
1426+ for slot, touch in enumerate(fingers):
1427+ touch.finger_up()
1428+
1429+ self.assert_finger_up_emitted_write_and_syn(touch, slot=slot)
1430+ touch._device.reset_mock()
1431+
1432+ def test_finger_up_must_release_slot(self):
1433+ fingers = []
1434+ # Claim all the available slots.
1435+ for slot in range(self._number_of_slots):
1436+ touch = self.get_touch_with_mocked_backend()
1437+ touch.finger_down(0, 0)
1438+ fingers.append(touch)
1439+
1440+ slot_to_reuse = 3
1441+ fingers[slot_to_reuse].finger_up()
1442+
1443+ touch = self.get_touch_with_mocked_backend()
1444+
1445+ # Try to use one more.
1446+ touch.finger_down(15, 15)
1447+ self.assert_finger_down_emitted_write_and_syn(
1448+ touch, slot=slot_to_reuse, tracking_id=ANY, x=15, y=15)
1449+
1450+ def test_device_with_finger_down_must_be_pressed(self):
1451+ touch = self.get_touch_with_mocked_backend()
1452+ touch.finger_down(0, 0)
1453+
1454+ self.assertTrue(touch.pressed)
1455+
1456+ def test_device_without_finger_down_must_not_be_pressed(self):
1457+ touch = self.get_touch_with_mocked_backend()
1458+ self.assertFalse(touch.pressed)
1459+
1460+ def test_device_after_finger_up_must_not_be_pressed(self):
1461+ touch = self.get_touch_with_mocked_backend()
1462+ touch.finger_down(0, 0)
1463+ touch.finger_up()
1464+
1465+ self.assertFalse(touch.pressed)
1466+
1467+ def test_press_other_device_must_not_press_all_of_them(self):
1468+ other_touch = self.get_touch_with_mocked_backend()
1469+ other_touch.finger_down(0, 0)
1470+
1471+ touch = self.get_touch_with_mocked_backend()
1472+ self.assertFalse(touch.pressed)
1473+
1474+
1475+class UInputTouchTestCase(TestCase):
1476+ """Test UInput Touch helper for autopilot tests."""
1477+
1478+ def setUp(self):
1479+ super(UInputTouchTestCase, self).setUp()
1480+ # Mock the sleeps so we don't have to spend time actually sleeping.
1481+ self.addCleanup(utilities.sleep.disable_mock)
1482+ utilities.sleep.enable_mock()
1483+
1484+ def get_touch_with_mocked_backend(self):
1485+ touch = _uinput.Touch(device_class=Mock)
1486+ touch._device.mock_add_spec(
1487+ _uinput._UInputTouchDevice, spec_set=True)
1488+ return touch
1489+
1490+ def test_tap_must_put_finger_down_and_then_up(self):
1491+ expected_calls = [
1492+ call.finger_down(0, 0),
1493+ call.finger_up()
1494+ ]
1495+
1496+ touch = self.get_touch_with_mocked_backend()
1497+ touch.tap(0, 0)
1498+ self.assertEqual(expected_calls, touch._device.mock_calls)
1499+
1500+ def test_tap_object_must_put_finger_down_and_then_up_on_the_center(self):
1501+ object_ = type('Dummy', (object,), {'globalRect': (0, 0, 10, 10)})
1502+ expected_calls = [
1503+ call.finger_down(5, 5),
1504+ call.finger_up()
1505+ ]
1506+
1507+ touch = self.get_touch_with_mocked_backend()
1508+ touch.tap_object(object_)
1509+ self.assertEqual(expected_calls, touch._device.mock_calls)
1510+
1511+ def test_press_must_put_finger_down(self):
1512+ expected_calls = [call.finger_down(0, 0)]
1513+
1514+ touch = self.get_touch_with_mocked_backend()
1515+ touch.press(0, 0)
1516+ self.assertEqual(expected_calls, touch._device.mock_calls)
1517+
1518+ def test_release_must_put_finger_up(self):
1519+ expected_calls = [call.finger_up()]
1520+
1521+ touch = self.get_touch_with_mocked_backend()
1522+ touch.release()
1523+ self.assertEqual(expected_calls, touch._device.mock_calls)
1524+
1525+ def test_move_must_move_finger(self):
1526+ expected_calls = [call.finger_move(10, 10)]
1527+
1528+ touch = self.get_touch_with_mocked_backend()
1529+ touch.move(10, 10)
1530+ self.assertEqual(expected_calls, touch._device.mock_calls)
1531+
1532+
1533+class MultipleUInputTouchBackend(_uinput._UInputTouchDevice):
1534+
1535+ def __init__(self, res_x=100, res_y=100, device_class=Mock):
1536+ super(MultipleUInputTouchBackend, self).__init__(
1537+ res_x, res_y, device_class)
1538+
1539+
1540+class MultipleUInputTouchTestCase(TestCase):
1541+
1542+ def setUp(self):
1543+ super(MultipleUInputTouchTestCase, self).setUp()
1544+ # Return to the original device after the test.
1545+ self.addCleanup(
1546+ self.set_mouse_device,
1547+ _uinput._UInputTouchDevice._device,
1548+ _uinput._UInputTouchDevice._touch_fingers_in_use,
1549+ _uinput._UInputTouchDevice._last_tracking_id)
1550+
1551+ def set_mouse_device(
1552+ self, device, touch_fingers_in_use, last_tracking_id):
1553+ _uinput._UInputTouchDevice._device = device
1554+ _uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
1555+ _uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
1556+
1557+ def test_press_other_device_must_not_press_all_of_them(self):
1558+ finger1 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
1559+ finger2 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
1560+
1561+ finger1.press(0, 0)
1562+ self.addCleanup(finger1.release)
1563+
1564+ self.assertFalse(finger2.pressed)
1565+
1566+
1567+class DragUinputTouchTestCase(testscenarios.TestWithScenarios, TestCase):
1568+
1569+ scenarios = [
1570+ ('drag to top', dict(
1571+ start_x=50, start_y=50, stop_x=50, stop_y=30,
1572+ expected_moves=[call(50, 40), call(50, 30)])),
1573+ ('drag to bottom', dict(
1574+ start_x=50, start_y=50, stop_x=50, stop_y=70,
1575+ expected_moves=[call(50, 60), call(50, 70)])),
1576+ ('drag to left', dict(
1577+ start_x=50, start_y=50, stop_x=30, stop_y=50,
1578+ expected_moves=[call(40, 50), call(30, 50)])),
1579+ ('drag to right', dict(
1580+ start_x=50, start_y=50, stop_x=70, stop_y=50,
1581+ expected_moves=[call(60, 50), call(70, 50)])),
1582+
1583+ ('drag to top-left', dict(
1584+ start_x=50, start_y=50, stop_x=30, stop_y=30,
1585+ expected_moves=[call(40, 40), call(30, 30)])),
1586+ ('drag to top-right', dict(
1587+ start_x=50, start_y=50, stop_x=70, stop_y=30,
1588+ expected_moves=[call(60, 40), call(70, 30)])),
1589+ ('drag to bottom-left', dict(
1590+ start_x=50, start_y=50, stop_x=30, stop_y=70,
1591+ expected_moves=[call(40, 60), call(30, 70)])),
1592+ ('drag to bottom-right', dict(
1593+ start_x=50, start_y=50, stop_x=70, stop_y=70,
1594+ expected_moves=[call(60, 60), call(70, 70)])),
1595+
1596+ ('drag less than rate', dict(
1597+ start_x=50, start_y=50, stop_x=55, stop_y=55,
1598+ expected_moves=[call(55, 55)])),
1599+
1600+ ('drag with last move less than rate', dict(
1601+ start_x=50, start_y=50, stop_x=65, stop_y=65,
1602+ expected_moves=[call(60, 60), call(65, 65)])),
1603+ ]
1604+
1605+ def test_drag_moves(self):
1606+ with MockUinputTouch() as mock_touch:
1607+ mock_touch.drag(
1608+ self.start_x, self.start_y, self.stop_x, self.stop_y)
1609+
1610+ self.assertEqual(
1611+ self.expected_moves, mock_touch.get_finger_move_call_args_list())
1612+
1613+
1614+class PointerTestCase(TestCase):
1615+
1616+ def setUp(self):
1617+ super(PointerTestCase, self).setUp()
1618+ self.pointer = autopilot.input.Pointer(autopilot.input.Mouse.create())
1619+
1620+ def test_drag_with_rate(self):
1621+ with patch.object(self.pointer._device, 'drag') as mock_drag:
1622+ self.pointer.drag(0, 0, 20, 20, rate=5)
1623+
1624+ mock_drag.assert_called_once_with(0, 0, 20, 20, rate=5)
1625+
1626+ def test_drag_with_default_rate(self):
1627+ with patch.object(self.pointer._device, 'drag') as mock_drag:
1628+ self.pointer.drag(0, 0, 20, 20)
1629+
1630+ mock_drag.assert_called_once_with(0, 0, 20, 20, rate=10)
1631
1632=== modified file 'debian/control'
1633--- debian/control 2014-01-29 20:48:43 +0000
1634+++ debian/control 2014-02-11 06:39:03 +0000
1635@@ -16,6 +16,7 @@
1636 python-dbus,
1637 python-debian,
1638 python-dev,
1639+ python-evdev,
1640 python-fixtures,
1641 python-gi,
1642 python-junitxml,
1643@@ -30,6 +31,7 @@
1644 python-xlib,
1645 python3-all-dev (>= 3.3),
1646 python3-dbus,
1647+ python3-evdev,
1648 python3-fixtures,
1649 python3-gi,
1650 python3-junitxml,

Subscribers

People subscribed via source and target branches