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