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
=== modified file 'autopilot/input/_X11.py'
--- autopilot/input/_X11.py 2013-11-07 05:53:36 +0000
+++ autopilot/input/_X11.py 2014-02-11 06:39:03 +0000
@@ -1,7 +1,7 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#2#
3# Autopilot Functional Test Tool3# Autopilot Functional Test Tool
4# Copyright (C) 2012-2013 Canonical4# Copyright (C) 2012, 2013, 2014 Canonical
5#5#
6# This program is free software: you can redistribute it and/or modify6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by7# it under the terms of the GNU General Public License as published by
@@ -455,7 +455,7 @@
455 x, y = coord["root_x"], coord["root_y"]455 x, y = coord["root_x"], coord["root_y"]
456 return x, y456 return x, y
457457
458 def drag(self, x1, y1, x2, y2):458 def drag(self, x1, y1, x2, y2, rate=10):
459 """Performs a press, move and release.459 """Performs a press, move and release.
460460
461 This is to keep a common API between Mouse and Finger as long as461 This is to keep a common API between Mouse and Finger as long as
@@ -464,7 +464,7 @@
464 """464 """
465 self.move(x1, y1)465 self.move(x1, y1)
466 self.press()466 self.press()
467 self.move(x2, y2)467 self.move(x2, y2, rate=rate)
468 self.release()468 self.release()
469469
470 @classmethod470 @classmethod
471471
=== modified file 'autopilot/input/__init__.py'
--- autopilot/input/__init__.py 2013-09-20 19:01:27 +0000
+++ autopilot/input/__init__.py 2014-02-11 06:39:03 +0000
@@ -369,7 +369,7 @@
369 """369 """
370 raise NotImplementedError("You cannot use this class directly.")370 raise NotImplementedError("You cannot use this class directly.")
371371
372 def drag(self, x1, y1, x2, y2):372 def drag(self, x1, y1, x2, y2, rate=10):
373 """Performs a press, move and release.373 """Performs a press, move and release.
374374
375 This is to keep a common API between Mouse and Finger as long as375 This is to keep a common API between Mouse and Finger as long as
@@ -466,7 +466,7 @@
466 """Release a previously pressed finger"""466 """Release a previously pressed finger"""
467 raise NotImplementedError("You cannot use this class directly.")467 raise NotImplementedError("You cannot use this class directly.")
468468
469 def drag(self, x1, y1, x2, y2):469 def drag(self, x1, y1, x2, y2, rate=10):
470 """Perform a drag gesture from (x1,y1) to (x2,y2)"""470 """Perform a drag gesture from (x1,y1) to (x2,y2)"""
471 raise NotImplementedError("You cannot use this class directly.")471 raise NotImplementedError("You cannot use this class directly.")
472472
@@ -641,9 +641,9 @@
641 else:641 else:
642 return (self._x, self._y)642 return (self._x, self._y)
643643
644 def drag(self, x1, y1, x2, y2):644 def drag(self, x1, y1, x2, y2, rate=10):
645 """Performs a press, move and release."""645 """Performs a press, move and release."""
646 self._device.drag(x1, y1, x2, y2)646 self._device.drag(x1, y1, x2, y2, rate=rate)
647 if isinstance(self._device, Touch):647 if isinstance(self._device, Touch):
648 self._x = x2648 self._x = x2
649 self._y = y2649 self._y = y2
650650
=== modified file 'autopilot/input/_common.py'
--- autopilot/input/_common.py 2013-12-10 03:10:11 +0000
+++ autopilot/input/_common.py 2014-02-11 06:39:03 +0000
@@ -26,8 +26,18 @@
2626
2727
28def get_center_point(object_proxy):28def get_center_point(object_proxy):
29 """Get the center point of an object, searching for several different ways29 """Get the center point of an object.
30 of determining exactly where the center is.30
31 It searches for several different ways of determining exactly where the
32 center is.
33
34 :raises ValueError: if `object_proxy` has the globalRect attribute but it
35 is not of the correct type.
36 :raises ValueError: if `object_proxy` doesn't have the globalRect
37 attribute, it has the x and y attributes instead, but they are not of
38 the correct type.
39 :raises ValueError: if `object_proxy` doesn't have any recognised position
40 attributes.
3141
32 """42 """
33 try:43 try:
3444
=== modified file 'autopilot/input/_uinput.py'
--- autopilot/input/_uinput.py 2013-11-07 05:53:36 +0000
+++ autopilot/input/_uinput.py 2014-02-11 06:39:03 +0000
@@ -1,7 +1,7 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#2#
3# Autopilot Functional Test Tool3# Autopilot Functional Test Tool
4# Copyright (C) 2012-2013 Canonical4# Copyright (C) 2012, 2013, 2014 Canonical
5#5#
6# This program is free software: you can redistribute it and/or modify6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by7# it under the terms of the GNU General Public License as published by
@@ -17,27 +17,23 @@
17# along with this program. If not, see <http://www.gnu.org/licenses/>.17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#18#
1919
20
21"""UInput device drivers."""20"""UInput device drivers."""
2221
22import logging
23import os.path
24
25import six
26from evdev import UInput, ecodes as e
27
28import autopilot.platform
23from autopilot.input import Keyboard as KeyboardBase29from autopilot.input import Keyboard as KeyboardBase
24from autopilot.input import Touch as TouchBase30from autopilot.input import Touch as TouchBase
25from autopilot.input._common import get_center_point31from autopilot.input._common import get_center_point
26from autopilot.utilities import sleep32from autopilot.utilities import deprecated, sleep
27import autopilot.platform
2833
29import logging
30from evdev import UInput, ecodes as e
31import os.path
32import six
3334
34logger = logging.getLogger(__name__)35logger = logging.getLogger(__name__)
3536
36PRESS = 1
37RELEASE = 0
38
39_PRESSED_KEYS = []
40
4137
42def _get_devnode_path():38def _get_devnode_path():
43 """Provide a fallback uinput node for devices which don't support udev"""39 """Provide a fallback uinput node for devices which don't support udev"""
@@ -47,13 +43,78 @@
47 return devnode43 return devnode
4844
4945
46class _UInputKeyboardDevice(object):
47 """Wrapper for the UInput Keyboard to execute its primitives."""
48
49 def __init__(self, device_class=UInput):
50 super(_UInputKeyboardDevice, self).__init__()
51 self._device = device_class(devnode=_get_devnode_path())
52 self._pressed_keys_ecodes = []
53
54 def press(self, key):
55 """Press one key button.
56
57 It ignores case, so, for example, 'a' and 'A' are mapped to the same
58 key.
59
60 """
61 ecode = self._get_ecode_for_key(key)
62 logger.debug('Pressing %s (%r).', key, ecode)
63 self._emit_press_event(ecode)
64 self._pressed_keys_ecodes.append(ecode)
65
66 def _get_ecode_for_key(self, key):
67 key_name = key if key.startswith('KEY_') else 'KEY_' + key
68 key_name = key_name.upper()
69 ecode = e.ecodes.get(key_name, None)
70 if ecode is None:
71 raise ValueError('Unknown key name: %s.' % key)
72 return ecode
73
74 def _emit_press_event(self, ecode):
75 press_value = 1
76 self._emit(ecode, press_value)
77
78 def _emit(self, ecode, value):
79 self._device.write(e.EV_KEY, ecode, value)
80 self._device.syn()
81
82 def release(self, key):
83 """Release one key button.
84
85 It ignores case, so, for example, 'a' and 'A' are mapped to the same
86 key.
87
88 :raises ValueError: if ``key`` is not pressed.
89
90 """
91 ecode = self._get_ecode_for_key(key)
92 if ecode in self._pressed_keys_ecodes:
93 logger.debug('Releasing %s (%r).', key, ecode)
94 self._emit_release_event(ecode)
95 self._pressed_keys_ecodes.remove(ecode)
96 else:
97 raise ValueError('Key %r not pressed.' % key)
98
99 def _emit_release_event(self, ecode):
100 release_value = 0
101 self._emit(ecode, release_value)
102
103 def release_pressed_keys(self):
104 """Release all the keys that are currently pressed."""
105 for ecode in self._pressed_keys_ecodes:
106 self._emit_release_event(ecode)
107 self._pressed_keys_ecodes = []
108
109
50class Keyboard(KeyboardBase):110class Keyboard(KeyboardBase):
51111
52 _device = UInput(devnode=_get_devnode_path())112 _device = None
53113
54 def _emit(self, event, value):114 def __init__(self, device_class=_UInputKeyboardDevice):
55 Keyboard._device.write(e.EV_KEY, event, value)115 super(Keyboard, self).__init__()
56 Keyboard._device.syn()116 if Keyboard._device is None:
117 Keyboard._device = device_class()
57118
58 def _sanitise_keys(self, keys):119 def _sanitise_keys(self, keys):
59 if keys == '+':120 if keys == '+':
@@ -71,15 +132,15 @@
71132
72 presses the 'Alt' and 'F2' keys.133 presses the 'Alt' and 'F2' keys.
73134
135 :raises TypeError: if ``keys`` is not a string.
136
74 """137 """
75 if not isinstance(keys, six.string_types):138 if not isinstance(keys, six.string_types):
76 raise TypeError("'keys' argument must be a string.")139 raise TypeError("'keys' argument must be a string.")
77140
78 for key in self._sanitise_keys(keys):141 for key in self._sanitise_keys(keys):
79 for event in Keyboard._get_events_for_key(key):142 for key_button in self._get_key_buttons(key):
80 logger.debug("Pressing %s (%r)", key, event)143 self._device.press(key_button)
81 _PRESSED_KEYS.append(event)
82 self._emit(event, PRESS)
83 sleep(delay)144 sleep(delay)
84145
85 def release(self, keys, delay=0.1):146 def release(self, keys, delay=0.1):
@@ -94,16 +155,16 @@
94155
95 Keys are released in the reverse order in which they are specified.156 Keys are released in the reverse order in which they are specified.
96157
158 :raises TypeError: if ``keys`` is not a string.
159 :raises ValueError: if one of the keys to be released is not pressed.
160
97 """161 """
98 if not isinstance(keys, six.string_types):162 if not isinstance(keys, six.string_types):
99 raise TypeError("'keys' argument must be a string.")163 raise TypeError("'keys' argument must be a string.")
100164
101 for key in reversed(self._sanitise_keys(keys)):165 for key in reversed(self._sanitise_keys(keys)):
102 for event in Keyboard._get_events_for_key(key):166 for key_button in reversed(self._get_key_buttons(key)):
103 logger.debug("Releasing %s (%r)", key, event)167 self._device.release(key_button)
104 if event in _PRESSED_KEYS:
105 _PRESSED_KEYS.remove(event)
106 self._emit(event, RELEASE)
107 sleep(delay)168 sleep(delay)
108169
109 def press_and_release(self, keys, delay=0.1):170 def press_and_release(self, keys, delay=0.1):
@@ -118,6 +179,8 @@
118179
119 presses both the 'Alt' and 'F2' keys, and then releases both keys.180 presses both the 'Alt' and 'F2' keys, and then releases both keys.
120181
182 :raises TypeError: if ``keys`` is not a string.
183
121 """184 """
122 logger.debug("Pressing and Releasing: %s", keys)185 logger.debug("Pressing and Releasing: %s", keys)
123 self.press(keys, delay)186 self.press(keys, delay)
@@ -129,6 +192,8 @@
129 Only 'normal' keys can be typed with this method. Control characters192 Only 'normal' keys can be typed with this method. Control characters
130 (such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').193 (such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').
131194
195 :raises TypeError: if ``keys`` is not a string.
196
132 """197 """
133 if not isinstance(string, six.string_types):198 if not isinstance(string, six.string_types):
134 raise TypeError("'keys' argument must be a string.")199 raise TypeError("'keys' argument must be a string.")
@@ -145,98 +210,35 @@
145 any keys that were pressed and not released.210 any keys that were pressed and not released.
146211
147 """212 """
148 global _PRESSED_KEYS213 if cls._device is not None:
149 if len(_PRESSED_KEYS) == 0:214 cls._device.release_pressed_keys()
150 return215
151216 def _get_key_buttons(self, key):
152 def _release(event):217 """Return a list of the key buttons required to press.
153 Keyboard._device.write(e.EV_KEY, event, RELEASE)218
154 Keyboard._device.syn()219 Multiple buttons will be returned when the key specified requires more
155 for event in _PRESSED_KEYS:
156 logger.warning("Releasing key %r as part of cleanup call.", event)
157 _release(event)
158 _PRESSED_KEYS = []
159
160 @staticmethod
161 def _get_events_for_key(key):
162 """Return a list of events required to generate 'key' as an input.
163
164 Multiple keys will be returned when the key specified requires more
165 than one keypress to generate (for example, upper-case letters).220 than one keypress to generate (for example, upper-case letters).
166221
167 """222 """
168 events = []223 key_buttons = []
169 if key.isupper() or key in _SHIFTED_KEYS:224 if key.isupper() or key in _SHIFTED_KEYS:
170 events.append(e.KEY_LEFTSHIFT)225 key_buttons.append('KEY_LEFTSHIFT')
171 keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)226 key_name = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
172 evt = getattr(e, 'KEY_' + keyname.upper(), None)227 key_buttons.append(key_name)
173 if evt is None:228 return key_buttons
174 raise ValueError("Unknown key name: '%s'" % key)229
175 events.append(evt)230
176 return events231@deprecated('the Touch class to instantiate a device object')
177
178
179last_tracking_id = 0
180
181
182def get_next_tracking_id():
183 global last_tracking_id
184 last_tracking_id += 1
185 return last_tracking_id
186
187
188def create_touch_device(res_x=None, res_y=None):232def create_touch_device(res_x=None, res_y=None):
189 """Create and return a UInput touch device.233 """Create and return a UInput touch device.
190234
191 If res_x and res_y are not specified, they will be queried from the system.235 If res_x and res_y are not specified, they will be queried from the system.
192236
193 """237 """
194238 return UInput(events=_get_touch_events(res_x, res_y),
195 if res_x is None or res_y is None:239 name='autopilot-finger',
196 from autopilot.display import Display240 version=0x2, devnode=_get_devnode_path())
197 display = Display.create()241
198 # TODO: This calculation needs to become part of the display module:
199 l = r = t = b = 0
200 for screen in range(display.get_num_screens()):
201 geometry = display.get_screen_geometry(screen)
202 if geometry[0] < l:
203 l = geometry[0]
204 if geometry[1] < t:
205 t = geometry[1]
206 if geometry[0] + geometry[2] > r:
207 r = geometry[0] + geometry[2]
208 if geometry[1] + geometry[3] > b:
209 b = geometry[1] + geometry[3]
210 res_x = r - l
211 res_y = b - t
212
213 # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no
214 # idea why...
215 touch_tool = e.BTN_TOOL_FINGER
216 if autopilot.platform.model() == 'Desktop':
217 touch_tool = e.BTN_TOUCH
218
219 cap_mt = {
220 e.EV_ABS: [
221 (e.ABS_X, (0, res_x, 0, 0)),
222 (e.ABS_Y, (0, res_y, 0, 0)),
223 (e.ABS_PRESSURE, (0, 65535, 0, 0)),
224 (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
225 (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
226 (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
227 (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
228 (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
229 (e.ABS_MT_SLOT, (0, 9, 0, 0)),
230 ],
231 e.EV_KEY: [
232 touch_tool,
233 ]
234 }
235
236 return UInput(cap_mt, name='autopilot-finger', version=0x2,
237 devnode=_get_devnode_path())
238
239_touch_device = create_touch_device()
240242
241# Multiouch notes:243# Multiouch notes:
242# ----------------244# ----------------
@@ -281,143 +283,277 @@
281# about this is that the SLOT refers to a finger number, and the TRACKING_ID283# about this is that the SLOT refers to a finger number, and the TRACKING_ID
282# identifies a unique touch for the duration of it's existance.284# identifies a unique touch for the duration of it's existance.
283285
284_touch_fingers_in_use = []286
285287def _get_touch_events(res_x=None, res_y=None):
286288 if res_x is None or res_y is None:
287def _get_touch_finger():289 res_x, res_y = _get_system_resolution()
288 """Claim a touch finger id for use.290
289291 touch_tool = _get_touch_tool()
290 :raises: RuntimeError if no more fingers are available.292
291293 events = {
292 """294 e.EV_ABS: [
293 global _touch_fingers_in_use295 (e.ABS_X, (0, res_x, 0, 0)),
294296 (e.ABS_Y, (0, res_y, 0, 0)),
295 for i in range(9):297 (e.ABS_PRESSURE, (0, 65535, 0, 0)),
296 if i not in _touch_fingers_in_use:298 (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
297 _touch_fingers_in_use.append(i)299 (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
298 return i300 (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
299 raise RuntimeError("All available fingers have been used already.")301 (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
300302 (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
301303 (e.ABS_MT_SLOT, (0, 9, 0, 0)),
302def _release_touch_finger(finger_num):304 ],
303 """Relase a previously-claimed finger id.305 e.EV_KEY: [
304306 touch_tool,
305 :raises: RuntimeError if the finger given was never claimed, or was already307 ]
306 released.308 }
307309 return events
308 """310
309 global _touch_fingers_in_use311
310312def _get_system_resolution():
311 if finger_num not in _touch_fingers_in_use:313 from autopilot.display import Display
312 raise RuntimeError(314 display = Display.create()
313 "Finger %d was never claimed, or has already been released." %315 # TODO: This calculation needs to become part of the display module:
314 (finger_num))316 l = r = t = b = 0
315 _touch_fingers_in_use.remove(finger_num)317 for screen in range(display.get_num_screens()):
316 assert(finger_num not in _touch_fingers_in_use)318 geometry = display.get_screen_geometry(screen)
319 if geometry[0] < l:
320 l = geometry[0]
321 if geometry[1] < t:
322 t = geometry[1]
323 if geometry[0] + geometry[2] > r:
324 r = geometry[0] + geometry[2]
325 if geometry[1] + geometry[3] > b:
326 b = geometry[1] + geometry[3]
327 res_x = r - l
328 res_y = b - t
329 return res_x, res_y
330
331
332def _get_touch_tool():
333 # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have
334 # no idea why...
335 if autopilot.platform.model() == 'Desktop':
336 touch_tool = e.BTN_TOUCH
337 else:
338 touch_tool = e.BTN_TOOL_FINGER
339 return touch_tool
340
341
342class _UInputTouchDevice(object):
343 """Wrapper for the UInput Touch to execute its primitives."""
344
345 _device = None
346 _touch_fingers_in_use = []
347 _last_tracking_id = 0
348
349 def __init__(self, res_x=None, res_y=None, device_class=UInput):
350 """Class constructor.
351
352 If res_x and res_y are not specified, they will be queried from the
353 system.
354
355 """
356 super(_UInputTouchDevice, self).__init__()
357 if _UInputTouchDevice._device is None:
358 _UInputTouchDevice._device = device_class(
359 events=_get_touch_events(res_x, res_y),
360 name='autopilot-finger',
361 version=0x2, devnode=_get_devnode_path())
362 self._touch_finger_slot = None
363
364 @property
365 def pressed(self):
366 return self._touch_finger_slot is not None
367
368 def finger_down(self, x, y):
369 """Internal: moves finger "finger" down on the touchscreen.
370
371 :param x: The finger will be moved to this x coordinate.
372 :param y: The finger will be moved to this y coordinate.
373
374 :raises RuntimeError: if the finger is already pressed.
375 :raises RuntimeError: if no more touch slots are available.
376
377 """
378 if self.pressed:
379 raise RuntimeError("Cannot press finger: it's already pressed.")
380 self._touch_finger_slot = self._get_free_touch_finger_slot()
381
382 self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
383 self._device.write(
384 e.EV_ABS, e.ABS_MT_TRACKING_ID, self._get_next_tracking_id())
385 press_value = 1
386 self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, press_value)
387 self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
388 self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
389 self._device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
390 self._device.syn()
391
392 def _get_free_touch_finger_slot(self):
393 """Return the id of a free touch finger.
394
395 :raises RuntimeError: if no more touch slots are available.
396
397 """
398 max_number_of_fingers = 9
399 for i in range(max_number_of_fingers):
400 if i not in _UInputTouchDevice._touch_fingers_in_use:
401 _UInputTouchDevice._touch_fingers_in_use.append(i)
402 return i
403 raise RuntimeError('All available fingers have been used already.')
404
405 def _get_next_tracking_id(self):
406 _UInputTouchDevice._last_tracking_id += 1
407 return _UInputTouchDevice._last_tracking_id
408
409 def finger_move(self, x, y):
410 """Internal: moves finger "finger" on the touchscreen to pos (x,y)
411
412 NOTE: The finger has to be down for this to have any effect.
413
414 :raises RuntimeError: if the finger is not pressed.
415
416 """
417 if not self.pressed:
418 raise RuntimeError('Attempting to move without finger being down.')
419 self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
420 self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
421 self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
422 self._device.syn()
423
424 def finger_up(self):
425 """Internal: moves finger "finger" up from the touchscreen
426
427 :raises RuntimeError: if the finger is not pressed.
428
429 """
430 if not self.pressed:
431 raise RuntimeError("Cannot release finger: it's not pressed.")
432 self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
433 lift_tracking_id = -1
434 self._device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, lift_tracking_id)
435 release_value = 0
436 self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, release_value)
437 self._device.syn()
438 self._release_touch_finger()
439
440 def _release_touch_finger(self):
441 """Release the touch finger.
442
443 :raises RuntimeError: if the finger was not claimed before or was
444 already released.
445
446 """
447 if (self._touch_finger_slot not in
448 _UInputTouchDevice._touch_fingers_in_use):
449 raise RuntimeError(
450 "Finger %d was never claimed, or has already been released." %
451 self._touch_finger_slot)
452 _UInputTouchDevice._touch_fingers_in_use.remove(
453 self._touch_finger_slot)
454 self._touch_finger_slot = None
317455
318456
319class Touch(TouchBase):457class Touch(TouchBase):
320 """Low level interface to generate single finger touch events."""458 """Low level interface to generate single finger touch events."""
321459
322 def __init__(self):460 def __init__(self, device_class=_UInputTouchDevice):
323 super(Touch, self).__init__()461 super(Touch, self).__init__()
324 self._touch_finger = None462 self._device = device_class()
325463
326 @property464 @property
327 def pressed(self):465 def pressed(self):
328 return self._touch_finger is not None466 return self._device.pressed
329467
330 def tap(self, x, y):468 def tap(self, x, y):
331 """Click (or 'tap') at given x and y coordinates."""469 """Click (or 'tap') at given x and y coordinates.
470
471 :raises RuntimeError: if the finger is already pressed.
472 :raises RuntimeError: if no more finger slots are available.
473
474 """
332 logger.debug("Tapping at: %d,%d", x, y)475 logger.debug("Tapping at: %d,%d", x, y)
333 self._finger_down(x, y)476 self._device.finger_down(x, y)
334 sleep(0.1)477 sleep(0.1)
335 self._finger_up()478 self._device.finger_up()
336479
337 def tap_object(self, object):480 def tap_object(self, object_):
338 """Click (or 'tap') a given object"""481 """Click (or 'tap') a given object.
482
483 :raises RuntimeError: if the finger is already pressed.
484 :raises RuntimeError: if no more finger slots are available.
485 :raises ValueError: if `object_` doesn't have any recognised position
486 attributes or if they are not of the correct type.
487
488 """
339 logger.debug("Tapping object: %r", object)489 logger.debug("Tapping object: %r", object)
340 x, y = get_center_point(object)490 x, y = get_center_point(object_)
341 self.tap(x, y)491 self.tap(x, y)
342492
343 def press(self, x, y):493 def press(self, x, y):
344 """Press and hold a given object or at the given coordinates494 """Press and hold a given object or at the given coordinates.
345 Call release() when the object has been pressed long enough"""495
496 Call release() when the object has been pressed long enough.
497
498 :raises RuntimeError: if the finger is already pressed.
499 :raises RuntimeError: if no more finger slots are available.
500
501 """
346 logger.debug("Pressing at: %d,%d", x, y)502 logger.debug("Pressing at: %d,%d", x, y)
347 self._finger_down(x, y)503 self._device.finger_down(x, y)
348504
349 def release(self):505 def release(self):
350 """Release a previously pressed finger"""506 """Release a previously pressed finger.
507
508 :raises RuntimeError: if the touch is not pressed.
509
510 """
351 logger.debug("Releasing")511 logger.debug("Releasing")
352 self._finger_up()512 self._device.finger_up()
353513
354 def move(self, x, y):514 def move(self, x, y):
355 """Moves the pointing "finger" to pos(x,y).515 """Moves the pointing "finger" to pos(x,y).
356516
357 NOTE: The finger has to be down for this to have any effect.517 NOTE: The finger has to be down for this to have any effect.
358518
359 """519 :raises RuntimeError: if the finger is not pressed.
360 if self._touch_finger is None:520
361 raise RuntimeError("Attempting to move without finger being down.")521 """
362 self._finger_move(x, y)522 self._device.finger_move(x, y)
363523
364 def drag(self, x1, y1, x2, y2):524 def drag(self, x1, y1, x2, y2, rate=10):
365 """Perform a drag gesture from (x1,y1) to (x2,y2)"""525 """Perform a drag gesture from (x1,y1) to (x2,y2).
526
527 :raises RuntimeError: if the finger is already pressed.
528 :raises RuntimeError: if no more finger slots are available.
529
530 """
366 logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)531 logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)
367 self._finger_down(x1, y1)532 self._device.finger_down(x1, y1)
368533
369 # Let's drag in 100 steps for now...534 current_x, current_y = x1, y1
370 dx = 1.0 * (x2 - x1) / 100535 while current_x != x2 or current_y != y2:
371 dy = 1.0 * (y2 - y1) / 100536 dx = abs(x2 - current_x)
372 cur_x = x1 + dx537 dy = abs(y2 - current_y)
373 cur_y = y1 + dy538
374 for i in range(0, 100):539 intx = float(dx) / max(dx, dy)
375 self._finger_move(int(cur_x), int(cur_y))540 inty = float(dy) / max(dx, dy)
541
542 step_x = min(rate * intx, dx)
543 step_y = min(rate * inty, dy)
544
545 if x2 < current_x:
546 step_x *= -1
547 if y2 < current_y:
548 step_y *= -1
549
550 current_x += step_x
551 current_y += step_y
552 self.device._finger_move(current_x, current_y)
553
376 sleep(0.002)554 sleep(0.002)
377 cur_x += dx555
378 cur_y += dy556 self.device._finger_up()
379 # Make sure we actually end up at target
380 self._finger_move(x2, y2)
381 self._finger_up()
382
383 def _finger_down(self, x, y):
384 """Internal: moves finger "finger" down on the touchscreen.
385
386 :param x: The finger will be moved to this x coordinate.
387 :param y: The finger will be moved to this y coordinate.
388
389 """
390 if self._touch_finger is not None:
391 raise RuntimeError("Cannot press finger: it's already pressed.")
392 self._touch_finger = _get_touch_finger()
393
394 _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
395 _touch_device.write(
396 e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())
397 _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)
398 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
399 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
400 _touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
401 _touch_device.syn()
402
403 def _finger_move(self, x, y):
404 """Internal: moves finger "finger" on the touchscreen to pos (x,y)
405 NOTE: The finger has to be down for this to have any effect."""
406 if self._touch_finger is not None:
407 _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
408 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
409 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
410 _touch_device.syn()
411
412 def _finger_up(self):
413 """Internal: moves finger "finger" up from the touchscreen"""
414 if self._touch_finger is None:
415 raise RuntimeError("Cannot release finger: it's not pressed.")
416 _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
417 _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
418 _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)
419 _touch_device.syn()
420 self._touch_finger = _release_touch_finger(self._touch_finger)
421557
422558
423# veebers: there should be a better way to handle this.559# veebers: there should be a better way to handle this.
424560
=== modified file 'autopilot/tests/functional/test_input_stack.py'
--- autopilot/tests/functional/test_input_stack.py 2013-12-16 00:20:40 +0000
+++ autopilot/tests/functional/test_input_stack.py 2014-02-11 06:39:03 +0000
@@ -1,7 +1,7 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#2#
3# Autopilot Functional Test Tool3# Autopilot Functional Test Tool
4# Copyright (C) 2012-2013 Canonical4# Copyright (C) 2012, 2013, 2014 Canonical
5#5#
6# This program is free software: you can redistribute it and/or modify6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by7# it under the terms of the GNU General Public License as published by
@@ -150,8 +150,8 @@
150 from autopilot.input._X11 import _PRESSED_KEYS150 from autopilot.input._X11 import _PRESSED_KEYS
151 return _PRESSED_KEYS151 return _PRESSED_KEYS
152 elif self.backend == 'UInput':152 elif self.backend == 'UInput':
153 from autopilot.input._uinput import _PRESSED_KEYS153 from autopilot.input import _uinput
154 return _PRESSED_KEYS154 return _uinput.Keyboard._device._pressed_keys_ecodes
155 else:155 else:
156 self.fail("Don't know how to get pressed keys list for backend "156 self.fail("Don't know how to get pressed keys list for backend "
157 + self.backend157 + self.backend
@@ -551,8 +551,9 @@
551 test_result = FakeTestCase("test_press_key").run()551 test_result = FakeTestCase("test_press_key").run()
552552
553 self.assertThat(test_result.wasSuccessful(), Equals(True))553 self.assertThat(test_result.wasSuccessful(), Equals(True))
554 from autopilot.input._uinput import _PRESSED_KEYS554 from autopilot.input import _uinput
555 self.assertThat(_PRESSED_KEYS, Equals([]))555 self.assertThat(
556 _uinput.Keyboard._device._pressed_keys_ecodes, Equals([]))
556557
557 @patch('autopilot.input._X11.fake_input', new=lambda *args: None, )558 @patch('autopilot.input._X11.fake_input', new=lambda *args: None, )
558 def test_mouse_button_released(self):559 def test_mouse_button_released(self):
559560
=== modified file 'autopilot/tests/unit/test_input.py'
--- autopilot/tests/unit/test_input.py 2013-12-10 03:10:11 +0000
+++ autopilot/tests/unit/test_input.py 2014-02-11 06:39:03 +0000
@@ -1,7 +1,7 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#2#
3# Autopilot Functional Test Tool3# Autopilot Functional Test Tool
4# Copyright (C) 2013 Canonical4# Copyright (C) 2013, 2014 Canonical
5#5#
6# This program is free software: you can redistribute it and/or modify6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by7# it under the terms of the GNU General Public License as published by
@@ -17,10 +17,18 @@
17# along with this program. If not, see <http://www.gnu.org/licenses/>.17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#18#
1919
20from mock import patch20import testscenarios
21
22from mock import call, Mock, patch
23from evdev import ecodes, uinput
24from mock import ANY, call, patch, Mock
25from six import StringIO
21from testtools import TestCase26from testtools import TestCase
22from testtools.matchers import raises27from testtools.matchers import Contains, raises
2328
29import autopilot.input
30from autopilot import utilities
31from autopilot.input import _uinput, _X11
24from autopilot.input._common import get_center_point32from autopilot.input._common import get_center_point
2533
2634
@@ -151,3 +159,774 @@
151159
152 self.assertEqual(123, x)160 self.assertEqual(123, x)
153 self.assertEqual(345, y)161 self.assertEqual(345, y)
162
163
164class PartialMock(object):
165 """Mock some of the methods of an object, and record their calls."""
166
167 def __init__(self, real_object, *args):
168 super(PartialMock, self).__init__()
169 self._mock_manager = Mock()
170 self._real_object = real_object
171 self.patched_attributes = args
172
173 def __getattr__(self, name):
174 """Forward all the calls to the real object."""
175 return self._real_object.__getattribute__(name)
176
177 @property
178 def mock_calls(self):
179 """Return the calls recorded for the mocked attributes."""
180 return self._mock_manager.mock_calls
181
182 def __enter__(self):
183 self._start_patchers()
184 return self
185
186 def _start_patchers(self):
187 self._patchers = []
188 for attribute in self.patched_attributes:
189 patcher = patch.object(self._real_object, attribute)
190 self._patchers.append(patcher)
191
192 self._mock_manager.attach_mock(patcher.start(), attribute)
193
194 def __exit__(self, exc_type, exc_val, exc_tb):
195 self._stop_patchers()
196
197 def _stop_patchers(self):
198 for patcher in self._patchers:
199 patcher.stop()
200
201
202class MockX11Mouse(PartialMock):
203 """Mock for the X11 Mouse Touch.
204
205 It records the calls to press, release and move, but doesn't perform them.
206
207 """
208
209 def __init__(self):
210 super(MockX11Mouse, self).__init__(
211 _X11.Mouse(), 'press', 'release', 'move')
212
213 def get_move_call_args_list(self):
214 return self._mock_manager.move.call_args_list
215
216
217class X11MouseTestCase(TestCase):
218
219 def test_drag_should_call_move_with_rate(self):
220 expected_first_move_call = call(0, 0)
221 expected_second_move_call = call(100, 100, rate=1)
222 with MockX11Mouse() as mock_mouse:
223 mock_mouse.drag(0, 0, 100, 100, rate=1)
224
225 self.assertEqual(
226 [expected_first_move_call, expected_second_move_call],
227 mock_mouse.get_move_call_args_list())
228
229 def test_drag_with_default_rate(self):
230 expected_first_move_call = call(0, 0)
231 expected_second_move_call = call(100, 100, rate=10)
232 with MockX11Mouse() as mock_mouse:
233 mock_mouse.drag(0, 0, 100, 100)
234
235 self.assertEqual(
236 [expected_first_move_call, expected_second_move_call],
237 mock_mouse.get_move_call_args_list())
238
239
240class MockUinputTouch(PartialMock):
241 """Mock for the uinput Touch.
242
243 It records the calls to _finger_down, _finger_up and _finger_move, but
244 doesn't perform them.
245
246 """
247
248 def __init__(self):
249 super(MockUinputTouch, self).__init__(
250 _uinput.Touch(), '_finger_down', '_finger_up', '_finger_move')
251
252 def get_finger_move_call_args_list(self):
253 return self._mock_manager._finger_move.call_args_list
254
255
256class UinputTouchTestCase(TestCase):
257
258 def test_drag_finger_actions(self):
259 expected_finger_calls = [
260 call._finger_down(0, 0),
261 call._finger_move(10, 10),
262 call._finger_up()
263 ]
264 with MockUinputTouch() as mock_touch:
265 mock_touch.drag(0, 0, 10, 10)
266 self.assertEqual(mock_touch.mock_calls, expected_finger_calls)
267
268 def test_drag_should_call_move_with_rate(self):
269 expected_move_calls = [call(5, 5), call(10, 10), call(15, 15)]
270 with MockUinputTouch() as mock_touch:
271 mock_touch.drag(0, 0, 15, 15, rate=5)
272
273 self.assertEqual(
274 expected_move_calls, mock_touch.get_finger_move_call_args_list())
275
276 def test_drag_with_default_rate(self):
277 expected_move_calls = [call(10, 10), call(20, 20)]
278 with MockUinputTouch() as mock_touch:
279 mock_touch.drag(0, 0, 20, 20)
280
281 self.assertEqual(
282 expected_move_calls, mock_touch.get_finger_move_call_args_list())
283
284 def test_drag_to_same_place_should_not_move(self):
285 expected_finger_calls = [
286 call._finger_down(0, 0),
287 call._finger_up()
288 ]
289 with MockUinputTouch() as mock_touch:
290 mock_touch.drag(0, 0, 0, 0)
291 self.assertEqual(mock_touch.mock_calls, expected_finger_calls)
292
293
294class UInputTestCase(TestCase):
295 """Tests for the global methods of the uinput module."""
296
297 def test_create_touch_device_must_print_deprecation_message(self):
298 with patch('sys.stderr', new=StringIO()) as stderr:
299 with patch('autopilot.input._uinput.UInput'):
300 _uinput.create_touch_device('dummy', 'dummy')
301 self.assertThat(
302 stderr.getvalue(),
303 Contains(
304 "This function is deprecated. Please use 'the Touch class to "
305 "instantiate a device object' instead."
306 )
307 )
308
309
310class UInputKeyboardDeviceTestCase(TestCase):
311 """Test the integration with evdev.UInput for the keyboard."""
312
313 _PRESS_VALUE = 1
314 _RELEASE_VALUE = 0
315
316 def get_keyboard_with_mocked_backend(self):
317 keyboard = _uinput._UInputKeyboardDevice(device_class=Mock)
318 keyboard._device.mock_add_spec(uinput.UInput, spec_set=True)
319 return keyboard
320
321 def assert_key_press_emitted_write_and_syn(self, keyboard, key):
322 self.assert_emitted_write_and_syn(keyboard, key, self._PRESS_VALUE)
323
324 def assert_key_release_emitted_write_and_syn(self, keyboard, key):
325 self.assert_emitted_write_and_syn(keyboard, key, self._RELEASE_VALUE)
326
327 def assert_emitted_write_and_syn(self, keyboard, key, value):
328 key_ecode = ecodes.ecodes.get(key)
329 expected_calls = [
330 call.write(ecodes.EV_KEY, key_ecode, value),
331 call.syn()
332 ]
333
334 self.assertEqual(expected_calls, keyboard._device.mock_calls)
335
336 def press_key_and_reset_mock(self, keyboard, key):
337 keyboard.press(key)
338 keyboard._device.reset_mock()
339
340 def test_press_key_must_emit_write_and_syn(self):
341 keyboard = self.get_keyboard_with_mocked_backend()
342 keyboard.press('KEY_A')
343 self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
344
345 def test_press_key_must_append_leading_string(self):
346 keyboard = self.get_keyboard_with_mocked_backend()
347 keyboard.press('A')
348 self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
349
350 def test_press_key_must_ignore_case(self):
351 keyboard = self.get_keyboard_with_mocked_backend()
352 keyboard.press('a')
353 self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
354
355 def test_press_unexisting_key_must_raise_error(self):
356 keyboard = self.get_keyboard_with_mocked_backend()
357 error = self.assertRaises(
358 ValueError, keyboard.press, 'unexisting')
359
360 self.assertEqual('Unknown key name: unexisting.', str(error))
361
362 def test_release_not_pressed_key_must_raise_error(self):
363 keyboard = self.get_keyboard_with_mocked_backend()
364 error = self.assertRaises(
365 ValueError, keyboard.release, 'A')
366
367 self.assertEqual("Key 'A' not pressed.", str(error))
368
369 def test_release_key_must_emit_write_and_syn(self):
370 keyboard = self.get_keyboard_with_mocked_backend()
371 self.press_key_and_reset_mock(keyboard, 'KEY_A')
372
373 keyboard.release('KEY_A')
374 self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
375
376 def test_release_key_must_append_leading_string(self):
377 keyboard = self.get_keyboard_with_mocked_backend()
378 self.press_key_and_reset_mock(keyboard, 'KEY_A')
379
380 keyboard.release('A')
381 self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
382
383 def test_release_key_must_ignore_case(self):
384 keyboard = self.get_keyboard_with_mocked_backend()
385 self.press_key_and_reset_mock(keyboard, 'KEY_A')
386
387 keyboard.release('a')
388 self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
389
390 def test_release_unexisting_key_must_raise_error(self):
391 keyboard = self.get_keyboard_with_mocked_backend()
392 error = self.assertRaises(
393 ValueError, keyboard.release, 'unexisting')
394
395 self.assertEqual('Unknown key name: unexisting.', str(error))
396
397 def test_release_pressed_keys_without_pressed_keys_must_do_nothing(self):
398 keyboard = self.get_keyboard_with_mocked_backend()
399 keyboard.release_pressed_keys()
400 self.assertEqual([], keyboard._device.mock_calls)
401
402 def test_release_pressed_keys_with_pressed_keys(self):
403 expected_calls = [
404 call.write(
405 ecodes.EV_KEY, ecodes.ecodes.get('KEY_A'),
406 self._RELEASE_VALUE),
407 call.syn(),
408 call.write(
409 ecodes.EV_KEY, ecodes.ecodes.get('KEY_B'),
410 self._RELEASE_VALUE),
411 call.syn()
412 ]
413
414 keyboard = self.get_keyboard_with_mocked_backend()
415 self.press_key_and_reset_mock(keyboard, 'KEY_A')
416 self.press_key_and_reset_mock(keyboard, 'KEY_B')
417
418 keyboard.release_pressed_keys()
419
420 self.assertEqual(expected_calls, keyboard._device.mock_calls)
421
422 def test_release_pressed_keys_already_released(self):
423 expected_calls = []
424 keyboard = self.get_keyboard_with_mocked_backend()
425 keyboard.press('KEY_A')
426 keyboard.release_pressed_keys()
427 keyboard._device.reset_mock()
428
429 keyboard.release_pressed_keys()
430 self.assertEqual(expected_calls, keyboard._device.mock_calls)
431
432
433class UInputKeyboardTestCase(testscenarios.TestWithScenarios, TestCase):
434 """Test UInput Keyboard helper for autopilot tests."""
435
436 scenarios = [
437 ('single key', dict(keys='a', expected_calls_args=['a'])),
438 ('upper-case letter', dict(
439 keys='A', expected_calls_args=['KEY_LEFTSHIFT', 'A'])),
440 ('key combination', dict(
441 keys='a+b', expected_calls_args=['a', 'b']))
442 ]
443
444 def setUp(self):
445 super(UInputKeyboardTestCase, self).setUp()
446 # Return to the original device after the test.
447 self.addCleanup(self.set_keyboard_device, _uinput.Keyboard._device)
448 # Mock the sleeps so we don't have to spend time actually sleeping.
449 self.addCleanup(utilities.sleep.disable_mock)
450 utilities.sleep.enable_mock()
451
452 def set_keyboard_device(self, device):
453 _uinput.Keyboard._device = device
454
455 def get_keyboard_with_mocked_backend(self):
456 _uinput.Keyboard._device = None
457 keyboard = _uinput.Keyboard(device_class=Mock)
458 keyboard._device.mock_add_spec(
459 _uinput._UInputKeyboardDevice, spec_set=True)
460 return keyboard
461
462 def test_press_must_put_press_device_keys(self):
463 expected_calls = [
464 call.press(arg) for arg in self.expected_calls_args]
465 keyboard = self.get_keyboard_with_mocked_backend()
466 keyboard.press(self.keys)
467
468 self.assertEqual(expected_calls, keyboard._device.mock_calls)
469
470 def test_release_must_release_device_keys(self):
471 keyboard = self.get_keyboard_with_mocked_backend()
472 keyboard.press(self.keys)
473 keyboard._device.reset_mock()
474
475 expected_calls = [
476 call.release(arg) for arg in
477 reversed(self.expected_calls_args)]
478 keyboard.release(self.keys)
479
480 self.assertEqual(
481 expected_calls, keyboard._device.mock_calls)
482
483 def test_press_and_release_must_press_device_keys(self):
484 expected_press_calls = [
485 call.press(arg) for arg in self.expected_calls_args]
486 ignored_calls = [
487 ANY for arg in self.expected_calls_args]
488
489 keyboard = self.get_keyboard_with_mocked_backend()
490 keyboard.press_and_release(self.keys)
491
492 self.assertEqual(
493 expected_press_calls + ignored_calls,
494 keyboard._device.mock_calls)
495
496 def test_press_and_release_must_release_device_keys_in_reverse_order(
497 self):
498 ignored_calls = [
499 ANY for arg in self.expected_calls_args]
500 expected_release_calls = [
501 call.release(arg) for arg in
502 reversed(self.expected_calls_args)]
503
504 keyboard = self.get_keyboard_with_mocked_backend()
505 keyboard.press_and_release(self.keys)
506
507 self.assertEqual(
508 ignored_calls + expected_release_calls,
509 keyboard._device.mock_calls)
510
511 def test_on_test_end_without_device_must_do_nothing(self):
512 _uinput.Keyboard._device = None
513 # This will fail if it calls anything from the device, as it's None.
514 _uinput.Keyboard.on_test_end(self)
515
516 def test_on_test_end_with_device_must_release_pressed_keys(self):
517 keyboard = self.get_keyboard_with_mocked_backend()
518 _uinput.Keyboard.on_test_end(self)
519 self.assertEqual(
520 [call.release_pressed_keys()], keyboard._device.mock_calls)
521
522
523class TouchEventsTestCase(TestCase):
524
525 def assert_expected_ev_abs(self, res_x, res_y, actual_ev_abs):
526 expected_ev_abs = [
527 (ecodes.ABS_X, (0, res_x, 0, 0)),
528 (ecodes.ABS_Y, (0, res_y, 0, 0)),
529 (ecodes.ABS_PRESSURE, (0, 65535, 0, 0)),
530 (ecodes.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
531 (ecodes.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
532 (ecodes.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
533 (ecodes.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
534 (ecodes.ABS_MT_PRESSURE, (0, 255, 0, 0)),
535 (ecodes.ABS_MT_SLOT, (0, 9, 0, 0))
536 ]
537 self.assertEqual(expected_ev_abs, actual_ev_abs)
538
539 def test_get_touch_events_without_args_must_use_system_resolution(self):
540 with patch.object(
541 _uinput, '_get_system_resolution', spec_set=True,
542 autospec=True) as mock_system_resolution:
543 mock_system_resolution.return_value = (
544 'system_res_x', 'system_res_y')
545 events = _uinput._get_touch_events()
546
547 ev_abs = events.get(ecodes.EV_ABS)
548 self.assert_expected_ev_abs('system_res_x', 'system_res_y', ev_abs)
549
550 def test_get_touch_events_with_args_must_use_given_resulution(self):
551 events = _uinput._get_touch_events('given_res_x', 'given_res_y')
552 ev_abs = events.get(ecodes.EV_ABS)
553 self.assert_expected_ev_abs('given_res_x', 'given_res_y', ev_abs)
554
555
556class UInputTouchDeviceTestCase(TestCase):
557 """Test the integration with evdev.UInput for the touch device."""
558
559 def setUp(self):
560 super(UInputTouchDeviceTestCase, self).setUp()
561 self._number_of_slots = 9
562
563 # Return to the original device after the test.
564 self.addCleanup(
565 self.set_mouse_device,
566 _uinput._UInputTouchDevice._device,
567 _uinput._UInputTouchDevice._touch_fingers_in_use,
568 _uinput._UInputTouchDevice._last_tracking_id)
569
570 # Always start the tests without fingers in use.
571 _uinput._UInputTouchDevice._touch_fingers_in_use = []
572 _uinput._UInputTouchDevice._last_tracking_id = 0
573
574 def set_mouse_device(
575 self, device, touch_fingers_in_use, last_tracking_id):
576 _uinput._UInputTouchDevice._device = device
577 _uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
578 _uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
579
580 def get_touch_with_mocked_backend(self):
581 dummy_x_resolution = 100
582 dummy_y_resolution = 100
583
584 _uinput._UInputTouchDevice._device = None
585 touch = _uinput._UInputTouchDevice(
586 res_x=dummy_x_resolution, res_y=dummy_y_resolution,
587 device_class=Mock)
588 touch._device.mock_add_spec(uinput.UInput, spec_set=True)
589 return touch
590
591 def assert_finger_down_emitted_write_and_syn(
592 self, touch, slot, tracking_id, x, y):
593 press_value = 1
594 expected_calls = [
595 call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
596 call.write(
597 ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, tracking_id),
598 call.write(
599 ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, press_value),
600 call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
601 call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
602 call.write(ecodes.EV_ABS, ecodes.ABS_MT_PRESSURE, 400),
603 call.syn()
604 ]
605 self.assertEqual(expected_calls, touch._device.mock_calls)
606
607 def assert_finger_move_emitted_write_and_syn(self, touch, slot, x, y):
608 expected_calls = [
609 call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
610 call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
611 call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
612 call.syn()
613 ]
614 self.assertEqual(expected_calls, touch._device.mock_calls)
615
616 def assert_finger_up_emitted_write_and_syn(self, touch, slot):
617 lift_tracking_id = -1
618 release_value = 0
619 expected_calls = [
620 call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
621 call.write(
622 ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, lift_tracking_id),
623 call.write(
624 ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, release_value),
625 call.syn()
626 ]
627 self.assertEqual(expected_calls, touch._device.mock_calls)
628
629 def test_finger_down_must_use_free_slot(self):
630 for slot in range(self._number_of_slots):
631 touch = self.get_touch_with_mocked_backend()
632
633 touch.finger_down(0, 0)
634
635 self.assert_finger_down_emitted_write_and_syn(
636 touch, slot=slot, tracking_id=ANY, x=0, y=0)
637
638 def test_finger_down_without_free_slots_must_raise_error(self):
639 # Claim all the available slots.
640 for slot in range(self._number_of_slots):
641 touch = self.get_touch_with_mocked_backend()
642 touch.finger_down(0, 0)
643
644 touch = self.get_touch_with_mocked_backend()
645
646 # Try to use one more.
647 error = self.assertRaises(RuntimeError, touch.finger_down, 11, 11)
648 self.assertEqual(
649 'All available fingers have been used already.', str(error))
650
651 def test_finger_down_must_use_unique_tracking_id(self):
652 for number in range(self._number_of_slots):
653 touch = self.get_touch_with_mocked_backend()
654 touch.finger_down(0, 0)
655
656 self.assert_finger_down_emitted_write_and_syn(
657 touch, slot=ANY, tracking_id=number + 1, x=0, y=0)
658
659 def test_finger_down_must_not_reuse_tracking_ids(self):
660 # Claim and release all the available slots once.
661 for number in range(self._number_of_slots):
662 touch = self.get_touch_with_mocked_backend()
663 touch.finger_down(0, 0)
664 touch.finger_up()
665
666 touch = self.get_touch_with_mocked_backend()
667
668 touch.finger_down(12, 12)
669 self.assert_finger_down_emitted_write_and_syn(
670 touch, slot=ANY, tracking_id=number + 2, x=12, y=12)
671
672 def test_finger_down_with_finger_pressed_must_raise_error(self):
673 touch = self.get_touch_with_mocked_backend()
674 touch.finger_down(0, 0)
675
676 error = self.assertRaises(RuntimeError, touch.finger_down, 0, 0)
677 self.assertEqual(
678 "Cannot press finger: it's already pressed.", str(error))
679
680 def test_finger_move_without_finger_pressed_must_raise_error(self):
681 touch = self.get_touch_with_mocked_backend()
682
683 error = self.assertRaises(RuntimeError, touch.finger_move, 10, 10)
684 self.assertEqual(
685 'Attempting to move without finger being down.', str(error))
686
687 def test_finger_move_must_use_assigned_slot(self):
688 for slot in range(self._number_of_slots):
689 touch = self.get_touch_with_mocked_backend()
690 touch.finger_down(0, 0)
691 touch._device.reset_mock()
692
693 touch.finger_move(10, 10)
694
695 self.assert_finger_move_emitted_write_and_syn(
696 touch, slot=slot, x=10, y=10)
697
698 def test_finger_move_must_reuse_assigned_slot(self):
699 first_slot = 0
700 touch = self.get_touch_with_mocked_backend()
701 touch.finger_down(1, 1)
702 touch._device.reset_mock()
703
704 touch.finger_move(13, 13)
705 self.assert_finger_move_emitted_write_and_syn(
706 touch, slot=first_slot, x=13, y=13)
707 touch._device.reset_mock()
708
709 touch.finger_move(14, 14)
710 self.assert_finger_move_emitted_write_and_syn(
711 touch, slot=first_slot, x=14, y=14)
712
713 def test_finger_up_without_finger_pressed_must_raise_error(self):
714 touch = self.get_touch_with_mocked_backend()
715
716 error = self.assertRaises(RuntimeError, touch.finger_up)
717 self.assertEqual(
718 "Cannot release finger: it's not pressed.", str(error))
719
720 def test_finger_up_must_use_assigned_slot(self):
721 fingers = []
722 for slot in range(self._number_of_slots):
723 touch = self.get_touch_with_mocked_backend()
724 touch.finger_down(0, 0)
725 touch._device.reset_mock()
726 fingers.append(touch)
727
728 for slot, touch in enumerate(fingers):
729 touch.finger_up()
730
731 self.assert_finger_up_emitted_write_and_syn(touch, slot=slot)
732 touch._device.reset_mock()
733
734 def test_finger_up_must_release_slot(self):
735 fingers = []
736 # Claim all the available slots.
737 for slot in range(self._number_of_slots):
738 touch = self.get_touch_with_mocked_backend()
739 touch.finger_down(0, 0)
740 fingers.append(touch)
741
742 slot_to_reuse = 3
743 fingers[slot_to_reuse].finger_up()
744
745 touch = self.get_touch_with_mocked_backend()
746
747 # Try to use one more.
748 touch.finger_down(15, 15)
749 self.assert_finger_down_emitted_write_and_syn(
750 touch, slot=slot_to_reuse, tracking_id=ANY, x=15, y=15)
751
752 def test_device_with_finger_down_must_be_pressed(self):
753 touch = self.get_touch_with_mocked_backend()
754 touch.finger_down(0, 0)
755
756 self.assertTrue(touch.pressed)
757
758 def test_device_without_finger_down_must_not_be_pressed(self):
759 touch = self.get_touch_with_mocked_backend()
760 self.assertFalse(touch.pressed)
761
762 def test_device_after_finger_up_must_not_be_pressed(self):
763 touch = self.get_touch_with_mocked_backend()
764 touch.finger_down(0, 0)
765 touch.finger_up()
766
767 self.assertFalse(touch.pressed)
768
769 def test_press_other_device_must_not_press_all_of_them(self):
770 other_touch = self.get_touch_with_mocked_backend()
771 other_touch.finger_down(0, 0)
772
773 touch = self.get_touch_with_mocked_backend()
774 self.assertFalse(touch.pressed)
775
776
777class UInputTouchTestCase(TestCase):
778 """Test UInput Touch helper for autopilot tests."""
779
780 def setUp(self):
781 super(UInputTouchTestCase, self).setUp()
782 # Mock the sleeps so we don't have to spend time actually sleeping.
783 self.addCleanup(utilities.sleep.disable_mock)
784 utilities.sleep.enable_mock()
785
786 def get_touch_with_mocked_backend(self):
787 touch = _uinput.Touch(device_class=Mock)
788 touch._device.mock_add_spec(
789 _uinput._UInputTouchDevice, spec_set=True)
790 return touch
791
792 def test_tap_must_put_finger_down_and_then_up(self):
793 expected_calls = [
794 call.finger_down(0, 0),
795 call.finger_up()
796 ]
797
798 touch = self.get_touch_with_mocked_backend()
799 touch.tap(0, 0)
800 self.assertEqual(expected_calls, touch._device.mock_calls)
801
802 def test_tap_object_must_put_finger_down_and_then_up_on_the_center(self):
803 object_ = type('Dummy', (object,), {'globalRect': (0, 0, 10, 10)})
804 expected_calls = [
805 call.finger_down(5, 5),
806 call.finger_up()
807 ]
808
809 touch = self.get_touch_with_mocked_backend()
810 touch.tap_object(object_)
811 self.assertEqual(expected_calls, touch._device.mock_calls)
812
813 def test_press_must_put_finger_down(self):
814 expected_calls = [call.finger_down(0, 0)]
815
816 touch = self.get_touch_with_mocked_backend()
817 touch.press(0, 0)
818 self.assertEqual(expected_calls, touch._device.mock_calls)
819
820 def test_release_must_put_finger_up(self):
821 expected_calls = [call.finger_up()]
822
823 touch = self.get_touch_with_mocked_backend()
824 touch.release()
825 self.assertEqual(expected_calls, touch._device.mock_calls)
826
827 def test_move_must_move_finger(self):
828 expected_calls = [call.finger_move(10, 10)]
829
830 touch = self.get_touch_with_mocked_backend()
831 touch.move(10, 10)
832 self.assertEqual(expected_calls, touch._device.mock_calls)
833
834
835class MultipleUInputTouchBackend(_uinput._UInputTouchDevice):
836
837 def __init__(self, res_x=100, res_y=100, device_class=Mock):
838 super(MultipleUInputTouchBackend, self).__init__(
839 res_x, res_y, device_class)
840
841
842class MultipleUInputTouchTestCase(TestCase):
843
844 def setUp(self):
845 super(MultipleUInputTouchTestCase, self).setUp()
846 # Return to the original device after the test.
847 self.addCleanup(
848 self.set_mouse_device,
849 _uinput._UInputTouchDevice._device,
850 _uinput._UInputTouchDevice._touch_fingers_in_use,
851 _uinput._UInputTouchDevice._last_tracking_id)
852
853 def set_mouse_device(
854 self, device, touch_fingers_in_use, last_tracking_id):
855 _uinput._UInputTouchDevice._device = device
856 _uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
857 _uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
858
859 def test_press_other_device_must_not_press_all_of_them(self):
860 finger1 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
861 finger2 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
862
863 finger1.press(0, 0)
864 self.addCleanup(finger1.release)
865
866 self.assertFalse(finger2.pressed)
867
868
869class DragUinputTouchTestCase(testscenarios.TestWithScenarios, TestCase):
870
871 scenarios = [
872 ('drag to top', dict(
873 start_x=50, start_y=50, stop_x=50, stop_y=30,
874 expected_moves=[call(50, 40), call(50, 30)])),
875 ('drag to bottom', dict(
876 start_x=50, start_y=50, stop_x=50, stop_y=70,
877 expected_moves=[call(50, 60), call(50, 70)])),
878 ('drag to left', dict(
879 start_x=50, start_y=50, stop_x=30, stop_y=50,
880 expected_moves=[call(40, 50), call(30, 50)])),
881 ('drag to right', dict(
882 start_x=50, start_y=50, stop_x=70, stop_y=50,
883 expected_moves=[call(60, 50), call(70, 50)])),
884
885 ('drag to top-left', dict(
886 start_x=50, start_y=50, stop_x=30, stop_y=30,
887 expected_moves=[call(40, 40), call(30, 30)])),
888 ('drag to top-right', dict(
889 start_x=50, start_y=50, stop_x=70, stop_y=30,
890 expected_moves=[call(60, 40), call(70, 30)])),
891 ('drag to bottom-left', dict(
892 start_x=50, start_y=50, stop_x=30, stop_y=70,
893 expected_moves=[call(40, 60), call(30, 70)])),
894 ('drag to bottom-right', dict(
895 start_x=50, start_y=50, stop_x=70, stop_y=70,
896 expected_moves=[call(60, 60), call(70, 70)])),
897
898 ('drag less than rate', dict(
899 start_x=50, start_y=50, stop_x=55, stop_y=55,
900 expected_moves=[call(55, 55)])),
901
902 ('drag with last move less than rate', dict(
903 start_x=50, start_y=50, stop_x=65, stop_y=65,
904 expected_moves=[call(60, 60), call(65, 65)])),
905 ]
906
907 def test_drag_moves(self):
908 with MockUinputTouch() as mock_touch:
909 mock_touch.drag(
910 self.start_x, self.start_y, self.stop_x, self.stop_y)
911
912 self.assertEqual(
913 self.expected_moves, mock_touch.get_finger_move_call_args_list())
914
915
916class PointerTestCase(TestCase):
917
918 def setUp(self):
919 super(PointerTestCase, self).setUp()
920 self.pointer = autopilot.input.Pointer(autopilot.input.Mouse.create())
921
922 def test_drag_with_rate(self):
923 with patch.object(self.pointer._device, 'drag') as mock_drag:
924 self.pointer.drag(0, 0, 20, 20, rate=5)
925
926 mock_drag.assert_called_once_with(0, 0, 20, 20, rate=5)
927
928 def test_drag_with_default_rate(self):
929 with patch.object(self.pointer._device, 'drag') as mock_drag:
930 self.pointer.drag(0, 0, 20, 20)
931
932 mock_drag.assert_called_once_with(0, 0, 20, 20, rate=10)
154933
=== modified file 'debian/control'
--- debian/control 2014-01-29 20:48:43 +0000
+++ debian/control 2014-02-11 06:39:03 +0000
@@ -16,6 +16,7 @@
16 python-dbus,16 python-dbus,
17 python-debian,17 python-debian,
18 python-dev,18 python-dev,
19 python-evdev,
19 python-fixtures,20 python-fixtures,
20 python-gi,21 python-gi,
21 python-junitxml,22 python-junitxml,
@@ -30,6 +31,7 @@
30 python-xlib,31 python-xlib,
31 python3-all-dev (>= 3.3),32 python3-all-dev (>= 3.3),
32 python3-dbus,33 python3-dbus,
34 python3-evdev,
33 python3-fixtures,35 python3-fixtures,
34 python3-gi,36 python3-gi,
35 python3-junitxml,37 python3-junitxml,

Subscribers

People subscribed via source and target branches