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