Merge lp:~thomir-deactivatedaccount/autopilot/merge-private-code into lp:autopilot
- merge-private-code
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Thomi Richards | ||||
Approved revision: | 197 | ||||
Merged at revision: | 158 | ||||
Proposed branch: | lp:~thomir-deactivatedaccount/autopilot/merge-private-code | ||||
Merge into: | lp:autopilot | ||||
Diff against target: |
5734 lines (+2705/-1947) 60 files modified
autopilot/compizconfig.py (+0/-57) autopilot/display/_X11.py (+50/-0) autopilot/display/__init__.py (+136/-0) autopilot/display/_upa.py (+40/-0) autopilot/emulators.py (+21/-0) autopilot/emulators/X11.py (+0/-187) autopilot/emulators/__init__.py (+0/-11) autopilot/emulators/processmanager.py (+0/-66) autopilot/emulators/zeitgeist.py (+0/-66) autopilot/gestures.py (+62/-0) autopilot/glibrunner.py (+0/-26) autopilot/globals.py (+123/-21) autopilot/input/_X11.py (+4/-147) autopilot/input/__init__.py (+170/-76) autopilot/input/_common.py (+46/-0) autopilot/input/_uinput.py (+171/-275) autopilot/introspection/__init__.py (+85/-93) autopilot/introspection/dbus.py (+1/-1) autopilot/introspection/gtk.py (+4/-4) autopilot/introspection/qt.py (+3/-3) autopilot/keybindings.py (+50/-5) autopilot/matchers/__init__.py (+48/-3) autopilot/platform.py (+105/-0) autopilot/process/__init__.py (+403/-0) autopilot/process/_bamf.py (+145/-34) autopilot/testcase.py (+128/-390) autopilot/tests/test_ap_apps.py (+10/-6) autopilot/tests/test_application_mixin.py (+12/-25) autopilot/tests/test_application_registration.py (+0/-94) autopilot/tests/test_compiz_key_translate.py (+0/-69) autopilot/tests/test_compiz_option_support.py (+0/-31) autopilot/tests/test_keyboard.py (+2/-2) autopilot/tests/test_mouse_emulator.py (+2/-2) autopilot/tests/test_open_window.py (+4/-3) autopilot/tests/test_out_of_test_addcleanup.py (+34/-0) autopilot/tests/test_platform.py (+134/-0) autopilot/tests/test_process_emulator.py (+4/-4) autopilot/utilities.py (+43/-94) bin/autopilot (+11/-19) debian/changelog (+6/-0) debian/control (+1/-1) docs/_templates/indexcontent.html (+45/-0) docs/api/autopilot.rst (+7/-100) docs/api/display.rst (+7/-0) docs/api/emulators.rst (+20/-0) docs/api/gestures.rst (+6/-0) docs/api/input.rst (+7/-0) docs/api/introspection.rst (+6/-0) docs/api/matchers.rst (+6/-0) docs/api/platform.rst (+7/-0) docs/api/testcase.rst (+7/-0) docs/conf.py (+23/-6) docs/faq/faq.rst (+95/-15) docs/images/test_pyramid.svg (+134/-0) docs/images/test_stages.svg (+166/-0) docs/index.rst (+6/-10) docs/porting/porting.rst (+31/-0) docs/tutorial/tutorial.rst (+3/-0) docs/tutorial/what_is_autopilot.rst (+62/-0) setup.py (+9/-1) |
||||
To merge this branch: | bzr merge lp:~thomir-deactivatedaccount/autopilot/merge-private-code | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Sergio Schvezov | Needs Fixing | ||
Michael Zanetti (community) | Needs Fixing | ||
Review via email:
|
Commit message
Merge in large changes for version 1.3.
Description of the change
This branch contains the 'new hotness' - large scale changes for autopilot 1.3.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michael Zanetti (mzanetti) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Olivier Tilloy (osomon) wrote : | # |
I built a package from this MR, installed on my raring desktop, and tried to run the autopilot tests for lp:webbrowser-app.
Listing the tests fails with the following error:
ImportError: cannot import name QtIntrospection
but can be easily fixed by simply removing this import, and not inheriting from QtIntrospection
Running the tests fails with the following error:
_StringException: Traceback (most recent call last):
File "/tmp/webbrowse
super(
File "/tmp/webbrowse
super(
File "/tmp/webbrowse
self.
File "/tmp/webbrowse
*self.ARGS)
File "/usr/lib/
process = launch_
File "/usr/lib/
path, args = launcher.
AttributeError: 'NoneType' object has no attribute 'prepare_
Looks like a problem in autopilot itself.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michael Zanetti (mzanetti) wrote : | # |
I'd like to see some section in the docs that describe how to upgrade tests to be compatible with this version.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sergio Schvezov (sergiusens) wrote : | # |
Packaging/docs need fixing
$ bzr bd
...
/home/sergiusen
make[1]: *** [override_
make[1]: Leaving directory `/home/
make: *** [build] Error 2
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:184
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://
Click here to trigger a rebuild:
http://
- 185. By Thomi Richards
-
Bump current version to v1.3.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
Hi Michael,
> There is no qmlscene in Qt4. It was the QWidget based "qmlviewer" back then.
> qmlscene is the new Qt5 version which directly paints qml on a openGL
> scenegraph.
Fixed. I'll add a second example that uses 'wmlscene' and specifies that it's for Qt5 as well. Note that the tutorial section of the documentation is unfinished - once that's complete this should become clearer.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
Hi Oliver
> I built a package from this MR, installed on my raring desktop, and tried to
> run the autopilot tests for lp:webbrowser-app.
>
Autopilot 1.3 (the version number has now been updated) contains API breaks from 1.2, you won't be able to run your old tests without changing them. However, see my comments below:
> Listing the tests fails with the following error:
>
> ImportError: cannot import name QtIntrospection
>
> but can be easily fixed by simply removing this import, and not inheriting
> from QtIntrospection
Yes - these Mixin Classes were rather nasty, and were the cause of many errors.
>
> Running the tests fails with the following error:
>
> _StringException: Traceback (most recent call last):
> File "/tmp/webbrowser-
> app/tests/
> setUp
> super(TestMainW
> File "/tmp/webbrowse
> line 161, in setUp
> super(BrowserTe
> File "/tmp/webbrowse
> line 45, in setUp
> self.launch_
> File "/tmp/webbrowse
> line 62, in launch_test_local
> *self.ARGS)
> File "/usr/lib/
> launch_
> process = launch_
> File "/usr/lib/
> line 80, in launch_application
> path, args = launcher.
> AttributeError: 'NoneType' object has no attribute 'prepare_
>
> Looks like a problem in autopilot itself.
Kind of :)
We should raise a better exception in this case, which I have just comitted to this branch:
if launcher is None:
raise RuntimeError(
the AutopilotTestCa
What's happening is that autopilot is trying to guess what kind of introspection support to load - Gtk or Qt. Currently it simply does 'ldd <app_path>' and looks for 'qtcore' or 'gtk' - clearly this isn't going to work for python/Qt or python/Gtk apps. I have plans to extend that auto-detection routine, so it works for python scripts automatically. However, you can specify the introspection type manually, by overriding the pick_app_launcher method. In fact, there' san autopilot test that does exactly that, in autopilot/
class QtTests(
def pick_app_
# force Qt app introspection:
from autopilot.
return QtApplicationLa
def test_can_
path = self.write_
from PyQt4.QtGui import QMainWindow, QAppli...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
> I'd like to see some section in the docs that describe how to upgrade tests to
> be compatible with this version.
Agreed:
- 186. By Thomi Richards
-
Fix problem with Qml launch example in documentation.
- 187. By Thomi Richards
-
Better error when autopilot cannot auto-detect the introspection type.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:185
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 188. By Thomi Richards
-
Marked top-level pages as :orphan: sphinx docs, since we have our own custom html page, instead of an index toctree element.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
Hi Sergio,
> Packaging/docs need fixing
>
> $ bzr bd
> ...
> /home/sergiusen
> area/autopilot-
> isn't included in any toctree
>
Fixed - this is because we have our own custom index page now.
Thanks,
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:188
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 189. By Thomi Richards
-
Merged chris's work with the process managaer.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:189
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 190. By Thomi Richards
-
Made a good start on new, better, introductory tutorial page.
- 191. By Thomi Richards
-
Documentation and setup.py now get version numbers from debian/changelog, rather than maintaining separate strings.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:191
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 192. By Thomi Richards
-
Merged trunk, resolved conflicts.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:192
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 193. By Thomi Richards
-
More docs.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:193
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 194. By Thomi Richards
-
Added the start of a porting document.
- 195. By Thomi Richards
-
Merge in process test fixes.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:195
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 196. By Thomi Richards
-
Turn off intersphinx support since we can't build it anyway, and testtools doesn't use sphinx for it's API docs.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:196
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 197. By Thomi Richards
-
Fix bullet list in porting document.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:197
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === renamed file 'autopilot/emulators/clipboard.py' => 'autopilot/clipboard.py' |
2 | === removed file 'autopilot/compizconfig.py' |
3 | --- autopilot/compizconfig.py 2012-08-27 21:26:45 +0000 |
4 | +++ autopilot/compizconfig.py 1970-01-01 00:00:00 +0000 |
5 | @@ -1,57 +0,0 @@ |
6 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
7 | -# Copyright 2012 Canonical |
8 | -# Author: Thomi Richards |
9 | -# |
10 | -# This program is free software: you can redistribute it and/or modify it |
11 | -# under the terms of the GNU General Public License version 3, as published |
12 | -# by the Free Software Foundation. |
13 | -# |
14 | -# This script is designed to run unity in a test drive manner. It will drive |
15 | -# X and test the GL calls that Unity makes, so that we can easily find out if |
16 | -# we are triggering graphics driver/X bugs. |
17 | - |
18 | -"""Functions that wrap compizconfig to avoid some unpleasantness in that module.""" |
19 | - |
20 | -from __future__ import absolute_import |
21 | - |
22 | - |
23 | -from autopilot.utilities import Silence |
24 | - |
25 | -_global_context = None |
26 | - |
27 | -def get_global_context(): |
28 | - """Get the compizconfig global context object.""" |
29 | - global _global_context |
30 | - if _global_context is None: |
31 | - with Silence(): |
32 | - from compizconfig import Context |
33 | - _global_context = Context() |
34 | - return _global_context |
35 | - |
36 | - |
37 | -def get_plugin(plugin_name): |
38 | - """Get a compizconfig plugin with the specified name. |
39 | - |
40 | - Raises KeyError of the plugin named does not exist. |
41 | - |
42 | - """ |
43 | - ctx = get_global_context() |
44 | - with Silence(): |
45 | - try: |
46 | - return ctx.Plugins[plugin_name] |
47 | - except KeyError: |
48 | - raise KeyError("Compiz plugin '%s' does not exist." % (plugin_name)) |
49 | - |
50 | - |
51 | -def get_setting(plugin_name, setting_name): |
52 | - """Get a compizconfig setting object, given a plugin name and setting name. |
53 | - |
54 | - Raises KeyError if the plugin or setting is not found. |
55 | - |
56 | - """ |
57 | - plugin = get_plugin(plugin_name) |
58 | - with Silence(): |
59 | - try: |
60 | - return plugin.Screen[setting_name] |
61 | - except KeyError: |
62 | - raise KeyError("Compiz setting '%s' does not exist in plugin '%s'." % (setting_name, plugin_name)) |
63 | |
64 | === renamed file 'autopilot/emulators/dbus_handler.py' => 'autopilot/dbus_handler.py' |
65 | === added directory 'autopilot/display' |
66 | === added file 'autopilot/display/_X11.py' |
67 | --- autopilot/display/_X11.py 1970-01-01 00:00:00 +0000 |
68 | +++ autopilot/display/_X11.py 2013-04-15 21:43:32 +0000 |
69 | @@ -0,0 +1,50 @@ |
70 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
71 | +# Copyright 2013 Canonical |
72 | +# Author: Christopher Lee |
73 | +# |
74 | +# This program is free software: you can redistribute it and/or modify it |
75 | +# under the terms of the GNU General Public License version 3, as published |
76 | +# by the Free Software Foundation. |
77 | + |
78 | +import logging |
79 | + |
80 | +from autopilot.display import Display as DisplayBase |
81 | + |
82 | +logger = logging.getLogger(__name__) |
83 | + |
84 | +class Display(DisplayBase): |
85 | + def __init__(self): |
86 | + # Note: MUST import these here, rather than at the top of the file. Why? |
87 | + # Because sphinx imports these modules to build the API documentation, |
88 | + # which in turn tries to import Gdk, which in turn fails because there's |
89 | + # no DISPlAY environment set in the package builder. |
90 | + from gi.repository import Gdk |
91 | + self._default_screen = Gdk.Screen.get_default() |
92 | + self._blacklisted_drivers = ["NVIDIA"] |
93 | + |
94 | + def get_num_screens(self): |
95 | + """Get the number of screens attached to the PC.""" |
96 | + return self._default_screen.get_n_monitors() |
97 | + |
98 | + def get_primary_screen(self): |
99 | + """Returns an integer of which screen is considered the primary""" |
100 | + return self._default_screen.get_primary_monitor() |
101 | + |
102 | + def get_screen_width(self, screen_number=0): |
103 | + # return self._default_screen.get_width() |
104 | + return self.get_screen_geometry(screen_number)[2] |
105 | + |
106 | + def get_screen_height(self, screen_number=0): |
107 | + #return self._default_screen.get_height() |
108 | + return self.get_screen_geometry(screen_number)[3] |
109 | + |
110 | + def get_screen_geometry(self, screen_number): |
111 | + """Get the geometry for a particular screen. |
112 | + |
113 | + :return: Tuple containing (x, y, width, height). |
114 | + |
115 | + """ |
116 | + if screen_number < 0 or screen_number >= self.get_num_screens(): |
117 | + raise ValueError('Specified screen number is out of range.') |
118 | + rect = self._default_screen.get_monitor_geometry(screen_number) |
119 | + return (rect.x, rect.y, rect.width, rect.height) |
120 | |
121 | === added file 'autopilot/display/__init__.py' |
122 | --- autopilot/display/__init__.py 1970-01-01 00:00:00 +0000 |
123 | +++ autopilot/display/__init__.py 2013-04-15 21:43:32 +0000 |
124 | @@ -0,0 +1,136 @@ |
125 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
126 | +# Copyright 2013 Canonical |
127 | +# Author: Christopher Lee |
128 | +# |
129 | +# This program is free software: you can redistribute it and/or modify it |
130 | +# under the terms of the GNU General Public License version 3, as published |
131 | +# by the Free Software Foundation. |
132 | + |
133 | +"""The display module contaions support for getting screen information.""" |
134 | + |
135 | +from collections import OrderedDict |
136 | +from autopilot.utilities import _pick_variant |
137 | +from autopilot.input import Mouse |
138 | + |
139 | + |
140 | +def is_rect_on_screen(screen_number, rect): |
141 | + """Returns True if *rect* is **entirely** on the specified screen, with no overlap.""" |
142 | + (x, y, w, h) = rect |
143 | + (mx, my, mw, mh) = Display.create().get_screen_geometry(screen_number) |
144 | + return (x >= mx and x + w <= mx + mw and y >= my and y + h <= my + mh) |
145 | + |
146 | + |
147 | +def is_point_on_screen(screen_number, point): |
148 | + """Returns True if *point* is on the specified screen. |
149 | + |
150 | + *point* must be an iterable type with two elements: (x, y) |
151 | + |
152 | + """ |
153 | + x, y = point |
154 | + (mx, my, mw, mh) = Display.create().get_screen_geometry(screen_number) |
155 | + return (x >= mx and x < mx + mw and y >= my and y < my + mh) |
156 | + |
157 | + |
158 | +def is_point_on_any_screen(point): |
159 | + """Returns true if *point* is on any currently configured screen.""" |
160 | + return any([is_point_on_screen(m, point) for m in range(Display.create().get_num_screens())]) |
161 | + |
162 | + |
163 | +def move_mouse_to_screen(screen_number): |
164 | + """Move the mouse to the center of the specified screen.""" |
165 | + geo = Display.create().get_screen_geometry(screen_number) |
166 | + x = geo[0] + (geo[2] / 2) |
167 | + y = geo[1] + (geo[3] / 2) |
168 | + #dont animate this or it might not get there due to barriers |
169 | + Mouse.create().move(x, y, False) |
170 | + |
171 | + |
172 | +# veebers TODO: Write this so it's usable. |
173 | +# def drag_window_to_screen(self, window, screen): |
174 | +# """Drags *window* to *screen* |
175 | + |
176 | +# :param BamfWindow window: The window to drag |
177 | +# :param integer screen: The screen to drag the *window* to |
178 | +# :raises: **TypeError** if *window* is not a BamfWindow |
179 | + |
180 | +# """ |
181 | +# if not isinstance(window, BamfWindow): |
182 | +# raise TypeError("Window must be a BamfWindow") |
183 | + |
184 | +# if window.monitor == screen: |
185 | +# logger.debug("Window %r is already on screen %d." % (window.x_id, screen)) |
186 | +# return |
187 | + |
188 | +# assert(not window.is_maximized) |
189 | +# (win_x, win_y, win_w, win_h) = window.geometry |
190 | +# (mx, my, mw, mh) = self.get_screen_geometry(screen) |
191 | + |
192 | +# logger.debug("Dragging window %r to screen %d." % (window.x_id, screen)) |
193 | + |
194 | +# mouse = Mouse() |
195 | +# keyboard = Keyboard() |
196 | +# mouse.move(win_x + win_w/2, win_y + win_h/2) |
197 | +# keyboard.press("Alt") |
198 | +# mouse.press() |
199 | +# keyboard.release("Alt") |
200 | + |
201 | +# # We do the movements in two steps, to reduce the risk of being |
202 | +# # blocked by the pointer barrier |
203 | +# target_x = mx + mw/2 |
204 | +# target_y = my + mh/2 |
205 | +# mouse.move(win_x, target_y, rate=20, time_between_events=0.005) |
206 | +# mouse.move(target_x, target_y, rate=20, time_between_events=0.005) |
207 | +# mouse.release() |
208 | + |
209 | + |
210 | +class Display: |
211 | + """The base class/inteface for the display devices""" |
212 | + |
213 | + @staticmethod |
214 | + def create(preferred_variant=''): |
215 | + """Get an instance of the Display class. |
216 | + |
217 | + If variant is specified, it should be a string that specifies a backend to |
218 | + use. However, this hint can be ignored - autopilot will prefer to return a |
219 | + variant other than the one requested, rather than fail to return anything at |
220 | + all. |
221 | + |
222 | + If autopilot cannot instantate any of the possible backends, a RuntimeError |
223 | + will be raised. |
224 | + """ |
225 | + def get_x11_display(): |
226 | + from autopilot.display._X11 import Display |
227 | + return Display() |
228 | + |
229 | + def get_upa_display(): |
230 | + from autopilot.display._upa import Display |
231 | + return Display() |
232 | + |
233 | + variants = OrderedDict() |
234 | + variants['X11'] = get_x11_display |
235 | + variants['UPA'] = get_upa_display |
236 | + return _pick_variant(variants, preferred_variant) |
237 | + |
238 | + class BlacklistedDriverError(RuntimeError): |
239 | + """Cannot set primary monitor when running drivers listed in the driver blacklist.""" |
240 | + |
241 | + def get_num_screens(self): |
242 | + """Get the number of screens attached to the PC.""" |
243 | + raise NotImplementedError("You cannot use this class directly.") |
244 | + |
245 | + def get_primary_screen(self): |
246 | + raise NotImplementedError("You cannot use this class directly.") |
247 | + |
248 | + def get_screen_width(self, screen_number=0): |
249 | + raise NotImplementedError("You cannot use this class directly.") |
250 | + |
251 | + def get_screen_height(self, screen_number=0): |
252 | + raise NotImplementedError("You cannot use this class directly.") |
253 | + |
254 | + def get_screen_geometry(self, monitor_number): |
255 | + """Get the geometry for a particular monitor. |
256 | + |
257 | + :return: Tuple containing (x, y, width, height). |
258 | + |
259 | + """ |
260 | + raise NotImplementedError("You cannot use this class directly.") |
261 | |
262 | === added file 'autopilot/display/_upa.py' |
263 | --- autopilot/display/_upa.py 1970-01-01 00:00:00 +0000 |
264 | +++ autopilot/display/_upa.py 2013-04-15 21:43:32 +0000 |
265 | @@ -0,0 +1,40 @@ |
266 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
267 | +# Copyright 2013 Canonical |
268 | +# Author: Christopher Lee |
269 | +# |
270 | +# This program is free software: you can redistribute it and/or modify it |
271 | +# under the terms of the GNU General Public License version 3, as published |
272 | +# by the Free Software Foundation. |
273 | + |
274 | +import logging |
275 | + |
276 | +from autopilot.display import Display as DisplayBase |
277 | +from upa import get_resolution |
278 | + |
279 | +logger = logging.getLogger(__name__) |
280 | + |
281 | +class Display(DisplayBase): |
282 | + """The base class/inteface for the display devices""" |
283 | + |
284 | + def get_num_screens(self): |
285 | + """Get the number of screens attached to the PC.""" |
286 | + return 1 |
287 | + |
288 | + def get_primary_screen(self): |
289 | + """Returns an integer of which screen is considered the primary""" |
290 | + return 0 |
291 | + |
292 | + def get_screen_width(self): |
293 | + return get_resolution()[0] |
294 | + |
295 | + def get_screen_height(self): |
296 | + return get_resolution()[1] |
297 | + |
298 | + def get_screen_geometry(self, screen_number): |
299 | + """Get the geometry for a particular screen. |
300 | + |
301 | + :return: Tuple containing (x, y, width, height). |
302 | + |
303 | + """ |
304 | + res = get_resolution() |
305 | + return (0, 0, res[0], res[1]) |
306 | |
307 | === removed directory 'autopilot/emulators' |
308 | === added file 'autopilot/emulators.py' |
309 | --- autopilot/emulators.py 1970-01-01 00:00:00 +0000 |
310 | +++ autopilot/emulators.py 2013-04-15 21:43:32 +0000 |
311 | @@ -0,0 +1,21 @@ |
312 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
313 | +# Copyright 2013 Canonical |
314 | +# Author: Thomi Richards |
315 | +# |
316 | +# This program is free software: you can redistribute it and/or modify it |
317 | +# under the terms of the GNU General Public License version 3, as published |
318 | +# by the Free Software Foundation. |
319 | +# |
320 | +# This script is designed to run unity in a test drive manner. It will drive |
321 | +# X and test the GL calls that Unity makes, so that we can easily find out if |
322 | +# we are triggering graphics driver/X bugs. |
323 | + |
324 | +import autopilot.display as display |
325 | +import autopilot.clipboard as clipboard |
326 | +import autopilot.dbus_handler as dbus_handler |
327 | +import autopilot.ibus as ibus |
328 | +import autopilot.input as input |
329 | + |
330 | + |
331 | +""" |
332 | +""" |
333 | |
334 | === removed file 'autopilot/emulators/X11.py' |
335 | --- autopilot/emulators/X11.py 2013-03-03 21:21:08 +0000 |
336 | +++ autopilot/emulators/X11.py 1970-01-01 00:00:00 +0000 |
337 | @@ -1,187 +0,0 @@ |
338 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
339 | -# Copyright 2010 Canonical |
340 | -# Author: Alex Launi |
341 | -# |
342 | -# This program is free software: you can redistribute it and/or modify it |
343 | -# under the terms of the GNU General Public License version 3, as published |
344 | -# by the Free Software Foundation. |
345 | -# |
346 | -# This script is designed to run unity in a test drive manner. It will drive |
347 | -# X and test the GL calls that Unity makes, so that we can easily find out if |
348 | -# we are triggering graphics driver/X bugs. |
349 | - |
350 | -"""A collection of emulators for X11 - namely keyboards and mice. |
351 | - |
352 | -In the future we may also need other devices. |
353 | - |
354 | -""" |
355 | - |
356 | -from __future__ import absolute_import |
357 | - |
358 | -import logging |
359 | -import os |
360 | -import subprocess |
361 | - |
362 | -from autopilot.emulators.bamf import BamfWindow |
363 | -from autopilot.emulators.input import get_keyboard, get_mouse |
364 | -from autopilot.utilities import deprecated |
365 | - |
366 | - |
367 | -logger = logging.getLogger(__name__) |
368 | - |
369 | - |
370 | -def reset_display(): |
371 | - from autopilot.emulators.input._X11 import reset_display |
372 | - reset_display() |
373 | - |
374 | - |
375 | -# Keyboard and Mouse are no longer here. This is for backwards compatibility, |
376 | -# but will eventually dissapear. |
377 | -@deprecated('autopilot.emulators.input.get_keyboard') |
378 | -def Keyboard(): |
379 | - return get_keyboard() |
380 | - |
381 | - |
382 | -@deprecated('autopilot.emulators.input.get_mouse') |
383 | -def Mouse(): |
384 | - return get_mouse() |
385 | - |
386 | - |
387 | -class ScreenGeometry: |
388 | - """Get details about screen geometry.""" |
389 | - |
390 | - class BlacklistedDriverError(RuntimeError): |
391 | - """Cannot set primary monitor when running drivers listed in the driver blacklist.""" |
392 | - |
393 | - def __init__(self): |
394 | - # Note: MUST import these here, rather than at the top of the file. Why? |
395 | - # Because sphinx imports these modules to build the API documentation, |
396 | - # which in turn tries to import Gdk, which in turn fails because there's |
397 | - # no DISPlAY environment set in the package builder. |
398 | - from gi.repository import Gdk |
399 | - self._default_screen = Gdk.Screen.get_default() |
400 | - self._blacklisted_drivers = ["NVIDIA"] |
401 | - |
402 | - def get_num_monitors(self): |
403 | - """Get the number of monitors attached to the PC.""" |
404 | - return self._default_screen.get_n_monitors() |
405 | - |
406 | - def get_primary_monitor(self): |
407 | - return self._default_screen.get_primary_monitor() |
408 | - |
409 | - def set_primary_monitor(self, monitor): |
410 | - """Set *monitor* to be the primary monitor. |
411 | - |
412 | - :param int monitor: Must be between 0 and the number of configured |
413 | - monitors. |
414 | - :raises: **ValueError** if an invalid monitor is specified. |
415 | - :raises: **BlacklistedDriverError** if your video driver does not |
416 | - support this. |
417 | - |
418 | - """ |
419 | - try: |
420 | - glxinfo_out = subprocess.check_output("glxinfo") |
421 | - except OSError, e: |
422 | - raise OSError("Failed to run glxinfo: %s. (do you have mesa-utils installed?)" % e) |
423 | - |
424 | - for dri in self._blacklisted_drivers: |
425 | - if dri in glxinfo_out: |
426 | - raise ScreenGeometry.BlacklistedDriverError('Impossible change the primary monitor for the given driver') |
427 | - |
428 | - if monitor < 0 or monitor >= self.get_num_monitors(): |
429 | - raise ValueError('Monitor %d is not in valid range of 0 <= monitor < %d.' % (self.get_num_monitors())) |
430 | - |
431 | - monitor_name = self._default_screen.get_monitor_plug_name(monitor) |
432 | - |
433 | - if not monitor_name: |
434 | - raise ValueError('Could not get monitor name from monitor number %d.' % (monitor)) |
435 | - |
436 | - ret = os.spawnlp(os.P_WAIT, "xrandr", "xrandr", "--output", monitor_name, "--primary") |
437 | - |
438 | - if ret != 0: |
439 | - raise RuntimeError('Xrandr can\'t set the primary monitor. error code: %d' % (ret)) |
440 | - |
441 | - def get_screen_width(self): |
442 | - return self._default_screen.get_width() |
443 | - |
444 | - def get_screen_height(self): |
445 | - return self._default_screen.get_height() |
446 | - |
447 | - def get_monitor_geometry(self, monitor_number): |
448 | - """Get the geometry for a particular monitor. |
449 | - |
450 | - :return: Tuple containing (x, y, width, height). |
451 | - |
452 | - """ |
453 | - if monitor_number < 0 or monitor_number >= self.get_num_monitors(): |
454 | - raise ValueError('Specified monitor number is out of range.') |
455 | - rect = self._default_screen.get_monitor_geometry(monitor_number) |
456 | - return (rect.x, rect.y, rect.width, rect.height) |
457 | - |
458 | - def is_rect_on_monitor(self, monitor_number, rect): |
459 | - """Returns True if *rect* is **entirely** on the specified monitor, with no overlap.""" |
460 | - |
461 | - if type(rect) is not tuple or len(rect) != 4: |
462 | - raise TypeError("rect must be a tuple of 4 int elements.") |
463 | - |
464 | - (x, y, w, h) = rect |
465 | - (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number) |
466 | - return (x >= mx and x + w <= mx + mw and y >= my and y + h <= my + mh) |
467 | - |
468 | - def is_point_on_monitor(self, monitor_number, point): |
469 | - """Returns True if *point* is on the specified monitor. |
470 | - |
471 | - *point* must be an iterable type with two elements: (x, y) |
472 | - |
473 | - """ |
474 | - x,y = point |
475 | - (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number) |
476 | - return (x >= mx and x < mx + mw and y >= my and y < my + mh) |
477 | - |
478 | - def is_point_on_any_monitor(self, point): |
479 | - """Returns true if *point* is on any currently configured monitor.""" |
480 | - return any([self.is_point_on_monitor(m, point) for m in range(self.get_num_monitors())]) |
481 | - |
482 | - def move_mouse_to_monitor(self, monitor_number): |
483 | - """Move the mouse to the center of the specified monitor.""" |
484 | - geo = self.get_monitor_geometry(monitor_number) |
485 | - x = geo[0] + (geo[2] / 2) |
486 | - y = geo[1] + (geo[3] / 2) |
487 | - #dont animate this or it might not get there due to barriers |
488 | - Mouse().move(x, y, False) |
489 | - |
490 | - def drag_window_to_monitor(self, window, monitor): |
491 | - """Drags *window* to *monitor* |
492 | - |
493 | - :param BamfWindow window: The window to drag |
494 | - :param integer monitor: The monitor to drag the *window* to |
495 | - :raises: **TypeError** if *window* is not a BamfWindow |
496 | - |
497 | - """ |
498 | - if not isinstance(window, BamfWindow): |
499 | - raise TypeError("Window must be a BamfWindow") |
500 | - |
501 | - if window.monitor == monitor: |
502 | - logger.debug("Window %r is already on monitor %d." % (window.x_id, monitor)) |
503 | - return |
504 | - |
505 | - assert(not window.is_maximized) |
506 | - (win_x, win_y, win_w, win_h) = window.geometry |
507 | - (mx, my, mw, mh) = self.get_monitor_geometry(monitor) |
508 | - |
509 | - logger.debug("Dragging window %r to monitor %d." % (window.x_id, monitor)) |
510 | - |
511 | - mouse = Mouse() |
512 | - keyboard = Keyboard() |
513 | - mouse.move(win_x + win_w/2, win_y + win_h/2) |
514 | - keyboard.press("Alt") |
515 | - mouse.press() |
516 | - keyboard.release("Alt") |
517 | - |
518 | - # We do the movements in two steps, to reduce the risk of being |
519 | - # blocked by the pointer barrier |
520 | - target_x = mx + mw/2 |
521 | - target_y = my + mh/2 |
522 | - mouse.move(win_x, target_y, rate=20, time_between_events=0.005) |
523 | - mouse.move(target_x, target_y, rate=20, time_between_events=0.005) |
524 | - mouse.release() |
525 | |
526 | === removed file 'autopilot/emulators/__init__.py' |
527 | --- autopilot/emulators/__init__.py 2012-08-20 23:57:20 +0000 |
528 | +++ autopilot/emulators/__init__.py 1970-01-01 00:00:00 +0000 |
529 | @@ -1,11 +0,0 @@ |
530 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
531 | -# Copyright 2012 Canonical |
532 | -# Author: Thomi Richards |
533 | -# |
534 | -# This program is free software: you can redistribute it and/or modify it |
535 | -# under the terms of the GNU General Public License version 3, as published |
536 | -# by the Free Software Foundation. |
537 | - |
538 | -""" |
539 | -A collection of emulators that make it easier to interact with X11 and Unity. |
540 | -""" |
541 | |
542 | === removed file 'autopilot/emulators/processmanager.py' |
543 | --- autopilot/emulators/processmanager.py 2012-06-07 04:52:04 +0000 |
544 | +++ autopilot/emulators/processmanager.py 1970-01-01 00:00:00 +0000 |
545 | @@ -1,66 +0,0 @@ |
546 | -# Copyright 2012 Canonical |
547 | -# Author: Thomi Richards |
548 | -# |
549 | -# This program is free software: you can redistribute it and/or modify it |
550 | -# under the terms of the GNU General Public License version 3, as published |
551 | -# by the Free Software Foundation. |
552 | - |
553 | -"""The processmanager module contains utilities for starting, stopping, and |
554 | -generally managing processes during a test.""" |
555 | - |
556 | -from __future__ import absolute_import |
557 | -import logging |
558 | -from time import sleep |
559 | - |
560 | -from autopilot.emulators.bamf import Bamf |
561 | - |
562 | - |
563 | -logger = logging.getLogger(__name__) |
564 | - |
565 | -class ProcessManager(object): |
566 | - """Manage Processes during a test cycle.""" |
567 | - |
568 | - def __init__(self): |
569 | - self._bamf = Bamf() |
570 | - self.snapshot = None |
571 | - |
572 | - def snapshot_running_apps(self): |
573 | - """Make a list of all the running applications, and store it. |
574 | - |
575 | - The stored list can later be used to detect any applications that have |
576 | - been launched during a test and not shut down. |
577 | - |
578 | - You may only call this method once before calling |
579 | - compare_system_with_snapshot. Calling this method multiple times will |
580 | - cause a RuntimeError to be raised. |
581 | - """ |
582 | - |
583 | - if self.snapshot: |
584 | - raise RuntimeError("You may only call snapshot_running_apps once \ |
585 | -before calling compare_system_with_snapshot.") |
586 | - |
587 | - self.snapshot = self._bamf.get_running_applications() |
588 | - |
589 | - def compare_system_with_snapshot(self): |
590 | - """Compare the currently running application with the last snapshot. |
591 | - |
592 | - This method will raise an AssertionError if there are any new applications |
593 | - currently running that were not running when the snapshot was taken. |
594 | - |
595 | - This method should typically be called at the every end of a test. |
596 | - """ |
597 | - if self.snapshot is None: |
598 | - raise RuntimeError("No snapshot to match against.") |
599 | - |
600 | - new_apps = [] |
601 | - for i in range(10): |
602 | - current_apps = self._bamf.get_running_applications() |
603 | - new_apps = filter(lambda i: i not in self.snapshot, current_apps) |
604 | - if not new_apps: |
605 | - self.snapshot = None |
606 | - return |
607 | - sleep(1) |
608 | - self.snapshot = None |
609 | - raise AssertionError("The following apps were started during the test and not closed: %r", new_apps) |
610 | - |
611 | - |
612 | |
613 | === removed file 'autopilot/emulators/zeitgeist.py' |
614 | --- autopilot/emulators/zeitgeist.py 2012-10-26 14:15:43 +0000 |
615 | +++ autopilot/emulators/zeitgeist.py 1970-01-01 00:00:00 +0000 |
616 | @@ -1,66 +0,0 @@ |
617 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
618 | -# Copyright 2012 Canonical |
619 | -# Author: Brandon Schaefer |
620 | -# |
621 | -# This program is free software: you can redistribute it and/or modify it |
622 | -# under the terms of the GNU General Public License version 3, as published |
623 | -# by the Free Software Foundation. |
624 | - |
625 | -"""Provide ability to register text files with the file lens.""" |
626 | - |
627 | -from __future__ import absolute_import |
628 | - |
629 | -import logging |
630 | -import os.path |
631 | -from zeitgeist.client import ZeitgeistClient |
632 | -from zeitgeist.datamodel import Event, Interpretation, Manifestation, ResultType |
633 | - |
634 | - |
635 | -class Zeitgeist(object): |
636 | - """Provide access zeitgeist.""" |
637 | - |
638 | - def __init__(self): |
639 | - self.zg = ZeitgeistClient() |
640 | - self.logger = logging.getLogger(__name__) |
641 | - |
642 | - def add_existing_file(self, path): |
643 | - """Registers *file* with zeitgeist. |
644 | - |
645 | - :param string file: full path to an existing text file to register. |
646 | - :raises: **RuntimeError** if *file* does not exist. |
647 | - |
648 | - """ |
649 | - if os.path.exists(path): |
650 | - self.__add_text_file(path) |
651 | - else: |
652 | - raise RuntimeError("File not found on path: %s." % (path)) |
653 | - |
654 | - def __add_text_file(self, path): |
655 | - """Takes a path to a file and creates an event for it then querys it.""" |
656 | - file_lens = "file://" |
657 | - dir_path = os.path.dirname(path) |
658 | - name = os.path.basename(path) |
659 | - |
660 | - event = Event.new_for_values (interpretation=Interpretation.ACCESS_EVENT, |
661 | - manifestation=Manifestation.USER_ACTIVITY, |
662 | - subject_uri=file_lens + path, |
663 | - subject_interpretation=Interpretation.TEXT_DOCUMENT, |
664 | - subject_manifestation=Manifestation.FILE_DATA_OBJECT, |
665 | - subject_origin=file_lens + dir_path, |
666 | - subject_text=name) |
667 | - self.zg.insert_event(event) |
668 | - |
669 | - template = Event.new_for_values (interpretation=Interpretation.ACCESS_EVENT, |
670 | - manifestation=Manifestation.USER_ACTIVITY) |
671 | - |
672 | - self.zg.find_events_for_template (template, |
673 | - self.__log_events_cb, |
674 | - num_events=1, |
675 | - result_type=ResultType.MostRecentSubjects) |
676 | - |
677 | - def __log_events_cb(self, events): |
678 | - """Callback to recive events, we are just using it to log each event.""" |
679 | - self.logger.info("Found Events") |
680 | - for event in events: |
681 | - for subject in event.subjects: |
682 | - self.logger.info(" * %s" % (subject.uri)) |
683 | |
684 | === added file 'autopilot/gestures.py' |
685 | --- autopilot/gestures.py 1970-01-01 00:00:00 +0000 |
686 | +++ autopilot/gestures.py 2013-04-15 21:43:32 +0000 |
687 | @@ -0,0 +1,62 @@ |
688 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
689 | +# Copyright 2013 Canonical |
690 | +# Author: Thomi Richards |
691 | +# |
692 | +# This program is free software: you can redistribute it and/or modify it |
693 | +# under the terms of the GNU General Public License version 3, as published |
694 | +# by the Free Software Foundation. |
695 | + |
696 | +"""Gestural support for autopilot. |
697 | + |
698 | +This module contains functions that can generate touch and multi-tuch gestures |
699 | +for you. This is a convenience for the test author - there is nothing to prevent |
700 | +you from generating your own gestures! |
701 | + |
702 | +""" |
703 | + |
704 | +from autopilot.input import Touch |
705 | +from time import sleep |
706 | + |
707 | + |
708 | +def pinch(center, vector_start, vector_end): |
709 | + """Perform a two finger pinch (zoom) gesture. |
710 | + |
711 | + :param center: The coordinates (x,y) of the center of the pinch gesture. |
712 | + :param vector_start: The (x,y) values to move away from the center for the start. |
713 | + :param vector_end: The (x,y) values to move away from the center for the end. |
714 | + |
715 | + The fingers will move in 100 steps between the start and the end points. |
716 | + If start is smaller than end, the gesture will zoom in, otherwise it |
717 | + will zoom out. |
718 | + |
719 | + """ |
720 | + |
721 | + finger_1_start = [center[0] - vector_start[0], center[1] - vector_start[1]] |
722 | + finger_2_start = [center[0] + vector_start[0], center[1] + vector_start[1]] |
723 | + finger_1_end = [center[0] - vector_end[0], center[1] - vector_end[1]] |
724 | + finger_2_end = [center[0] + vector_end[0], center[1] + vector_end[1]] |
725 | + |
726 | + dx = 1.0 * (finger_1_end[0] - finger_1_start[0]) / 100 |
727 | + dy = 1.0 * (finger_1_end[1] - finger_1_start[1]) / 100 |
728 | + |
729 | + finger_1 = Touch.create() |
730 | + finger_2 = Touch.create() |
731 | + |
732 | + finger_1.press(*finger_1_start) |
733 | + finger_2.press(*finger_2_start) |
734 | + |
735 | + finger_1_cur = [finger_1_start[0] + dx, finger_1_start[1] + dy] |
736 | + finger_2_cur = [finger_2_start[0] - dx, finger_2_start[1] - dy] |
737 | + |
738 | + for i in range(0, 100): |
739 | + finger_1.move(*finger_1_cur) |
740 | + finger_2.move(*finger_2_cur) |
741 | + sleep(0.005) |
742 | + |
743 | + finger_1_cur = [finger_1_cur[0] + dx, finger_1_cur[1] + dy] |
744 | + finger_2_cur = [finger_2_cur[0] - dx, finger_2_cur[1] - dy] |
745 | + |
746 | + finger_1.move(*finger_1_end) |
747 | + finger_2.move(*finger_2_end) |
748 | + finger_1.release() |
749 | + finger_2.release() |
750 | |
751 | === removed file 'autopilot/glibrunner.py' |
752 | --- autopilot/glibrunner.py 2012-09-26 23:21:20 +0000 |
753 | +++ autopilot/glibrunner.py 1970-01-01 00:00:00 +0000 |
754 | @@ -1,26 +0,0 @@ |
755 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
756 | -# Copyright 2012 Canonical |
757 | -# Author: Thomi Richards |
758 | -# |
759 | -# This program is free software: you can redistribute it and/or modify it |
760 | -# under the terms of the GNU General Public License version 3, as published |
761 | -# by the Free Software Foundation. |
762 | - |
763 | -from __future__ import absolute_import |
764 | - |
765 | -import testtools |
766 | - |
767 | -try: |
768 | - import faulthandler |
769 | - faulthandler.enable() |
770 | -except: |
771 | - pass |
772 | - |
773 | - |
774 | -__all__ = [ |
775 | - 'AutopilotTestRunner', |
776 | - ] |
777 | - |
778 | -class AutopilotTestRunner(testtools.RunTest): |
779 | - pass |
780 | - |
781 | |
782 | === modified file 'autopilot/globals.py' |
783 | --- autopilot/globals.py 2012-09-19 23:31:57 +0000 |
784 | +++ autopilot/globals.py 2013-04-15 21:43:32 +0000 |
785 | @@ -7,12 +7,16 @@ |
786 | # by the Free Software Foundation. |
787 | |
788 | from __future__ import absolute_import |
789 | - |
790 | -# this can be set to True, in which case tests will be recorded. |
791 | -__video_recording_enabled = False |
792 | - |
793 | -# this is where videos will be put after being encoded. |
794 | -__video_record_directory = "/tmp/autopilot" |
795 | +from StringIO import StringIO |
796 | +from autopilot.utilities import LogFormatter |
797 | +from testtools.content import text_content |
798 | +import subprocess |
799 | +import os.path |
800 | +import logging |
801 | +from autopilot.utilities import addCleanup |
802 | + |
803 | +logger = logging.getLogger(__name__) |
804 | + |
805 | |
806 | # if set to true, autopilot will output all pythong logging to stderr |
807 | __log_verbose = False |
808 | @@ -23,12 +27,114 @@ |
809 | return __log_verbose |
810 | |
811 | |
812 | +class _TestLogger(object): |
813 | + """A class that handles adding test logs as test result content.""" |
814 | + |
815 | + def __call__(self, test_instance): |
816 | + self._setUpTestLogging(test_instance) |
817 | + if get_log_verbose(): |
818 | + global logger |
819 | + logger.info("*" * 60) |
820 | + logger.info("Starting test %s", test_instance.shortDescription()) |
821 | + |
822 | + def _setUpTestLogging(self, test_instance): |
823 | + self._log_buffer = StringIO() |
824 | + root_logger = logging.getLogger() |
825 | + root_logger.setLevel(logging.DEBUG) |
826 | + formatter = LogFormatter() |
827 | + self._log_handler = logging.StreamHandler(stream=self._log_buffer) |
828 | + self._log_handler.setFormatter(formatter) |
829 | + root_logger.addHandler(self._log_handler) |
830 | + test_instance.addCleanup(self._tearDownLogging, test_instance) |
831 | + |
832 | + def _tearDownLogging(self, test_instance): |
833 | + root_logger = logging.getLogger() |
834 | + self._log_handler.flush() |
835 | + self._log_buffer.seek(0) |
836 | + test_instance.addDetail('test-log', text_content(self._log_buffer.getvalue())) |
837 | + root_logger.removeHandler(self._log_handler) |
838 | + # Calling del to remove the handler and flush the buffer. We are |
839 | + # abusing the log handlers here a little. |
840 | + del self._log_buffer |
841 | + |
842 | + |
843 | def set_log_verbose(verbose): |
844 | """Set whether or not we should log verbosely.""" |
845 | + |
846 | if type(verbose) is not bool: |
847 | raise TypeError("Verbose flag must be a boolean.") |
848 | global __log_verbose |
849 | __log_verbose = verbose |
850 | + if verbose: |
851 | + logger = _TestLogger() |
852 | + _on_test_started_call.append(logger) |
853 | + |
854 | + |
855 | +class _VideoCaptureTestCase(object): |
856 | + """Video capture autopilot tests, saving the results if the test failed.""" |
857 | + |
858 | + _recording_app = '/usr/bin/recordmydesktop' |
859 | + _recording_opts = ['--no-sound', '--no-frame', '-o',] |
860 | + |
861 | + def __init__(self, recording_directory): |
862 | + self.recording_directory = recording_directory |
863 | + |
864 | + def __call__(self, test_instance): |
865 | + if not self._have_recording_app(): |
866 | + logger.warning("Disabling video capture since '%s' is not present", self._recording_app) |
867 | + |
868 | + self._test_passed = True |
869 | + test_instance.addOnException(self._on_test_failed) |
870 | + test_instance.addCleanup(self._stop_video_capture, test_instance) |
871 | + self._start_video_capture(test_instance.shortDescription()) |
872 | + |
873 | + def _have_recording_app(self): |
874 | + return os.path.exists(self._recording_app) |
875 | + |
876 | + def _start_video_capture(self, test_id): |
877 | + args = self._get_capture_command_line() |
878 | + self._capture_file = os.path.join( |
879 | + self.recording_directory, |
880 | + '%s.ogv' % (test_id) |
881 | + ) |
882 | + self._ensure_directory_exists_but_not_file(self._capture_file) |
883 | + args.append(self._capture_file) |
884 | + logger.debug("Starting: %r", args) |
885 | + self._capture_process = subprocess.Popen( |
886 | + args, |
887 | + stdout=subprocess.PIPE, |
888 | + stderr=subprocess.STDOUT |
889 | + ) |
890 | + |
891 | + def _stop_video_capture(self, test_instance): |
892 | + """Stop the video capture. If the test failed, save the resulting file.""" |
893 | + |
894 | + if self._test_passed: |
895 | + # We use kill here because we don't want the recording app to start |
896 | + # encoding the video file (since we're removing it anyway.) |
897 | + self._capture_process.kill() |
898 | + self._capture_process.wait() |
899 | + else: |
900 | + self._capture_process.terminate() |
901 | + self._capture_process.wait() |
902 | + if self._capture_process.returncode != 0: |
903 | + test_instance.addDetail('video capture log', text_content(self._capture_process.stdout.read())) |
904 | + self._capture_process = None |
905 | + |
906 | + def _get_capture_command_line(self): |
907 | + return [self._recording_app] + self._recording_opts |
908 | + |
909 | + def _ensure_directory_exists_but_not_file(self, file_path): |
910 | + dirpath = os.path.dirname(file_path) |
911 | + if not os.path.exists(dirpath): |
912 | + os.makedirs(dirpath) |
913 | + elif os.path.exists(file_path): |
914 | + logger.warning("Video capture file '%s' already exists, deleting.", file_path) |
915 | + os.remove(file_path) |
916 | + |
917 | + def _on_test_failed(self, ex_info): |
918 | + """Called when a test fails.""" |
919 | + self._test_passed = False |
920 | |
921 | |
922 | def configure_video_recording(enable_recording, record_dir): |
923 | @@ -43,18 +149,14 @@ |
924 | if not isinstance(record_dir, basestring): |
925 | raise TypeError("record_dir must be a string.") |
926 | |
927 | - global __video_recording_enabled |
928 | - global __video_record_directory |
929 | - |
930 | - __video_recording_enabled = enable_recording |
931 | - __video_record_directory = record_dir |
932 | - |
933 | - |
934 | -def get_video_recording_enabled(): |
935 | - global __video_recording_enabled |
936 | - return __video_recording_enabled |
937 | - |
938 | - |
939 | -def get_video_record_directory(): |
940 | - global __video_record_directory |
941 | - return __video_record_directory |
942 | + if enable_recording: |
943 | + recorder = _VideoCaptureTestCase(record_dir) |
944 | + _on_test_started_call.append(recorder) |
945 | + |
946 | + |
947 | +_on_test_started_call = [addCleanup.set_test_instance] |
948 | + |
949 | +def on_test_started(test_case_instance): |
950 | + global _on_test_started_call |
951 | + for fun in _on_test_started_call: |
952 | + fun(test_case_instance) |
953 | |
954 | === renamed file 'autopilot/emulators/ibus.py' => 'autopilot/ibus.py' |
955 | === renamed directory 'autopilot/emulators/input' => 'autopilot/input' |
956 | === modified file 'autopilot/input/_X11.py' |
957 | --- autopilot/emulators/input/_X11.py 2013-02-28 03:18:15 +0000 |
958 | +++ autopilot/input/_X11.py 2013-04-15 21:43:32 +0000 |
959 | @@ -19,13 +19,11 @@ |
960 | from __future__ import absolute_import |
961 | |
962 | import logging |
963 | -import os |
964 | -import subprocess |
965 | from time import sleep |
966 | |
967 | -from autopilot.emulators.bamf import BamfWindow |
968 | +from autopilot.display import is_point_on_any_screen, move_mouse_to_screen |
969 | from autopilot.utilities import Silence |
970 | -from autopilot.emulators.input import ( |
971 | +from autopilot.input import ( |
972 | Keyboard as KeyboardBase, |
973 | Mouse as MouseBase, |
974 | ) |
975 | @@ -321,7 +319,7 @@ |
976 | |
977 | dest_x, dest_y = x, y |
978 | curr_x, curr_y = self.position() |
979 | - coordinate_valid = ScreenGeometry().is_point_on_any_monitor((x,y)) |
980 | + coordinate_valid = is_point_on_any_screen((x,y)) |
981 | |
982 | while curr_x != dest_x or curr_y != dest_y: |
983 | dx = abs(dest_x - curr_x) |
984 | @@ -416,145 +414,4 @@ |
985 | logger.debug("Releasing mouse button %d as part of cleanup", btn) |
986 | fake_input(get_display(), X.ButtonRelease, btn) |
987 | _PRESSED_MOUSE_BUTTONS = [] |
988 | - sg = ScreenGeometry() |
989 | - sg.move_mouse_to_monitor(0) |
990 | - |
991 | - |
992 | -class ScreenGeometry: |
993 | - """Get details about screen geometry.""" |
994 | - |
995 | - class BlacklistedDriverError(RuntimeError): |
996 | - """Cannot set primary monitor when running drivers listed in the driver blacklist.""" |
997 | - |
998 | - def __init__(self): |
999 | - # Note: MUST import these here, rather than at the top of the file. Why? |
1000 | - # Because sphinx imports these modules to build the API documentation, |
1001 | - # which in turn tries to import Gdk, which in turn fails because there's |
1002 | - # no DISPlAY environment set in the package builder. |
1003 | - from gi.repository import Gdk |
1004 | - self._default_screen = Gdk.Screen.get_default() |
1005 | - self._blacklisted_drivers = ["NVIDIA"] |
1006 | - |
1007 | - def get_num_monitors(self): |
1008 | - """Get the number of monitors attached to the PC.""" |
1009 | - return self._default_screen.get_n_monitors() |
1010 | - |
1011 | - def get_primary_monitor(self): |
1012 | - return self._default_screen.get_primary_monitor() |
1013 | - |
1014 | - def set_primary_monitor(self, monitor): |
1015 | - """Set *monitor* to be the primary monitor. |
1016 | - |
1017 | - :param int monitor: Must be between 0 and the number of configured |
1018 | - monitors. |
1019 | - :raises: **ValueError** if an invalid monitor is specified. |
1020 | - :raises: **BlacklistedDriverError** if your video driver does not |
1021 | - support this. |
1022 | - |
1023 | - """ |
1024 | - try: |
1025 | - glxinfo_out = subprocess.check_output("glxinfo") |
1026 | - except OSError, e: |
1027 | - raise OSError("Failed to run glxinfo: %s. (do you have mesa-utils installed?)" % e) |
1028 | - |
1029 | - for dri in self._blacklisted_drivers: |
1030 | - if dri in glxinfo_out: |
1031 | - raise ScreenGeometry.BlacklistedDriverError('Impossible change the primary monitor for the given driver') |
1032 | - |
1033 | - if monitor < 0 or monitor >= self.get_num_monitors(): |
1034 | - raise ValueError('Monitor %d is not in valid range of 0 <= monitor < %d.' % (self.get_num_monitors())) |
1035 | - |
1036 | - monitor_name = self._default_screen.get_monitor_plug_name(monitor) |
1037 | - |
1038 | - if not monitor_name: |
1039 | - raise ValueError('Could not get monitor name from monitor number %d.' % (monitor)) |
1040 | - |
1041 | - ret = os.spawnlp(os.P_WAIT, "xrandr", "xrandr", "--output", monitor_name, "--primary") |
1042 | - |
1043 | - if ret != 0: |
1044 | - raise RuntimeError('Xrandr can\'t set the primary monitor. error code: %d' % (ret)) |
1045 | - |
1046 | - def get_screen_width(self): |
1047 | - return self._default_screen.get_width() |
1048 | - |
1049 | - def get_screen_height(self): |
1050 | - return self._default_screen.get_height() |
1051 | - |
1052 | - def get_monitor_geometry(self, monitor_number): |
1053 | - """Get the geometry for a particular monitor. |
1054 | - |
1055 | - :return: Tuple containing (x, y, width, height). |
1056 | - |
1057 | - """ |
1058 | - if monitor_number < 0 or monitor_number >= self.get_num_monitors(): |
1059 | - raise ValueError('Specified monitor number is out of range.') |
1060 | - rect = self._default_screen.get_monitor_geometry(monitor_number) |
1061 | - return (rect.x, rect.y, rect.width, rect.height) |
1062 | - |
1063 | - def is_rect_on_monitor(self, monitor_number, rect): |
1064 | - """Returns True if *rect* is **entirely** on the specified monitor, with no overlap.""" |
1065 | - |
1066 | - if type(rect) is not tuple or len(rect) != 4: |
1067 | - raise TypeError("rect must be a tuple of 4 int elements.") |
1068 | - |
1069 | - (x, y, w, h) = rect |
1070 | - (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number) |
1071 | - return (x >= mx and x + w <= mx + mw and y >= my and y + h <= my + mh) |
1072 | - |
1073 | - def is_point_on_monitor(self, monitor_number, point): |
1074 | - """Returns True if *point* is on the specified monitor. |
1075 | - |
1076 | - *point* must be an iterable type with two elements: (x, y) |
1077 | - |
1078 | - """ |
1079 | - x,y = point |
1080 | - (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number) |
1081 | - return (x >= mx and x < mx + mw and y >= my and y < my + mh) |
1082 | - |
1083 | - def is_point_on_any_monitor(self, point): |
1084 | - """Returns true if *point* is on any currently configured monitor.""" |
1085 | - return any([self.is_point_on_monitor(m, point) for m in range(self.get_num_monitors())]) |
1086 | - |
1087 | - def move_mouse_to_monitor(self, monitor_number): |
1088 | - """Move the mouse to the center of the specified monitor.""" |
1089 | - geo = self.get_monitor_geometry(monitor_number) |
1090 | - x = geo[0] + (geo[2] / 2) |
1091 | - y = geo[1] + (geo[3] / 2) |
1092 | - #dont animate this or it might not get there due to barriers |
1093 | - Mouse().move(x, y, False) |
1094 | - |
1095 | - def drag_window_to_monitor(self, window, monitor): |
1096 | - """Drags *window* to *monitor* |
1097 | - |
1098 | - :param BamfWindow window: The window to drag |
1099 | - :param integer monitor: The monitor to drag the *window* to |
1100 | - :raises: **TypeError** if *window* is not a BamfWindow |
1101 | - |
1102 | - """ |
1103 | - if not isinstance(window, BamfWindow): |
1104 | - raise TypeError("Window must be a BamfWindow") |
1105 | - |
1106 | - if window.monitor == monitor: |
1107 | - logger.debug("Window %r is already on monitor %d." % (window.x_id, monitor)) |
1108 | - return |
1109 | - |
1110 | - assert(not window.is_maximized) |
1111 | - (win_x, win_y, win_w, win_h) = window.geometry |
1112 | - (mx, my, mw, mh) = self.get_monitor_geometry(monitor) |
1113 | - |
1114 | - logger.debug("Dragging window %r to monitor %d." % (window.x_id, monitor)) |
1115 | - |
1116 | - mouse = Mouse() |
1117 | - keyboard = Keyboard() |
1118 | - mouse.move(win_x + win_w/2, win_y + win_h/2) |
1119 | - keyboard.press("Alt") |
1120 | - mouse.press() |
1121 | - keyboard.release("Alt") |
1122 | - |
1123 | - # We do the movements in two steps, to reduce the risk of being |
1124 | - # blocked by the pointer barrier |
1125 | - target_x = mx + mw/2 |
1126 | - target_y = my + mh/2 |
1127 | - mouse.move(win_x, target_y, rate=20, time_between_events=0.005) |
1128 | - mouse.move(target_x, target_y, rate=20, time_between_events=0.005) |
1129 | - mouse.release() |
1130 | + move_mouse_to_screen(0) |
1131 | |
1132 | === modified file 'autopilot/input/__init__.py' |
1133 | --- autopilot/emulators/input/__init__.py 2013-02-28 03:18:15 +0000 |
1134 | +++ autopilot/input/__init__.py 2013-04-15 21:43:32 +0000 |
1135 | @@ -6,99 +6,81 @@ |
1136 | # under the terms of the GNU General Public License version 3, as published |
1137 | # by the Free Software Foundation. |
1138 | |
1139 | -from collections import OrderedDict |
1140 | -from autopilot.utilities import get_debug_logger |
1141 | - |
1142 | -"""Autopilot unified input system. |
1143 | +""" |
1144 | +Autopilot unified input system. |
1145 | +=============================== |
1146 | |
1147 | This package provides input methods for various platforms. Autopilot aims to |
1148 | provide an appropriate implementation for the currently running system. For |
1149 | example, not all systems have an X11 stack running: on those systems, autopilot |
1150 | -will instantiate a Keyboard class that uses something other than X11 to generate |
1151 | -key events (possibly UInput). |
1152 | - |
1153 | -Test authors are encouraged to instantiate the input devices they need for their |
1154 | -tests using the get_keyboard and get_mouse methods directly. In the case where |
1155 | -these methods don't do the right thing, authors may access the underlying input |
1156 | -systems directly. However, these are not documented, and are liable to change |
1157 | -without notice. |
1158 | +will instantiate input classes class that use something other than X11 to generate |
1159 | +events (possibly UInput). |
1160 | + |
1161 | +Test authors should instantiate the appropriate class using the ``create`` method |
1162 | +on each class. Tests can provide a hint to this method to suggest that a particular |
1163 | +subsystem be used. However, autopilot will prefer to return a subsystem other than |
1164 | +the one specified, if the requested subsystem is unavailable. |
1165 | + |
1166 | +There are three basic input types available: |
1167 | + |
1168 | + * :class:`Keyboard` - traditional keyboard devices. |
1169 | + * :class:`Mouse` - traditional mouse devices. |
1170 | + * :class:`Touch` - single point-of-contact touch device. |
1171 | + * For multitouch capabilities, see the :mod:`autopilot.gestures` module. |
1172 | |
1173 | """ |
1174 | |
1175 | - |
1176 | -def get_keyboard(preferred_variant=""): |
1177 | - """Get an instance of the Keyboard class. |
1178 | - |
1179 | - If variant is specified, it should be a string that specifies a backend to |
1180 | - use. However, this hint can be ignored - autopilot will prefer to return a |
1181 | - keyboard variant other than the one requested, rather than fail to return |
1182 | - anything at all. |
1183 | - |
1184 | - If autopilot cannot instantate any of the possible backends, a RuntimeError |
1185 | - will be raised. |
1186 | - """ |
1187 | - def get_x11_kb(): |
1188 | - from autopilot.emulators.input._X11 import Keyboard |
1189 | - return Keyboard() |
1190 | - def get_uinput_kb(): |
1191 | - from autopilot.emulators.input._uinput import Keyboard |
1192 | - return Keyboard() |
1193 | - |
1194 | - variants = OrderedDict() |
1195 | - variants['X11'] = get_x11_kb |
1196 | - variants['UInput'] = get_uinput_kb |
1197 | - return _pick_variant(variants, preferred_variant) |
1198 | - |
1199 | - |
1200 | -def get_mouse(preferred_variant=""): |
1201 | - """Get an instance of the Mouse class. |
1202 | - |
1203 | - If variant is specified, it should be a string that specifies a backend to |
1204 | - use. However, this hint can be ignored - autopilot will prefer to return a |
1205 | - mouse variant other than the one requested, rather than fail to return |
1206 | - anything at all. |
1207 | - |
1208 | - If autopilot cannot instantate any of the possible backends, a RuntimeError |
1209 | - will be raised. |
1210 | - """ |
1211 | - def get_x11_mouse(): |
1212 | - from autopilot.emulators.input._X11 import Mouse |
1213 | - return Mouse() |
1214 | - |
1215 | - variants = OrderedDict() |
1216 | - variants['X11'] = get_x11_mouse |
1217 | - return _pick_variant(variants, preferred_variant) |
1218 | - |
1219 | - |
1220 | -def _pick_variant(variants, preferred_variant): |
1221 | - possible_backends = variants.keys() |
1222 | - get_debug_logger().debug("Possible keyboard variants: %s", ','.join(possible_backends)) |
1223 | - if preferred_variant in possible_backends: |
1224 | - possible_backends.sort(lambda a,b: -1 if a == preferred_variant else 0) |
1225 | - failure_reasons = [] |
1226 | - for be in possible_backends: |
1227 | - try: |
1228 | - return variants[be]() |
1229 | - except Exception as e: |
1230 | - get_debug_logger().warning("Can't create keyboard variant %s: %r", be, e) |
1231 | - failure_reasons.append('%s: %r' % (be, e)) |
1232 | - raise RuntimeError("Unable to instantiate any Keyboard backends\n%s" % '\n'.join(failure_reasons)) |
1233 | +from collections import OrderedDict |
1234 | +from autopilot.utilities import _pick_variant |
1235 | + |
1236 | |
1237 | |
1238 | class Keyboard(object): |
1239 | |
1240 | - """A base class for all keyboard-type devices.""" |
1241 | + """A simple keyboard device class. |
1242 | + |
1243 | + The keyboard class is used to generate key events while in an autopilot |
1244 | + test. This class should not be instantiated directly however. To get an |
1245 | + instance of the keyboard class, call :py:meth:`create` instead. |
1246 | + |
1247 | + """ |
1248 | + |
1249 | + @staticmethod |
1250 | + def create(preferred_variant=''): |
1251 | + """Get an instance of the :py:class:`Keyboard` class. |
1252 | + |
1253 | + :param preferred_variant: A string containing a hint as to which variant you |
1254 | + would like. However, this hint can be ignored - autopilot will prefer to |
1255 | + return a keyboard variant other than the one requested, rather than fail |
1256 | + to return anything at all. |
1257 | + :raises: a RuntimeError will be raised if autopilot cannot instantate any of |
1258 | + the possible backends. |
1259 | + |
1260 | + """ |
1261 | + def get_x11_kb(): |
1262 | + from autopilot.input._X11 import Keyboard |
1263 | + return Keyboard() |
1264 | + def get_uinput_kb(): |
1265 | + from autopilot.input._uinput import Keyboard |
1266 | + return Keyboard() |
1267 | + |
1268 | + variants = OrderedDict() |
1269 | + variants['X11'] = get_x11_kb |
1270 | + variants['UInput'] = get_uinput_kb |
1271 | + return _pick_variant(variants, preferred_variant) |
1272 | |
1273 | def press(self, keys, delay=0.2): |
1274 | """Send key press events only. |
1275 | |
1276 | - :param string keys: Keys you want pressed. |
1277 | + :param keys: Keys you want pressed. |
1278 | + :param delay: The delay (in Seconds) after pressing the keys before |
1279 | + returning control to the caller. |
1280 | |
1281 | Example: |
1282 | |
1283 | >>> press('Alt+F2') |
1284 | |
1285 | - presses the 'Alt' and 'F2' keys. |
1286 | + presses the 'Alt' and 'F2' keys, but does not release them. |
1287 | |
1288 | """ |
1289 | raise NotImplementedError("You cannot use this class directly.") |
1290 | @@ -106,7 +88,9 @@ |
1291 | def release(self, keys, delay=0.2): |
1292 | """Send key release events only. |
1293 | |
1294 | - :param string keys: Keys you want released. |
1295 | + :param keys: Keys you want released. |
1296 | + :param delay: The delay (in Seconds) after releasing the keys before |
1297 | + returning control to the caller. |
1298 | |
1299 | Example: |
1300 | |
1301 | @@ -122,7 +106,9 @@ |
1302 | |
1303 | This is the same as calling 'press(keys);release(keys)'. |
1304 | |
1305 | - :param string keys: Keys you want pressed and released. |
1306 | + :param keys: Keys you want pressed and released. |
1307 | + :param delay: The delay (in Seconds) after pressing and releasing each |
1308 | + key. |
1309 | |
1310 | Example: |
1311 | |
1312 | @@ -137,6 +123,11 @@ |
1313 | def type(self, string, delay=0.1): |
1314 | """Simulate a user typing a string of text. |
1315 | |
1316 | + :param string: The string to text to type. |
1317 | + :param delay: The delay (in Seconds) after pressing and releasing each |
1318 | + key. Note that the default value here is shorter than for the press, |
1319 | + release and press_and_release methods. |
1320 | + |
1321 | .. note:: Only 'normal' keys can be typed with this method. Control |
1322 | characters (such as 'Alt' will be interpreted as an 'A', and 'l', |
1323 | and a 't'). |
1324 | @@ -156,7 +147,40 @@ |
1325 | |
1326 | |
1327 | class Mouse(object): |
1328 | - """A base class for all mouse-type classes.""" |
1329 | + |
1330 | + """A simple mouse device class. |
1331 | + |
1332 | + The mouse class is used to generate mouse events while in an autopilot |
1333 | + test. This class should not be instantiated directly however. To get an |
1334 | + instance of the mouse class, call :py:meth:`create` instead. |
1335 | + |
1336 | + For example, to create a mouse object and click at (100,50): |
1337 | + |
1338 | + >>> mouse = autopilot.input.Mouse.create() |
1339 | + >>> mouse.move(100, 50) |
1340 | + >>> mouse.click() |
1341 | + |
1342 | + """ |
1343 | + |
1344 | + @staticmethod |
1345 | + def create(preferred_variant=''): |
1346 | + """Get an instance of the :py:class:`Mouse` class. |
1347 | + |
1348 | + :param preferred_variant: A string containing a hint as to which variant you |
1349 | + would like. However, this hint can be ignored - autopilot will prefer to |
1350 | + return a mouse variant other than the one requested, rather than fail |
1351 | + to return anything at all. |
1352 | + :raises: a RuntimeError will be raised if autopilot cannot instantate any of |
1353 | + the possible backends. |
1354 | + |
1355 | + """ |
1356 | + def get_x11_mouse(): |
1357 | + from autopilot.input._X11 import Mouse |
1358 | + return Mouse() |
1359 | + |
1360 | + variants = OrderedDict() |
1361 | + variants['X11'] = get_x11_mouse |
1362 | + return _pick_variant(variants, preferred_variant) |
1363 | |
1364 | @property |
1365 | def x(self): |
1366 | @@ -222,3 +246,73 @@ |
1367 | def cleanup(): |
1368 | """Put mouse in a known safe state.""" |
1369 | raise NotImplementedError("You cannot use this class directly.") |
1370 | + |
1371 | + |
1372 | +class Touch(object): |
1373 | + """A simple touch driver class. |
1374 | + |
1375 | + This class can be used for any touch events that require a single active |
1376 | + touch at once. If you want to do complex gestures (including multi-touch |
1377 | + gestures), look at the :py:mod:`autopilot.gestures` module. |
1378 | + |
1379 | + """ |
1380 | + |
1381 | + @staticmethod |
1382 | + def create(preferred_variant=''): |
1383 | + """Get an instance of the :py:class:`Touch` class. |
1384 | + |
1385 | + :param preferred_variant: A string containing a hint as to which variant you |
1386 | + would like. However, this hint can be ignored - autopilot will prefer to |
1387 | + return a touch variant other than the one requested, rather than fail |
1388 | + to return anything at all. |
1389 | + :raises: a RuntimeError will be raised if autopilot cannot instantate any of |
1390 | + the possible backends. |
1391 | + |
1392 | + """ |
1393 | + def get_uinput_touch(): |
1394 | + from autopilot.input._uinput import Touch |
1395 | + return Touch() |
1396 | + |
1397 | + variants = OrderedDict() |
1398 | + variants['UInput'] = get_uinput_touch |
1399 | + return _pick_variant(variants, preferred_variant) |
1400 | + |
1401 | + @property |
1402 | + def pressed(self): |
1403 | + """Return True if this touch is currently in use (i.e.- pressed on the |
1404 | + 'screen'). |
1405 | + |
1406 | + """ |
1407 | + raise NotImplementedError("You cannot use this class directly.") |
1408 | + |
1409 | + def tap(self, x, y): |
1410 | + """Click (or 'tap') at given x and y coordinates.""" |
1411 | + raise NotImplementedError("You cannot use this class directly.") |
1412 | + |
1413 | + def tap_object(self, object): |
1414 | + """Tap the center point of a given object. |
1415 | + |
1416 | + It does this by looking for several attributes, in order. The first |
1417 | + attribute found will be used. The attributes used are (in order): |
1418 | + |
1419 | + * globalRect (x,y,w,h) |
1420 | + * center_x, center_y |
1421 | + * x, y, w, h |
1422 | + |
1423 | + :raises: **ValueError** if none of these attributes are found, or if an |
1424 | + attribute is of an incorrect type. |
1425 | + |
1426 | + """ |
1427 | + raise NotImplementedError("You cannot use this class directly.") |
1428 | + |
1429 | + def press(self, x, y): |
1430 | + """Press and hold.""" |
1431 | + raise NotImplementedError("You cannot use this class directly.") |
1432 | + |
1433 | + def release(self): |
1434 | + """Release a previously pressed finger""" |
1435 | + raise NotImplementedError("You cannot use this class directly.") |
1436 | + |
1437 | + def drag(self, x1, y1, x2, y2): |
1438 | + """Perform a drag gesture from (x1,y1) to (x2,y2)""" |
1439 | + raise NotImplementedError("You cannot use this class directly.") |
1440 | |
1441 | === added file 'autopilot/input/_common.py' |
1442 | --- autopilot/input/_common.py 1970-01-01 00:00:00 +0000 |
1443 | +++ autopilot/input/_common.py 2013-04-15 21:43:32 +0000 |
1444 | @@ -0,0 +1,46 @@ |
1445 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
1446 | +# Copyright 2013 Canonical |
1447 | +# Author: Thomi Richards |
1448 | +# |
1449 | +# This program is free software: you can redistribute it and/or modify it |
1450 | +# under the terms of the GNU General Public License version 3, as published |
1451 | +# by the Free Software Foundation. |
1452 | + |
1453 | +"""Common, private utility code for input emulators.""" |
1454 | + |
1455 | +import logging |
1456 | + |
1457 | +logger = logging.getLogger(__name__) |
1458 | + |
1459 | + |
1460 | +def get_center_point(object_proxy): |
1461 | + """Get the center point of an object, searching for several different ways |
1462 | + of determining exactly where the center is. |
1463 | + |
1464 | + """ |
1465 | + try: |
1466 | + x,y,w,h = object_proxy.globalRect |
1467 | + logger.debug("Moving to object's globalRect coordinates.") |
1468 | + return x+w/2, y+h/2 |
1469 | + except AttributeError: |
1470 | + pass |
1471 | + except (TypeError, ValueError): |
1472 | + raise ValueError("Object '%r' has globalRect attribute, but it is not of the correct type" % object_proxy) |
1473 | + |
1474 | + try: |
1475 | + x,y = object_proxy.center_x, object_proxy.center_y |
1476 | + logger.debug("Moving to object's center_x, center_y coordinates.") |
1477 | + return x,y |
1478 | + except AttributeError: |
1479 | + pass |
1480 | + except (TypeError, ValueError): |
1481 | + raise ValueError("Object '%r' has center_x, center_y attributes, but they are not of the correct type" % object_proxy) |
1482 | + |
1483 | + try: |
1484 | + x,y,w,h = object_proxy.x, object_proxy.y, object_proxy.w, object_proxy.h |
1485 | + logger.debug("Moving to object's center point calculated from x,y,w,h attributes.") |
1486 | + return x+w/2,y+h/2 |
1487 | + except AttributeError: |
1488 | + raise ValueError("Object '%r' does not have any recognised position attributes" % object_proxy) |
1489 | + except (TypeError, ValueError): |
1490 | + raise ValueError("Object '%r' has x,y attribute, but they are not of the correct type" % object_proxy) |
1491 | |
1492 | === modified file 'autopilot/input/_uinput.py' |
1493 | --- autopilot/emulators/input/_uinput.py 2013-02-28 01:00:50 +0000 |
1494 | +++ autopilot/input/_uinput.py 2013-04-15 21:43:32 +0000 |
1495 | @@ -9,11 +9,14 @@ |
1496 | |
1497 | """UInput device drivers.""" |
1498 | |
1499 | -from autopilot.emulators.input import Keyboard as KeyboardBase |
1500 | +from autopilot.input import Keyboard as KeyboardBase |
1501 | +from autopilot.input import Touch as TouchBase |
1502 | +from autopilot.input._common import get_center_point |
1503 | +import autopilot.platform |
1504 | + |
1505 | import logging |
1506 | from time import sleep |
1507 | -from evdev import AbsData, InputDevice, UInput, ecodes as e |
1508 | -import os.path |
1509 | +from evdev import AbsData, UInput, ecodes as e |
1510 | |
1511 | logger = logging.getLogger(__name__) |
1512 | |
1513 | @@ -22,6 +25,7 @@ |
1514 | |
1515 | PRESSED_KEYS = [] |
1516 | |
1517 | + |
1518 | class Keyboard(KeyboardBase): |
1519 | |
1520 | def __init__(self): |
1521 | @@ -46,7 +50,7 @@ |
1522 | raise TypeError("'keys' argument must be a string.") |
1523 | |
1524 | for key in keys.split('+'): |
1525 | - for event in _get_events_for_key(key): |
1526 | + for event in Keyboard._get_events_for_key(key): |
1527 | self._emit(event, PRESS) |
1528 | sleep(delay) |
1529 | |
1530 | @@ -69,7 +73,7 @@ |
1531 | # # release keys in the reverse order they were pressed in. |
1532 | # keys = self.__translate_keys(keys) |
1533 | for key in reversed(keys.split('+')): |
1534 | - for event in _get_events_for_key(key): |
1535 | + for event in Keyboard._get_events_for_key(key): |
1536 | self._emit(event, RELEASE) |
1537 | sleep(delay) |
1538 | |
1539 | @@ -118,78 +122,58 @@ |
1540 | # fake_input(get_display(), X.KeyRelease, keycode) |
1541 | # _PRESSED_KEYS = [] |
1542 | |
1543 | + @staticmethod |
1544 | + def _get_events_for_key(key): |
1545 | + """Return a list of events required to generate 'key' as an input. |
1546 | + |
1547 | + Multiple keys will be returned when the key specified requires more than one |
1548 | + keypress to generate (for example, upper-case letters). |
1549 | + |
1550 | + """ |
1551 | + events = [] |
1552 | + if key.isupper(): |
1553 | + events.append(e.KEY_LEFTSHIFT) |
1554 | + keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key) |
1555 | + evt = getattr(e, 'KEY_' + keyname.upper(), None) |
1556 | + if evt is None: |
1557 | + raise ValueError("Unknown key name: '%s'" % key) |
1558 | + events.append(evt) |
1559 | + return events |
1560 | + |
1561 | + |
1562 | last_tracking_id = 0 |
1563 | def get_next_tracking_id(): |
1564 | global last_tracking_id |
1565 | last_tracking_id += 1 |
1566 | return last_tracking_id |
1567 | |
1568 | + |
1569 | def create_touch_device(res_x=None, res_y=None): |
1570 | """Create and return a UInput touch device. |
1571 | |
1572 | - If res_x and res_y are not specified, they will be queried from X11. |
1573 | - |
1574 | - The following needs to go into /system/usr/idc/autopilot-finger.idc |
1575 | - |
1576 | - # Copyright (C) 2011 The Android Open Source Project |
1577 | - # |
1578 | - # Licensed under the Apache License, Version 2.0 (the "License"); |
1579 | - # you may not use this file except in compliance with the License. |
1580 | - # You may obtain a copy of the License at |
1581 | - # |
1582 | - # http://www.apache.org/licenses/LICENSE-2.0 |
1583 | - # |
1584 | - # Unless required by applicable law or agreed to in writing, software |
1585 | - # distributed under the License is distributed on an "AS IS" BASIS, |
1586 | - # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1587 | - # See the License for the specific language governing permissions and |
1588 | - # limitations under the License. |
1589 | - |
1590 | - # |
1591 | - # Input Device Calibration File for the Tuna touch screen. |
1592 | - # |
1593 | - |
1594 | - device.internal = 1 |
1595 | - |
1596 | - # Basic Parameters |
1597 | - touch.deviceType = touchScreen |
1598 | - touch.orientationAware = 1 |
1599 | - |
1600 | - # Size |
1601 | - touch.size.calibration = diameter |
1602 | - touch.size.scale = 10 |
1603 | - touch.size.bias = 0 |
1604 | - touch.size.isSummed = 0 |
1605 | - |
1606 | - # Pressure |
1607 | - # Driver reports signal strength as pressure. |
1608 | - # |
1609 | - # A normal thumb touch typically registers about 200 signal strength |
1610 | - # units although we don't expect these values to be accurate. |
1611 | - touch.pressure.calibration = amplitude |
1612 | - touch.pressure.scale = 0.005 |
1613 | - |
1614 | - # Orientation |
1615 | - touch.orientation.calibration = none |
1616 | + If res_x and res_y are not specified, they will be queried from the system. |
1617 | + |
1618 | """ |
1619 | |
1620 | - # FIXME: remove the harcoded values and determine ScreenGeometry without X11 |
1621 | - res_x = 720 |
1622 | - res_y = 1280 |
1623 | - |
1624 | if res_x is None or res_y is None: |
1625 | - from autopilot.emulators.X11 import ScreenGeometry |
1626 | - sg = ScreenGeometry() |
1627 | - res_x = sg.get_screen_width() |
1628 | - res_y = sg.get_screen_height() |
1629 | + from autopilot.display import Display |
1630 | + display = Display.create() |
1631 | + res_x = display.get_screen_width() |
1632 | + res_y = display.get_screen_height() |
1633 | + |
1634 | + # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no |
1635 | + # idea why... |
1636 | + touch_tool = e.BTN_TOOL_FINGER |
1637 | + if autopilot.platform.model() == 'Desktop': |
1638 | + touch_tool = e.BTN_TOUCH |
1639 | |
1640 | cap_mt = { |
1641 | e.EV_ABS : [ |
1642 | (e.ABS_X, AbsData(0, res_x, 0, 0)), |
1643 | (e.ABS_Y, AbsData(0, res_y, 0, 0)), |
1644 | (e.ABS_PRESSURE, AbsData(0, 65535, 0, 0)), |
1645 | - (e.ABS_DISTANCE, AbsData(0, 65535, 0, 0)), |
1646 | - (e.ABS_TOOL_WIDTH, AbsData(0, 65535, 0, 0)), |
1647 | + # (e.ABS_DISTANCE, AbsData(0, 65535, 0, 0)), |
1648 | + # (e.ABS_TOOL_WIDTH, AbsData(0, 65535, 0, 0)), |
1649 | (e.ABS_MT_POSITION_X, AbsData(0, res_x, 0, 0)), |
1650 | (e.ABS_MT_POSITION_Y, AbsData(0, res_y, 0, 0)), |
1651 | (e.ABS_MT_TOUCH_MAJOR, AbsData(0, 30, 0, 0)), |
1652 | @@ -198,147 +182,122 @@ |
1653 | (e.ABS_MT_SLOT, (0, 9, 0, 0)), |
1654 | ], |
1655 | e.EV_KEY: [ |
1656 | - e.BTN_TOOL_FINGER, |
1657 | + touch_tool, |
1658 | ] |
1659 | } |
1660 | |
1661 | - try: |
1662 | - device = UInput(cap_mt, name='autopilot-finger', version=0x2) |
1663 | - return device |
1664 | - except: |
1665 | - logger.warning("Failed to open uinput device. Finger will not be available") |
1666 | - return None |
1667 | - |
1668 | - |
1669 | -def find_touch_device(): |
1670 | - """Return a list of touch devices found on the local machine. |
1671 | - |
1672 | - :returns: A list of evdev.inputDevice objects, possibly an empty list. |
1673 | - |
1674 | - """ |
1675 | - def device_cmp(dev_a, dev_b): |
1676 | - def get_track_slot(dev): |
1677 | - capabilities = dev.capabilities() |
1678 | - for track_feature in capabilities[e.EV_ABS]: |
1679 | - if track_feature[0] == e.ABS_MT_SLOT: |
1680 | - return track_feature[1][1] |
1681 | - return get_track_slot(dev_a) < get_track_slot(dev_b) |
1682 | - |
1683 | - i = 0 |
1684 | - touch_devices = [] |
1685 | - while True: |
1686 | - path = '/dev/input/event%d' % (i) |
1687 | - if not os.path.exists(path): |
1688 | - break |
1689 | - |
1690 | - dev = InputDevice(path) |
1691 | - capabilities = dev.capabilities() |
1692 | - if e.EV_ABS in capabilities: |
1693 | - touch_devices.append(dev) |
1694 | - i += 1 |
1695 | - touch_devices.sort(device_cmp) |
1696 | - return touch_devices |
1697 | - |
1698 | - |
1699 | -""" |
1700 | -Multiouch notes: |
1701 | ----------------- |
1702 | - |
1703 | -We're simulating a class of device that can track multiple touches, and keep |
1704 | -them separate. This is how most modern track devices work anyway. The device |
1705 | -is created with a capability to track a certain number of distinct touches at |
1706 | -once. This is the ABS_MT_SLOT capability. Since our target device can track 9 |
1707 | -separate touches, we'll do the same. |
1708 | - |
1709 | -Each finger contact starts by registering a slot number (0-8) with a tracking |
1710 | -Id. The Id should be unique for this touch - this can be an auto-inctrementing |
1711 | -integer. The very first packets to tell the kernel that we have a touch happening |
1712 | -should look like this: |
1713 | - |
1714 | - ABS_MT_SLOT 0 |
1715 | - ABS_MT_TRACKING_ID 45 |
1716 | - ABS_MT_POSITION_X x[0] |
1717 | - ABS_MT_POSITION_Y y[0] |
1718 | - |
1719 | -This associates Tracking id 45 (could be any number) with slot 0. Slot 0 can now |
1720 | -not be use by any other touch until it is released. |
1721 | - |
1722 | -If we want to move this contact's coordinates, we do this: |
1723 | - |
1724 | - ABS_MT_SLOT 0 |
1725 | - ABS_MT_POSITION_X 123 |
1726 | - ABS_MT_POSITION_Y 234 |
1727 | - |
1728 | -Technically, the 'SLOT 0' part isn't needed, since we're already in slot 0, but |
1729 | -it doesn't hurt to have it there. |
1730 | - |
1731 | -To lift the contact, we simply specify a tracking Id of -1: |
1732 | - |
1733 | - ABS_MT_SLOT 0 |
1734 | - ABS_MT_TRACKING_ID -1 |
1735 | - |
1736 | -The initial association between slot and tracking Id is made when the 'finger' |
1737 | -first makes contact with the device (well, not technically true, but close |
1738 | -enough). Multiple touches can be active simultaniously, as long as they all have |
1739 | -unique slots, and tracking Ids. The simplest way to think about this is that the |
1740 | -SLOT refers to a finger number, and the TRACKING_ID identifies a unique touch |
1741 | -for the duration of it's existance. |
1742 | - |
1743 | -""" |
1744 | - |
1745 | -class Finger(object): |
1746 | + return UInput(cap_mt, name='autopilot-finger', version=0x2) |
1747 | + |
1748 | +_touch_device = create_touch_device() |
1749 | + |
1750 | +# Multiouch notes: |
1751 | +# ---------------- |
1752 | + |
1753 | +# We're simulating a class of device that can track multiple touches, and keep |
1754 | +# them separate. This is how most modern track devices work anyway. The device |
1755 | +# is created with a capability to track a certain number of distinct touches at |
1756 | +# once. This is the ABS_MT_SLOT capability. Since our target device can track 9 |
1757 | +# separate touches, we'll do the same. |
1758 | + |
1759 | +# Each finger contact starts by registering a slot number (0-8) with a tracking |
1760 | +# Id. The Id should be unique for this touch - this can be an auto-inctrementing |
1761 | +# integer. The very first packets to tell the kernel that we have a touch happening |
1762 | +# should look like this: |
1763 | + |
1764 | +# ABS_MT_SLOT 0 |
1765 | +# ABS_MT_TRACKING_ID 45 |
1766 | +# ABS_MT_POSITION_X x[0] |
1767 | +# ABS_MT_POSITION_Y y[0] |
1768 | + |
1769 | +# This associates Tracking id 45 (could be any number) with slot 0. Slot 0 can now |
1770 | +# not be use by any other touch until it is released. |
1771 | + |
1772 | +# If we want to move this contact's coordinates, we do this: |
1773 | + |
1774 | +# ABS_MT_SLOT 0 |
1775 | +# ABS_MT_POSITION_X 123 |
1776 | +# ABS_MT_POSITION_Y 234 |
1777 | + |
1778 | +# Technically, the 'SLOT 0' part isn't needed, since we're already in slot 0, but |
1779 | +# it doesn't hurt to have it there. |
1780 | + |
1781 | +# To lift the contact, we simply specify a tracking Id of -1: |
1782 | + |
1783 | +# ABS_MT_SLOT 0 |
1784 | +# ABS_MT_TRACKING_ID -1 |
1785 | + |
1786 | +# The initial association between slot and tracking Id is made when the 'finger' |
1787 | +# first makes contact with the device (well, not technically true, but close |
1788 | +# enough). Multiple touches can be active simultaniously, as long as they all have |
1789 | +# unique slots, and tracking Ids. The simplest way to think about this is that the |
1790 | +# SLOT refers to a finger number, and the TRACKING_ID identifies a unique touch |
1791 | +# for the duration of it's existance. |
1792 | + |
1793 | +_touch_fingers_in_use = [] |
1794 | +def _get_touch_finger(): |
1795 | + """Claim a touch finger id for use. |
1796 | + |
1797 | + :raises: RuntimeError if no more fingers are available. |
1798 | + |
1799 | + """ |
1800 | + global _touch_fingers_in_use |
1801 | + |
1802 | + for i in range(9): |
1803 | + if i not in _touch_fingers_in_use: |
1804 | + _touch_fingers_in_use.append(i) |
1805 | + return i |
1806 | + raise RuntimeError("All available fingers have been used already.") |
1807 | + |
1808 | +def _release_touch_finger(finger_num): |
1809 | + """Relase a previously-claimed finger id. |
1810 | + |
1811 | + :raises: RuntimeError if the finger given was never claimed, or was already |
1812 | + released. |
1813 | + |
1814 | + """ |
1815 | + global _touch_fingers_in_use |
1816 | + |
1817 | + if finger_num not in _touch_fingers_in_use: |
1818 | + raise RuntimeError("Finger %d was never claimed, or has already been released." % (finger_num)) |
1819 | + _touch_fingers_in_use.remove(finger_num) |
1820 | + assert(finger_num not in _touch_fingers_in_use) |
1821 | + |
1822 | + |
1823 | +class Touch(TouchBase): |
1824 | """Low level interface to generate single finger touch events.""" |
1825 | |
1826 | def __init__(self): |
1827 | - self.device = create_touch_device() |
1828 | - if self.device == None: |
1829 | - raise UInputError |
1830 | - self.current_x = 0 |
1831 | - self.current_y = 0 |
1832 | - |
1833 | - def move(self, x, y): |
1834 | - """This is a convenience function to keep API compatibility with other pointer input methods""" |
1835 | - self.current_x = x |
1836 | - self.current_y = y |
1837 | - |
1838 | - def move_to_object(self, object): |
1839 | - """This is a convenience function to keep API compatibility with other pointer input methods""" |
1840 | - self.current_x = object.globalRect[0] + object.globalRect[2] / 2 |
1841 | - self.current_y = object.globalRect[1] + object.globalRect[3] / 2 |
1842 | - |
1843 | - def click(self): |
1844 | - self.tap(self.current_x, self.current_y) |
1845 | + super(TouchBase, self).__init__() |
1846 | + self._touch_finger = None |
1847 | + |
1848 | + @property |
1849 | + def pressed(self): |
1850 | + return self._touch_finger is not None |
1851 | |
1852 | def tap(self, x, y): |
1853 | """Click (or 'tap') at given x and y coordinates.""" |
1854 | - self._finger_down(0, x, y) |
1855 | + self._finger_down(x, y) |
1856 | sleep(0.1) |
1857 | - self._finger_up(0) |
1858 | + self._finger_up() |
1859 | |
1860 | def tap_object(self, object): |
1861 | """Click (or 'tap') a given object""" |
1862 | - self.tap(object.globalRect[0] + object.globalRect[2] / 2, object.globalRect[1] + object.globalRect[3] / 2) |
1863 | + x,y = get_center_point(object) |
1864 | + self.tap(x,y) |
1865 | |
1866 | - def press(self, *args): |
1867 | + def press(self, x, y): |
1868 | """Press and hold a given object or at the given coordinates |
1869 | Call release() when the object has been pressed long enough""" |
1870 | - if len(args) == 2: |
1871 | - self.current_x = args[0] |
1872 | - self.current_y = args[1] |
1873 | - elif len(args) == 0: |
1874 | - pass |
1875 | - else: |
1876 | - raise InvalidArgCount |
1877 | - self._finger_down(0, self.current_x, self.current_y) |
1878 | + self._finger_down(x, y) |
1879 | |
1880 | def release(self): |
1881 | """Release a previously pressed finger""" |
1882 | - self._finger_up(0) |
1883 | + self._finger_up() |
1884 | |
1885 | |
1886 | def drag(self, x1, y1, x2, y2): |
1887 | """Perform a drag gesture from (x1,y1) to (x2,y2)""" |
1888 | - self._finger_down(0, x1, y1) |
1889 | + self._finger_down(x1, y1) |
1890 | |
1891 | # Let's drag in 100 steps for now... |
1892 | dx = 1.0 * (x2 - x1) / 100 |
1893 | @@ -346,92 +305,49 @@ |
1894 | cur_x = x1 + dx |
1895 | cur_y = y1 + dy |
1896 | for i in range(0, 100): |
1897 | - self._finger_move(0, int(cur_x), int(cur_y)) |
1898 | + self._finger_move(int(cur_x), int(cur_y)) |
1899 | sleep(0.002) |
1900 | cur_x += dx |
1901 | cur_y += dy |
1902 | # Make sure we actually end up at target |
1903 | - self._finger_move(0, x2, y2) |
1904 | - self._finger_up(0) |
1905 | - |
1906 | - def pinch(self, center, distance_start, distance_end): |
1907 | - """Perform a two finger pinch (zoom) gesture |
1908 | - "center" gives the coordinates [x,y] of the center between the two fingers |
1909 | - "distance_start" [x,y] values to move away from the center for the start |
1910 | - "distance_end" [x,y] values to move away from the center for the end |
1911 | - The fingers will move in 100 steps between the start and the end points. |
1912 | - If start is smaller than end, the gesture will zoom in, otherwise it |
1913 | - will zoom out.""" |
1914 | - |
1915 | - finger_1_start = [center[0] - distance_start[0], center[1] - distance_start[1]] |
1916 | - finger_2_start = [center[0] + distance_start[0], center[1] + distance_start[1]] |
1917 | - finger_1_end = [center[0] - distance_end[0], center[1] - distance_end[1]] |
1918 | - finger_2_end = [center[0] + distance_end[0], center[1] + distance_end[1]] |
1919 | - |
1920 | - dx = 1.0 * (finger_1_end[0] - finger_1_start[0]) / 100 |
1921 | - dy = 1.0 * (finger_1_end[1] - finger_1_start[1]) / 100 |
1922 | - |
1923 | - self._finger_down(0, finger_1_start[0], finger_1_start[1]) |
1924 | - self._finger_down(1, finger_2_start[0], finger_2_start[1]) |
1925 | - |
1926 | - finger_1_cur = [finger_1_start[0] + dx, finger_1_start[1] + dy] |
1927 | - finger_2_cur = [finger_2_start[0] - dx, finger_2_start[1] - dy] |
1928 | - |
1929 | - for i in range(0, 100): |
1930 | - self._finger_move(0, finger_1_cur[0], finger_1_cur[1]) |
1931 | - self._finger_move(1, finger_2_cur[0], finger_2_cur[1]) |
1932 | - sleep(0.005) |
1933 | - |
1934 | - finger_1_cur = [finger_1_cur[0] + dx, finger_1_cur[1] + dy] |
1935 | - finger_2_cur = [finger_2_cur[0] - dx, finger_2_cur[1] - dy] |
1936 | - |
1937 | - self._finger_move(0, finger_1_end[0], finger_1_end[1]) |
1938 | - self._finger_move(1, finger_2_end[0], finger_2_end[1]) |
1939 | - self._finger_up(0) |
1940 | - self._finger_up(1) |
1941 | - |
1942 | - def _finger_down(self, finger, x, y): |
1943 | + self._finger_move(x2, y2) |
1944 | + self._finger_up() |
1945 | + |
1946 | + |
1947 | + |
1948 | + def _finger_down(self, x, y): |
1949 | """Internal: moves finger "finger" down to the touchscreen at pos (x,y)""" |
1950 | - self.device.write(e.EV_ABS, e.ABS_MT_SLOT, finger) |
1951 | - self.device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id()) |
1952 | - self.device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1) |
1953 | - self.device.write(e.EV_ABS, e.ABS_MT_POSITION_X, x) |
1954 | - self.device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, y) |
1955 | - self.device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400) |
1956 | - self.device.syn() |
1957 | - |
1958 | - |
1959 | - def _finger_move(self, finger, x, y): |
1960 | + if self._touch_finger is not None: |
1961 | + raise RuntimeError("Cannot press finger: it's already pressed.") |
1962 | + self._touch_finger = _get_touch_finger() |
1963 | + |
1964 | + _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger) |
1965 | + _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id()) |
1966 | + _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1) |
1967 | + _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x)) |
1968 | + _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y)) |
1969 | + _touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400) |
1970 | + _touch_device.syn() |
1971 | + |
1972 | + |
1973 | + def _finger_move(self, x, y): |
1974 | """Internal: moves finger "finger" on the touchscreen to pos (x,y) |
1975 | NOTE: The finger has to be down for this to have any effect.""" |
1976 | - self.device.write(e.EV_ABS, e.ABS_MT_SLOT, finger) |
1977 | - self.device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x)) |
1978 | - self.device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y)) |
1979 | - self.device.syn() |
1980 | - |
1981 | - |
1982 | - def _finger_up(self, finger): |
1983 | + if self._touch_finger is not None: |
1984 | + _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger) |
1985 | + _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x)) |
1986 | + _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y)) |
1987 | + _touch_device.syn() |
1988 | + |
1989 | + |
1990 | + def _finger_up(self): |
1991 | """Internal: moves finger "finger" up from the touchscreen""" |
1992 | - self.device.write(e.EV_ABS, e.ABS_MT_SLOT, finger) |
1993 | - self.device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1) |
1994 | - self.device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0) |
1995 | - self.device.syn() |
1996 | - |
1997 | - |
1998 | -class MultiTouch(object): |
1999 | - """High level interface to generate multi-touch events.""" |
2000 | - |
2001 | - # Need to work out a good interface for generating multitouch events... |
2002 | - # Possibly get users to specify each individual finger tracking path, or |
2003 | - # possibly specify a gesture by name, with parameters or so? |
2004 | - # |
2005 | - # Probably need to specify X & Y resolution as well. Think we can ignore |
2006 | - # pressure right now. |
2007 | - # |
2008 | - # Multi-touch protocol documentation is here: |
2009 | - # |
2010 | - # http://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt |
2011 | - |
2012 | + if self._touch_finger is None: |
2013 | + raise RuntimeError("Cannot release finger: it's not pressed.") |
2014 | + _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger) |
2015 | + _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1) |
2016 | + _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0) |
2017 | + _touch_device.syn() |
2018 | |
2019 | |
2020 | _UINPUT_CODE_TRANSLATIONS = { |
2021 | @@ -441,23 +357,3 @@ |
2022 | 'ALT': 'LEFTALT', |
2023 | 'SHIFT': 'LEFTSHIFT', |
2024 | } |
2025 | - |
2026 | - |
2027 | -def _get_events_for_key(key): |
2028 | - """Return a list of events required to generate 'key' as an input. |
2029 | - |
2030 | - Multiple keys will be returned when the key specified requires more than one |
2031 | - keypress to generate (for example, upper-case letters). |
2032 | - |
2033 | - """ |
2034 | - events = [] |
2035 | - if key.isupper(): |
2036 | - events.append(e.KEY_LEFTSHIFT) |
2037 | - keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key) |
2038 | - evt = getattr(e, 'KEY_' + keyname.upper(), None) |
2039 | - if evt is None: |
2040 | - raise ValueError("Unknown key name: '%s'" % key) |
2041 | - events.append(evt) |
2042 | - return events |
2043 | - |
2044 | - |
2045 | |
2046 | === modified file 'autopilot/introspection/__init__.py' |
2047 | --- autopilot/introspection/__init__.py 2013-02-22 03:31:40 +0000 |
2048 | +++ autopilot/introspection/__init__.py 2013-04-15 21:43:32 +0000 |
2049 | @@ -7,16 +7,18 @@ |
2050 | # by the Free Software Foundation. |
2051 | # |
2052 | |
2053 | -"""Package for introspection support.""" |
2054 | +"""Package for introspection support. |
2055 | + |
2056 | +This package contains the internal implementation of the autopilot introspection |
2057 | +mechanism, and probably isn't useful to most test authors. |
2058 | + |
2059 | +""" |
2060 | |
2061 | import dbus |
2062 | -from gi.repository import Gio |
2063 | import logging |
2064 | import subprocess |
2065 | -from testtools.content import text_content |
2066 | from time import sleep |
2067 | import os |
2068 | -import signal |
2069 | |
2070 | |
2071 | from autopilot.introspection.constants import ( |
2072 | @@ -31,62 +33,65 @@ |
2073 | object_passes_filters, |
2074 | get_session_bus, |
2075 | ) |
2076 | -from autopilot.utilities import get_debug_logger |
2077 | +from autopilot.utilities import get_debug_logger, addCleanup |
2078 | |
2079 | |
2080 | logger = logging.getLogger(__name__) |
2081 | |
2082 | |
2083 | -class ApplicationIntrospectionTestMixin(object): |
2084 | - """A mix-in class to make launching applications for introsection easier. |
2085 | - |
2086 | - .. important:: You should not instantiate this class directly. Instead, use |
2087 | - one of the derived classes. |
2088 | - |
2089 | - """ |
2090 | - |
2091 | - def launch_test_application(self, application, *arguments, **kwargs): |
2092 | - """Launch *application* and retrieve a proxy object for the application. |
2093 | - |
2094 | - Use this method to launch a supported application and start testing it. |
2095 | - The application can be specified as: |
2096 | - |
2097 | - * A Desktop file, either with or without a path component. |
2098 | - * An executable file, either with a path, or one that is in the $PATH. |
2099 | - |
2100 | - This method supports the following keyword arguments: |
2101 | - |
2102 | - * *launch_dir*. If set to a directory that exists the process will be |
2103 | - launched from that directory. |
2104 | - |
2105 | - * *capture_output*. If set to True (the default), the process output |
2106 | - will be captured and attached to the test as test detail. |
2107 | - |
2108 | - :raises: **ValueError** if unknown keyword arguments are passed. |
2109 | - :return: A proxy object that represents the application. Introspection |
2110 | - data is retrievable via this object. |
2111 | - |
2112 | - """ |
2113 | - if not isinstance(application, basestring): |
2114 | - raise TypeError("'application' parameter must be a string.") |
2115 | - cwd = kwargs.pop('launch_dir', None) |
2116 | - capture_output = kwargs.pop('capture_output', True) |
2117 | - if kwargs: |
2118 | - raise ValueError("Unknown keyword arguments: %s." % |
2119 | - (', '.join( repr(k) for k in kwargs.keys()))) |
2120 | - |
2121 | - if application.endswith('.desktop'): |
2122 | - proc = Gio.DesktopAppInfo.new(application) |
2123 | - application = proc.get_executable() |
2124 | - |
2125 | - path, args = self.prepare_environment(application, list(arguments)) |
2126 | - |
2127 | - process = launch_autopilot_enabled_process(path, |
2128 | - args, |
2129 | - capture_output, |
2130 | - cwd=cwd) |
2131 | - self.addCleanup(self._kill_process_and_attach_logs, process) |
2132 | - return get_autopilot_proxy_object_for_process(process) |
2133 | +def get_application_launcher(app_path): |
2134 | + """Return an instance of :class:`ApplicationLauncher` that knows how to launch |
2135 | + the application at 'app_path'. |
2136 | + """ |
2137 | + # TODO: this is a teeny bit hacky - we call ldd to check whether this application |
2138 | + # links to certain library. We're assuming that linking to libQt* or libGtk* |
2139 | + # means the application is introspectable. This excludes any non-dynamically |
2140 | + # linked executables, which we may need to fix further down the line. |
2141 | + try: |
2142 | + ldd_output = subprocess.check_output(["ldd", app_path]).strip().lower() |
2143 | + except subprocess.CalledProcessError: |
2144 | + print "Error: Cannot auto-detect introspection plugin to load." |
2145 | + print "Use the '-i' argument to specify an interface." |
2146 | + exit(1) # TODO - don't exit, raise an exception, and handle it appropriately in parent code. |
2147 | + if 'libqtcore' in ldd_output: |
2148 | + from autopilot.introspection.qt import QtApplicationLauncher |
2149 | + return QtApplicationLauncher() |
2150 | + elif 'libgtk' in ldd_output: |
2151 | + from autopilot.introspection.gtk import GtkApplicationLauncher |
2152 | + return GtkApplicationLauncher() |
2153 | + return None |
2154 | + |
2155 | + |
2156 | +def launch_application(launcher, application, *arguments, **kwargs): |
2157 | + """Launch an application, and return a process object. |
2158 | + |
2159 | + :param launcher: An instance of the :class:`ApplicationLauncher` class to |
2160 | + prepare the environment before launching the application itself. |
2161 | + """ |
2162 | + |
2163 | + if not isinstance(application, basestring): |
2164 | + raise TypeError("'application' parameter must be a string.") |
2165 | + cwd = kwargs.pop('launch_dir', None) |
2166 | + capture_output = kwargs.pop('capture_output', True) |
2167 | + if kwargs: |
2168 | + raise ValueError("Unknown keyword arguments: %s." % |
2169 | + (', '.join( repr(k) for k in kwargs.keys()))) |
2170 | + |
2171 | + path, args = launcher.prepare_environment(application, list(arguments)) |
2172 | + |
2173 | + process = launch_process(path, |
2174 | + args, |
2175 | + capture_output, |
2176 | + cwd=cwd |
2177 | + ) |
2178 | + return process |
2179 | + |
2180 | + |
2181 | +class ApplicationLauncher(object): |
2182 | + """A class that knows how to launch an application with a certain type of |
2183 | + introspection enabled. |
2184 | + |
2185 | + """ |
2186 | |
2187 | def prepare_environment(self, app_path, arguments): |
2188 | """Prepare the application, or environment to launch with autopilot-support. |
2189 | @@ -99,22 +104,9 @@ |
2190 | """ |
2191 | raise NotImplementedError("Sub-classes must implement this method.") |
2192 | |
2193 | - def _kill_process_and_attach_logs(self, process): |
2194 | - process.kill() |
2195 | - logger.info("waiting for process to exit.") |
2196 | - for i in range(10): |
2197 | - if process.returncode is not None: |
2198 | - break |
2199 | - if i == 9: |
2200 | - logger.info("Terminating process group, since it hasn't exited after 10 seconds.") |
2201 | - os.killpg(process.pid, signal.SIGTERM) |
2202 | - sleep(1) |
2203 | - stdout, stderr = process.communicate() |
2204 | - self.addDetail('process-stdout', text_content(stdout)) |
2205 | - self.addDetail('process-stderr', text_content(stderr)) |
2206 | - |
2207 | - |
2208 | -def launch_autopilot_enabled_process(application, args, capture_output, **kwargs): |
2209 | + |
2210 | + |
2211 | +def launch_process(application, args, capture_output, **kwargs): |
2212 | """Launch an autopilot-enabled process and return the proxy object.""" |
2213 | commandline = [application] |
2214 | commandline.extend(args) |
2215 | @@ -132,28 +124,6 @@ |
2216 | return process |
2217 | |
2218 | |
2219 | -def get_child_pids(pid): |
2220 | - """Get a list of all child process Ids, for the given parent. |
2221 | - |
2222 | - """ |
2223 | - def get_children(pid): |
2224 | - command = ['ps', '-o', 'pid', '--ppid', str(pid), '--noheaders'] |
2225 | - try: |
2226 | - raw_output = subprocess.check_output(command) |
2227 | - except subprocess.CalledProcessError: |
2228 | - return [] |
2229 | - return [int(p) for p in raw_output.split()] |
2230 | - |
2231 | - result = [pid] |
2232 | - data = get_children(pid) |
2233 | - while data: |
2234 | - pid = data.pop(0) |
2235 | - result.append(pid) |
2236 | - data.extend(get_children(pid)) |
2237 | - |
2238 | - return result |
2239 | - |
2240 | - |
2241 | def get_autopilot_proxy_object_for_process(process): |
2242 | """Return the autopilot proxy object for the given *process*. |
2243 | |
2244 | @@ -199,6 +169,28 @@ |
2245 | raise RuntimeError("Unable to find Autopilot interface.") |
2246 | |
2247 | |
2248 | +def get_child_pids(pid): |
2249 | + """Get a list of all child process Ids, for the given parent. |
2250 | + |
2251 | + """ |
2252 | + def get_children(pid): |
2253 | + command = ['ps', '-o', 'pid', '--ppid', str(pid), '--noheaders'] |
2254 | + try: |
2255 | + raw_output = subprocess.check_output(command) |
2256 | + except subprocess.CalledProcessError: |
2257 | + return [] |
2258 | + return [int(p) for p in raw_output.split()] |
2259 | + |
2260 | + result = [pid] |
2261 | + data = get_children(pid) |
2262 | + while data: |
2263 | + pid = data.pop(0) |
2264 | + result.append(pid) |
2265 | + data.extend(get_children(pid)) |
2266 | + |
2267 | + return result |
2268 | + |
2269 | + |
2270 | def make_proxy_object_from_service_name(service_name, obj_path): |
2271 | """Returns a root proxy object given a DBus service name.""" |
2272 | # parameters can sometimes be dbus.String instances, sometimes QString instances. |
2273 | |
2274 | === modified file 'autopilot/introspection/dbus.py' |
2275 | --- autopilot/introspection/dbus.py 2013-02-20 04:00:40 +0000 |
2276 | +++ autopilot/introspection/dbus.py 2013-04-15 21:43:32 +0000 |
2277 | @@ -23,7 +23,7 @@ |
2278 | from time import sleep |
2279 | from textwrap import dedent |
2280 | |
2281 | -from autopilot.emulators.dbus_handler import get_session_bus |
2282 | +from autopilot.dbus_handler import get_session_bus |
2283 | from autopilot.introspection.constants import AP_INTROSPECTION_IFACE |
2284 | from autopilot.utilities import Timer |
2285 | |
2286 | |
2287 | === modified file 'autopilot/introspection/gtk.py' |
2288 | --- autopilot/introspection/gtk.py 2013-01-25 01:47:48 +0000 |
2289 | +++ autopilot/introspection/gtk.py 2013-04-15 21:43:32 +0000 |
2290 | @@ -8,10 +8,10 @@ |
2291 | |
2292 | import os |
2293 | |
2294 | -from autopilot.introspection import ApplicationIntrospectionTestMixin |
2295 | - |
2296 | - |
2297 | -class GtkIntrospectionTestMixin(ApplicationIntrospectionTestMixin): |
2298 | +from autopilot.introspection import ApplicationLauncher |
2299 | + |
2300 | + |
2301 | +class GtkApplicationLauncher(ApplicationLauncher): |
2302 | """A mix-in class to make Gtk application introspection easier.""" |
2303 | |
2304 | def prepare_environment(self, app_path, arguments): |
2305 | |
2306 | === modified file 'autopilot/introspection/qt.py' |
2307 | --- autopilot/introspection/qt.py 2013-03-14 21:37:17 +0000 |
2308 | +++ autopilot/introspection/qt.py 2013-04-15 21:43:32 +0000 |
2309 | @@ -10,14 +10,14 @@ |
2310 | """Classes and tools to support Qt introspection.""" |
2311 | |
2312 | |
2313 | -__all__ = ['QtIntrospectionTestMixin'] |
2314 | +__all__ = ['QtApplicationLauncher'] |
2315 | |
2316 | import dbus |
2317 | import functools |
2318 | |
2319 | import logging |
2320 | |
2321 | -from autopilot.introspection import ApplicationIntrospectionTestMixin |
2322 | +from autopilot.introspection import ApplicationLauncher |
2323 | from autopilot.introspection.constants import QT_AUTOPILOT_IFACE |
2324 | from autopilot.introspection.dbus import get_session_bus |
2325 | |
2326 | @@ -25,7 +25,7 @@ |
2327 | logger = logging.getLogger(__name__) |
2328 | |
2329 | |
2330 | -class QtIntrospectionTestMixin(ApplicationIntrospectionTestMixin): |
2331 | +class QtApplicationLauncher(ApplicationLauncher): |
2332 | """A mix-in class to make Qt application introspection easier. |
2333 | |
2334 | Inherit from this class if you want to launch and test Qt application with |
2335 | |
2336 | === modified file 'autopilot/keybindings.py' |
2337 | --- autopilot/keybindings.py 2013-02-28 00:26:07 +0000 |
2338 | +++ autopilot/keybindings.py 2013-04-15 21:43:32 +0000 |
2339 | @@ -27,8 +27,8 @@ |
2340 | from types import NoneType |
2341 | import re |
2342 | |
2343 | -from autopilot.emulators.input import get_keyboard |
2344 | -from autopilot.compizconfig import get_plugin, get_setting |
2345 | +from autopilot.input import Keyboard |
2346 | +from autopilot.utilities import Silence |
2347 | |
2348 | logger = logging.getLogger(__name__) |
2349 | |
2350 | @@ -184,8 +184,8 @@ |
2351 | |
2352 | """ |
2353 | plugin_name, setting_name = compiz_tuple |
2354 | - plugin = get_plugin(plugin_name) |
2355 | - setting = get_setting(plugin_name, setting_name) |
2356 | + plugin = _get_compiz_plugin(plugin_name) |
2357 | + setting = _get_compiz_setting(plugin_name, setting_name) |
2358 | if setting.Type != 'Key': |
2359 | raise ValueError("Key binding maps to a compiz option that does not hold a keybinding.") |
2360 | if not plugin.Enabled: |
2361 | @@ -232,7 +232,7 @@ |
2362 | |
2363 | @property |
2364 | def _keyboard(self): |
2365 | - return get_keyboard() |
2366 | + return Keyboard.create() |
2367 | |
2368 | def keybinding(self, binding_name, delay=None): |
2369 | """Press and release the keybinding with the given name. |
2370 | @@ -263,3 +263,48 @@ |
2371 | def keybinding_hold_part_then_tap(self, binding_name): |
2372 | self.keybinding_hold(binding_name) |
2373 | self.keybinding_tap(binding_name) |
2374 | + |
2375 | + |
2376 | +# Functions that wrap compizconfig to avoid some unpleasantness in that module. |
2377 | +# Local to the this keybindings for now until their removal in the very near |
2378 | +# future. |
2379 | +_global_compiz_context = None |
2380 | + |
2381 | +def _get_global_compiz_context(): |
2382 | + """Get the compizconfig global context object.""" |
2383 | + global _global_compiz_context |
2384 | + if _global_compiz_context is None: |
2385 | + with Silence(): |
2386 | + from compizconfig import Context |
2387 | + _global_compiz_context = Context() |
2388 | + return _global_compiz_context |
2389 | + |
2390 | + |
2391 | +def _get_compiz_plugin(plugin_name): |
2392 | + """Get a compizconfig plugin with the specified name. |
2393 | + |
2394 | + Raises KeyError of the plugin named does not exist. |
2395 | + |
2396 | + """ |
2397 | + ctx = _get_global_compiz_context() |
2398 | + with Silence(): |
2399 | + try: |
2400 | + return ctx.Plugins[plugin_name] |
2401 | + except KeyError: |
2402 | + raise KeyError("Compiz plugin '%s' does not exist." % (plugin_name)) |
2403 | + |
2404 | + |
2405 | +def _get_compiz_setting(plugin_name, setting_name): |
2406 | + """Get a compizconfig setting object, given a plugin name and setting name. |
2407 | + |
2408 | + Raises KeyError if the plugin or setting is not found. |
2409 | + |
2410 | + """ |
2411 | + plugin = _get_compiz_plugin(plugin_name) |
2412 | + with Silence(): |
2413 | + try: |
2414 | + return plugin.Screen[setting_name] |
2415 | + except KeyError: |
2416 | + raise KeyError("Compiz setting '%s' does not exist in plugin '%s'." % (setting_name, plugin_name)) |
2417 | + |
2418 | + |
2419 | |
2420 | === modified file 'autopilot/matchers/__init__.py' |
2421 | --- autopilot/matchers/__init__.py 2012-12-05 02:15:45 +0000 |
2422 | +++ autopilot/matchers/__init__.py 2013-04-15 21:43:32 +0000 |
2423 | @@ -18,9 +18,54 @@ |
2424 | class Eventually(Matcher): |
2425 | """Asserts that a value will eventually equal a given Matcher object. |
2426 | |
2427 | - This works on objects that *either* have a :meth:`wait_for(expected)` |
2428 | - function, *or* objects that are callable and return the most current value |
2429 | - (i.e.- they refresh the objects value). |
2430 | + This matcher wraps another testtools matcher object. It makes that other |
2431 | + matcher work with a timeout. This is necessary for several reasons: |
2432 | + |
2433 | + 1. Since most actions in a GUI applicaton take some time to complete, the |
2434 | + test may need to wait for the application to enter the expected state. |
2435 | + |
2436 | + 2. Since the test is running in a separate process to the application under |
2437 | + test, test authors cannot make any assumptions about when the application |
2438 | + under test will recieve CPU time to update to the expected state. |
2439 | + |
2440 | + There are two main ways of using the Eventually matcher: |
2441 | + |
2442 | + **Attributes from the application**:: |
2443 | + |
2444 | + self.assertThat(window.maximized, Eventually(Equals(True))) |
2445 | + |
2446 | + Here, ``window`` is an object generated by autopilot from the applications |
2447 | + state. This pattern of usage will cover 90% (or more) of the assertions in |
2448 | + an autopilot test. Note that any matcher can be used - either from testtools |
2449 | + or any custom matcher that implements the matcher API:: |
2450 | + |
2451 | + self.assertThat(window.height, Eventually(GreaterThan(200))) |
2452 | + |
2453 | + **Callable Objects**:: |
2454 | + |
2455 | + self.assertThat(autopilot.platform.model, Eventually(Equals("Galaxy Nexus"))) |
2456 | + |
2457 | + In this example we're using the :func:`autopilot.platform.model` function as |
2458 | + a callable. In this form, Eventually matches against the return value of the |
2459 | + callable. |
2460 | + |
2461 | + This can also be used to use a regular python property inside an Eventually |
2462 | + matcher:: |
2463 | + |
2464 | + self.assertThat(lambda: self.mouse.x, Eventually(LessThan(10))) |
2465 | + |
2466 | + .. note:: Using this form generally makes your tests less readabvle, and |
2467 | + should be used with great care. It also relies the test author to have |
2468 | + knowledge about the implementation of the object being matched against. |
2469 | + In this example, if ``self.mouse.x`` were ever to change to be a regular |
2470 | + python attribute, this test would likely break. |
2471 | + |
2472 | + **Timeout** |
2473 | + |
2474 | + By default timeout period is ten seconds. This can be altered by passing the |
2475 | + timeout keyword:: |
2476 | + |
2477 | + self.assertThat(foo.bar, Eventually(Equals(123), timeout=30)) |
2478 | |
2479 | """ |
2480 | |
2481 | |
2482 | === added file 'autopilot/platform.py' |
2483 | --- autopilot/platform.py 1970-01-01 00:00:00 +0000 |
2484 | +++ autopilot/platform.py 2013-04-15 21:43:32 +0000 |
2485 | @@ -0,0 +1,105 @@ |
2486 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
2487 | +# Copyright 2013 Canonical |
2488 | +# Author: Thomi Richards |
2489 | +# |
2490 | +# This program is free software: you can redistribute it and/or modify it |
2491 | +# under the terms of the GNU General Public License version 3, as published |
2492 | +# by the Free Software Foundation. |
2493 | + |
2494 | +""" |
2495 | +Platform identification utilities for Autopilot. |
2496 | +================================================ |
2497 | + |
2498 | +This module provides functions that give test authors hints as to which platform |
2499 | +their tests are currently running on. This is useful when tests should only run |
2500 | +on certain platforms. |
2501 | + |
2502 | +""" |
2503 | + |
2504 | +from os.path import exists |
2505 | + |
2506 | + |
2507 | +def model(): |
2508 | + """Get the model name of the current platform. |
2509 | + |
2510 | + For desktop / laptop installations, this will return "Desktop". |
2511 | + Otherwise, the current hardware model will be returned. For example: |
2512 | + |
2513 | + >>> autopilot.platform.model() |
2514 | + ... "Galaxy Nexus" |
2515 | + |
2516 | + """ |
2517 | + return _PlatformDetector.create().model |
2518 | + |
2519 | + |
2520 | +def image_codename(): |
2521 | + """Get the image codename. |
2522 | + |
2523 | + For desktop / laptop installations this will return "Desktop". |
2524 | + Otherwise, the codename of the image that was installed will be |
2525 | + returned. For example: |
2526 | + |
2527 | + >>> autopilot.platform.image_codename() |
2528 | + ... "maguro" |
2529 | + |
2530 | + """ |
2531 | + return _PlatformDetector.create().image_codename |
2532 | + |
2533 | + |
2534 | +class _PlatformDetector(object): |
2535 | + |
2536 | + _cached_detector = None |
2537 | + |
2538 | + @staticmethod |
2539 | + def create(): |
2540 | + """Create a platform detector object, or return one we baked earlier.""" |
2541 | + if _PlatformDetector._cached_detector is None: |
2542 | + _PlatformDetector._cached_detector = _PlatformDetector() |
2543 | + return _PlatformDetector._cached_detector |
2544 | + |
2545 | + def __init__(self): |
2546 | + self.model = "Desktop" |
2547 | + self.image_codename = "Desktop" |
2548 | + |
2549 | + property_file = _get_property_file() |
2550 | + if property_file is not None: |
2551 | + self.update_values_from_build_file(property_file) |
2552 | + |
2553 | + def update_values_from_build_file(self, property_file): |
2554 | + """Read build.prop file and parse it.""" |
2555 | + properties = _parse_build_properties_file(property_file) |
2556 | + self.model = properties.get('ro.product.model', "Desktop") |
2557 | + self.image_codename = properties.get('ro.product.name', "Desktop") |
2558 | + |
2559 | + |
2560 | +def _get_property_file(): |
2561 | + """Return a file-like object that contains the contents of the build properties |
2562 | + file, if it exists, or None. |
2563 | + |
2564 | + """ |
2565 | + if exists('/system/build.prop'): |
2566 | + return open('/system/build.prop') |
2567 | + return None |
2568 | + |
2569 | + |
2570 | +def _parse_build_properties_file(property_file): |
2571 | + """Parse 'property_file', which must be a file-like object containing the |
2572 | + system build properties. |
2573 | + |
2574 | + Returns a dictionary of key,value pairs. |
2575 | + |
2576 | + """ |
2577 | + properties = {} |
2578 | + for line in property_file: |
2579 | + line = line.strip() |
2580 | + if not line or line.startswith('#') or line.isspace(): |
2581 | + continue |
2582 | + split_location = line.find('=') |
2583 | + if split_location == -1: |
2584 | + continue |
2585 | + key = line[:split_location] |
2586 | + value = line[split_location + 1:] |
2587 | + |
2588 | + properties[key] = value |
2589 | + return properties |
2590 | + |
2591 | |
2592 | === added directory 'autopilot/process' |
2593 | === added file 'autopilot/process/__init__.py' |
2594 | --- autopilot/process/__init__.py 1970-01-01 00:00:00 +0000 |
2595 | +++ autopilot/process/__init__.py 2013-04-15 21:43:32 +0000 |
2596 | @@ -0,0 +1,403 @@ |
2597 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
2598 | +# Copyright 2013 Canonical |
2599 | +# Author: Christopher Lee |
2600 | +# |
2601 | +# This program is free software: you can redistribute it and/or modify it |
2602 | +# under the terms of the GNU General Public License version 3, as published |
2603 | +# by the Free Software Foundation. |
2604 | + |
2605 | +from collections import OrderedDict |
2606 | + |
2607 | +from autopilot.utilities import _pick_variant |
2608 | + |
2609 | + |
2610 | +class ProcessManager(object): |
2611 | + |
2612 | + """A simple process manager class. |
2613 | + |
2614 | + The process manager is used to handle processes, windows and applications. |
2615 | + This class should not be instantiated directly however. To get an instance |
2616 | + of the keyboard class, call :py:meth:`create` instead. |
2617 | + |
2618 | + """ |
2619 | + |
2620 | + KNOWN_APPS = { |
2621 | + 'Character Map' : { |
2622 | + 'desktop-file': 'gucharmap.desktop', |
2623 | + 'process-name': 'gucharmap', |
2624 | + }, |
2625 | + 'Calculator' : { |
2626 | + 'desktop-file': 'gcalctool.desktop', |
2627 | + 'process-name': 'gnome-calculator', |
2628 | + }, |
2629 | + 'Mahjongg' : { |
2630 | + 'desktop-file': 'mahjongg.desktop', |
2631 | + 'process-name': 'gnome-mahjongg', |
2632 | + }, |
2633 | + 'Remmina' : { |
2634 | + 'desktop-file': 'remmina.desktop', |
2635 | + 'process-name': 'remmina', |
2636 | + }, |
2637 | + 'System Settings' : { |
2638 | + 'desktop-file': 'gnome-control-center.desktop', |
2639 | + 'process-name': 'gnome-control-center', |
2640 | + }, |
2641 | + 'Text Editor' : { |
2642 | + 'desktop-file': 'gedit.desktop', |
2643 | + 'process-name': 'gedit', |
2644 | + }, |
2645 | + 'Terminal' : { |
2646 | + 'desktop-file': 'gnome-terminal.desktop', |
2647 | + 'process-name': 'gnome-terminal', |
2648 | + }, |
2649 | + } |
2650 | + |
2651 | + |
2652 | + @staticmethod |
2653 | + def create(preferred_variant=""): |
2654 | + """Get an instance of the :py:class:`ProcessManager` class. |
2655 | + |
2656 | + :param preferred_variant: A string containing a hint as to which variant you |
2657 | + would like. However, this hint can be ignored - autopilot will prefer to |
2658 | + return a keyboard variant other than the one requested, rather than fail |
2659 | + to return anything at all. |
2660 | + :raises: a RuntimeError will be raised if autopilot cannot instantate any of |
2661 | + the possible backends. |
2662 | + """ |
2663 | + def get_bamf_pm(): |
2664 | + from autopilot.process._bamf import ProcessManager |
2665 | + return ProcessManager() |
2666 | + |
2667 | + def get_upa_pm(): |
2668 | + from autopilot.process._upa import ProcessManager |
2669 | + return ProcessManager() |
2670 | + |
2671 | + variants = OrderedDict() |
2672 | + variants['BAMF'] = get_bamf_pm |
2673 | + return _pick_variant(variants, preferred_variant) |
2674 | + |
2675 | + @classmethod |
2676 | + def register_known_application(cls, name, desktop_file, process_name): |
2677 | + """Register an application with autopilot. |
2678 | + |
2679 | + After calling this method, you may call :meth:`start_app` or |
2680 | + :meth:`start_app_window` with the `name` parameter to start this |
2681 | + application. |
2682 | + You need only call this once within a test run - the application will |
2683 | + remain registerred until the test run ends. |
2684 | + |
2685 | + :param name: The name to be used when launching the application. |
2686 | + :param desktop_file: The filename (without path component) of the desktop file used to launch the application. |
2687 | + :param process_name: The name of the executable process that gets run. |
2688 | + :raises: **KeyError** if application has been registered already |
2689 | + |
2690 | + """ |
2691 | + if name in cls.KNOWN_APPS: |
2692 | + raise KeyError("Application has been registered already") |
2693 | + else: |
2694 | + cls.KNOWN_APPS[name] = { |
2695 | + "desktop-file" : desktop_file, |
2696 | + "process-name" : process_name |
2697 | + } |
2698 | + |
2699 | + @classmethod |
2700 | + def unregister_known_application(cls, name): |
2701 | + """Unregister an application with the known_apps dictionary. |
2702 | + |
2703 | + :param name: The name to be used when launching the application. |
2704 | + :raises: **KeyError** if the application has not been registered. |
2705 | + |
2706 | + """ |
2707 | + if name in cls.KNOWN_APPS: |
2708 | + del cls.KNOWN_APPS[name] |
2709 | + else: |
2710 | + raise KeyError("Application has not been registered") |
2711 | + |
2712 | + def start_app(self, app_name, files=[], locale=None): |
2713 | + """Start one of the known applications, and kill it on tear down. |
2714 | + |
2715 | + .. warning:: This method will clear all instances of this application on |
2716 | + tearDown, not just the one opened by this method! We recommend that |
2717 | + you use the :meth:`start_app_window` method instead, as it is generally |
2718 | + safer. |
2719 | + |
2720 | + :param app_name: The application name. *This name must either already |
2721 | + be registered as one of the built-in applications that are supported |
2722 | + by autopilot, or must have been registered using* |
2723 | + :meth:`register_known_application` *beforehand.* |
2724 | + :param files: (Optional) A list of paths to open with the |
2725 | + given application. *Not all applications support opening files in this |
2726 | + way.* |
2727 | + :param locale: (Optional) The locale will to set when the application |
2728 | + is launched. *If you want to launch an application without any |
2729 | + localisation being applied, set this parameter to 'C'.* |
2730 | + :returns: A :class:`~autopilot.process.Application` instance. |
2731 | + |
2732 | + """ |
2733 | + raise NotImplementedError("You cannot use this class directly.") |
2734 | + |
2735 | + def start_app_window(self, app_name, files=[], locale=None): |
2736 | + """Open a single window for one of the known applications, and close it |
2737 | + at the end of the test. |
2738 | + |
2739 | + :param app_name: The application name. *This name must either already |
2740 | + be registered as one of the built-in applications that are supported |
2741 | + by autopilot, or must have been registered with* |
2742 | + :meth:`register_known_application` *beforehand.* |
2743 | + :param files: (Optional) Should be a list of paths to open with the |
2744 | + given application. *Not all applications support opening files in this |
2745 | + way.* |
2746 | + :param locale: (Optional) The locale will to set when the application |
2747 | + is launched. *If you want to launch an application without any |
2748 | + localisation being applied, set this parameter to 'C'.* |
2749 | + :raises: **AssertionError** if no window was opened, or more than one |
2750 | + window was opened. |
2751 | + :returns: A :class:`~autopilot.process.Window` instance. |
2752 | + |
2753 | + """ |
2754 | + raise NotImplementedError("You cannot use this class directly.") |
2755 | + |
2756 | + def get_open_windows_by_application(self, app_name): |
2757 | + """Get a list of ~autopilot.process.Window` instances |
2758 | + for the given application name. |
2759 | + |
2760 | + :param app_name: The name of one of the well-known applications. |
2761 | + :returns: A list of :class:`~autopilot.process.Window` |
2762 | + instances. |
2763 | + |
2764 | + """ |
2765 | + raise NotImplementedError("You cannot use this class directly.") |
2766 | + |
2767 | + def close_all_app(self, app_name): |
2768 | + raise NotImplementedError("You cannot use this class directly.") |
2769 | + |
2770 | + def get_app_instances(self, app_name): |
2771 | + raise NotImplementedError("You cannot use this class directly.") |
2772 | + |
2773 | + def app_is_running(self, app_name): |
2774 | + raise NotImplementedError("You cannot use this class directly.") |
2775 | + |
2776 | + def get_running_applications(self, user_visible_only=True): |
2777 | + """Get a list of the currently running applications. |
2778 | + |
2779 | + If user_visible_only is True (the default), only applications |
2780 | + visible to the user in the switcher will be returned. |
2781 | + |
2782 | + """ |
2783 | + raise NotImplementedError("You cannot use this class directly.") |
2784 | + |
2785 | + def get_running_applications_by_desktop_file(self, desktop_file): |
2786 | + """Return a list of applications that have the desktop file *desktop_file*. |
2787 | + |
2788 | + This method will return an empty list if no applications |
2789 | + are found with the specified desktop file. |
2790 | + |
2791 | + """ |
2792 | + raise NotImplementedError("You cannot use this class directly.") |
2793 | + |
2794 | + def get_open_windows(self, user_visible_only=True): |
2795 | + """Get a list of currently open windows. |
2796 | + |
2797 | + If *user_visible_only* is True (the default), only applications visible |
2798 | + to the user in the switcher will be returned. |
2799 | + |
2800 | + The result is sorted to be in stacking order. |
2801 | + |
2802 | + """ |
2803 | + raise NotImplementedError("You cannot use this class directly.") |
2804 | + |
2805 | + def wait_until_application_is_running(self, desktop_file, timeout): |
2806 | + """Wait until a given application is running. |
2807 | + |
2808 | + :param string desktop_file: The name of the application desktop file. |
2809 | + :param integer timeout: The maximum time to wait, in seconds. *If set to |
2810 | + something less than 0, this method will wait forever.* |
2811 | + |
2812 | + :return: true once the application is found, or false if the application |
2813 | + was not found until the timeout was reached. |
2814 | + """ |
2815 | + raise NotImplementedError("You cannot use this class directly.") |
2816 | + |
2817 | + def launch_application(self, desktop_file, files=[], wait=True): |
2818 | + """Launch an application by specifying a desktop file. |
2819 | + |
2820 | + :param files: List of files to pass to the application. *Not all |
2821 | + apps support this.* |
2822 | + :type files: List of strings |
2823 | + |
2824 | + .. note:: If `wait` is True, this method will wait up to 10 seconds for |
2825 | + the application to appear. |
2826 | + |
2827 | + :raises: **TypeError** on invalid *files* parameter. |
2828 | + :return: The Gobject process object. |
2829 | + """ |
2830 | + raise NotImplementedError("You cannot use this class directly.") |
2831 | + |
2832 | + |
2833 | +class Application(object): |
2834 | + @property |
2835 | + def desktop_file(self): |
2836 | + """Get the application desktop file. |
2837 | + |
2838 | + This returns just the filename, not the full path. |
2839 | + If the application no longer exists, this returns an empty string. |
2840 | + """ |
2841 | + raise NotImplementedError("You cannot use this class directly.") |
2842 | + |
2843 | + @property |
2844 | + def name(self): |
2845 | + """Get the application name. |
2846 | + |
2847 | + .. note:: This may change according to the current locale. If you want a |
2848 | + unique string to match applications against, use desktop_file instead. |
2849 | + |
2850 | + """ |
2851 | + raise NotImplementedError("You cannot use this class directly.") |
2852 | + |
2853 | + @property |
2854 | + def icon(self): |
2855 | + """Get the application icon. |
2856 | + |
2857 | + :return: The name of the icon. |
2858 | + |
2859 | + """ |
2860 | + raise NotImplementedError("You cannot use this class directly.") |
2861 | + |
2862 | + @property |
2863 | + def is_active(self): |
2864 | + """Is the application active (i.e. has keyboard focus)?""" |
2865 | + raise NotImplementedError("You cannot use this class directly.") |
2866 | + |
2867 | + @property |
2868 | + def is_urgent(self): |
2869 | + """Is the application currently signalling urgency?""" |
2870 | + raise NotImplementedError("You cannot use this class directly.") |
2871 | + |
2872 | + @property |
2873 | + def user_visible(self): |
2874 | + """Is this application visible to the user? |
2875 | + |
2876 | + .. note:: Some applications (such as the panel) are hidden to the user |
2877 | + but may still be returned. |
2878 | + |
2879 | + """ |
2880 | + raise NotImplementedError("You cannot use this class directly.") |
2881 | + |
2882 | + def get_windows(self): |
2883 | + """Get a list of the application windows.""" |
2884 | + raise NotImplementedError("You cannot use this class directly.") |
2885 | + |
2886 | + |
2887 | + |
2888 | +class Window(object): |
2889 | + @property |
2890 | + def x_id(self): |
2891 | + """Get the X11 Window Id.""" |
2892 | + raise NotImplementedError("You cannot use this class directly.") |
2893 | + |
2894 | + @property |
2895 | + def x_win(self): |
2896 | + """Get the X11 window object of the underlying window.""" |
2897 | + raise NotImplementedError("You cannot use this class directly.") |
2898 | + |
2899 | + @property |
2900 | + def get_wm_state(self): |
2901 | + """Get the state of the underlying window.""" |
2902 | + raise NotImplementedError("You cannot use this class directly.") |
2903 | + |
2904 | + @property |
2905 | + def name(self): |
2906 | + """Get the window name. |
2907 | + |
2908 | + .. note:: This may change according to the current locale. If you want a |
2909 | + unique string to match windows against, use the x_id instead. |
2910 | + |
2911 | + """ |
2912 | + raise NotImplementedError("You cannot use this class directly.") |
2913 | + |
2914 | + @property |
2915 | + def title(self): |
2916 | + """Get the window title. |
2917 | + |
2918 | + This may be different from the application name. |
2919 | + |
2920 | + .. note:: This may change depending on the current locale. |
2921 | + |
2922 | + """ |
2923 | + raise NotImplementedError("You cannot use this class directly.") |
2924 | + |
2925 | + @property |
2926 | + def geometry(self): |
2927 | + """Get the geometry for this window. |
2928 | + |
2929 | + :return: Tuple containing (x, y, width, height). |
2930 | + |
2931 | + """ |
2932 | + raise NotImplementedError("You cannot use this class directly.") |
2933 | + |
2934 | + @property |
2935 | + def is_maximized(self): |
2936 | + """Is the window maximized? |
2937 | + |
2938 | + Maximized in this case means both maximized vertically and |
2939 | + horizontally. If a window is only maximized in one direction it is not |
2940 | + considered maximized. |
2941 | + |
2942 | + """ |
2943 | + raise NotImplementedError("You cannot use this class directly.") |
2944 | + |
2945 | + @property |
2946 | + def application(self): |
2947 | + """Get the application that owns this window. |
2948 | + |
2949 | + This method may return None if the window does not have an associated |
2950 | + application. The 'desktop' window is one such example. |
2951 | + |
2952 | + """ |
2953 | + raise NotImplementedError("You cannot use this class directly.") |
2954 | + |
2955 | + @property |
2956 | + def user_visible(self): |
2957 | + """Is this window visible to the user in the switcher?""" |
2958 | + raise NotImplementedError("You cannot use this class directly.") |
2959 | + |
2960 | + @property |
2961 | + def is_hidden(self): |
2962 | + """Is this window hidden? |
2963 | + |
2964 | + Windows are hidden when the 'Show Desktop' mode is activated. |
2965 | + |
2966 | + """ |
2967 | + raise NotImplementedError("You cannot use this class directly.") |
2968 | + |
2969 | + @property |
2970 | + def is_focused(self): |
2971 | + """Is this window focused?""" |
2972 | + raise NotImplementedError("You cannot use this class directly.") |
2973 | + |
2974 | + @property |
2975 | + def is_valid(self): |
2976 | + """Is this window object valid? |
2977 | + |
2978 | + Invalid windows are caused by windows closing during the construction of |
2979 | + this object instance. |
2980 | + |
2981 | + """ |
2982 | + raise NotImplementedError("You cannot use this class directly.") |
2983 | + |
2984 | + @property |
2985 | + def monitor(self): |
2986 | + """Returns the monitor to which the windows belongs to""" |
2987 | + raise NotImplementedError("You cannot use this class directly.") |
2988 | + |
2989 | + @property |
2990 | + def closed(self): |
2991 | + """Returns True if the window has been closed""" |
2992 | + raise NotImplementedError("You cannot use this class directly.") |
2993 | + |
2994 | + def close(self): |
2995 | + """Close the window.""" |
2996 | + raise NotImplementedError("You cannot use this class directly.") |
2997 | + |
2998 | + def set_focus(self): |
2999 | + raise NotImplementedError("You cannot use this class directly.") |
3000 | |
3001 | === renamed file 'autopilot/emulators/bamf.py' => 'autopilot/process/_bamf.py' |
3002 | --- autopilot/emulators/bamf.py 2013-01-06 21:51:56 +0000 |
3003 | +++ autopilot/process/_bamf.py 2013-04-15 21:43:32 +0000 |
3004 | @@ -5,7 +5,7 @@ |
3005 | # under the terms of the GNU General Public License version 3, as published |
3006 | # by the Free Software Foundation. |
3007 | |
3008 | -"""Various classes for interacting with BAMF.""" |
3009 | +"""BAMF implementation of the Process Management""" |
3010 | |
3011 | from __future__ import absolute_import |
3012 | |
3013 | @@ -13,21 +13,25 @@ |
3014 | import dbus.glib |
3015 | from gi.repository import Gio |
3016 | from gi.repository import GLib |
3017 | +import logging |
3018 | import os |
3019 | +from time import sleep |
3020 | from Xlib import display, X, protocol |
3021 | |
3022 | -from autopilot.emulators.dbus_handler import get_session_bus |
3023 | -from autopilot.utilities import Silence |
3024 | - |
3025 | -__all__ = [ |
3026 | - "Bamf", |
3027 | - "BamfApplication", |
3028 | - "BamfWindow", |
3029 | - ] |
3030 | +from autopilot.dbus_handler import get_session_bus |
3031 | +from autopilot.utilities import addCleanup, Silence |
3032 | + |
3033 | +from autopilot.process import ( |
3034 | + ProcessManager as ProcessManagerBase, |
3035 | + Application as ApplicationBase, |
3036 | + Window as WindowBase |
3037 | + ) |
3038 | + |
3039 | |
3040 | _BAMF_BUS_NAME = 'org.ayatana.bamf' |
3041 | _X_DISPLAY = None |
3042 | |
3043 | +logger = logging.getLogger(__name__) |
3044 | |
3045 | def get_display(): |
3046 | """Create an Xlib display object (silently) and return it.""" |
3047 | @@ -51,7 +55,7 @@ |
3048 | return False |
3049 | |
3050 | |
3051 | -class Bamf(object): |
3052 | +class ProcessManager(ProcessManagerBase): |
3053 | """High-level class for interacting with Bamf from within a test. |
3054 | |
3055 | Use this class to inspect the state of running applications and open |
3056 | @@ -65,6 +69,126 @@ |
3057 | self.matcher_proxy = get_session_bus().get_object(_BAMF_BUS_NAME, matcher_path) |
3058 | self.matcher_interface = dbus.Interface(self.matcher_proxy, self.matcher_interface_name) |
3059 | |
3060 | + def start_app(self, app_name, files=[], locale=None): |
3061 | + """Start one of the known applications, and kill it on tear down. |
3062 | + |
3063 | + .. warning:: This method will clear all instances of this application on |
3064 | + tearDown, not just the one opened by this method! We recommend that |
3065 | + you use the :meth:`start_app_window` method instead, as it is generally |
3066 | + safer. |
3067 | + |
3068 | + :param app_name: The application name. *This name must either already |
3069 | + be registered as one of the built-in applications that are supported |
3070 | + by autopilot, or must have been registered using* |
3071 | + :meth:`register_known_application` *beforehand.* |
3072 | + :param files: (Optional) A list of paths to open with the |
3073 | + given application. *Not all applications support opening files in this |
3074 | + way.* |
3075 | + :param locale: (Optional) The locale will to set when the application |
3076 | + is launched. *If you want to launch an application without any |
3077 | + localisation being applied, set this parameter to 'C'.* |
3078 | + :returns: A :class:`~autopilot.process.Application` instance. |
3079 | + |
3080 | + """ |
3081 | + window = self._open_window(app_name, files, locale) |
3082 | + if window: |
3083 | + addCleanup(self.close_all_app, app_name) |
3084 | + return window.application |
3085 | + |
3086 | + raise AssertionError("No new application window was opened.") |
3087 | + |
3088 | + def start_app_window(self, app_name, files=[], locale=None): |
3089 | + """Open a single window for one of the known applications, and close it |
3090 | + at the end of the test. |
3091 | + |
3092 | + :param app_name: The application name. *This name must either already |
3093 | + be registered as one of the built-in applications that are supported |
3094 | + by autopilot, or must have been registered with* |
3095 | + :meth:`register_known_application` *beforehand.* |
3096 | + :param files: (Optional) Should be a list of paths to open with the |
3097 | + given application. *Not all applications support opening files in this |
3098 | + way.* |
3099 | + :param locale: (Optional) The locale will to set when the application |
3100 | + is launched. *If you want to launch an application without any |
3101 | + localisation being applied, set this parameter to 'C'.* |
3102 | + :raises: **AssertionError** if no window was opened, or more than one |
3103 | + window was opened. |
3104 | + :returns: A :class:`~autopilot.process.Window` instance. |
3105 | + |
3106 | + """ |
3107 | + window = self._open_window(app_name, files, locale) |
3108 | + if window: |
3109 | + addCleanup(window.close) |
3110 | + return window |
3111 | + raise AssertionError("No window was opened.") |
3112 | + |
3113 | + def _open_window(self, app_name, files, locale): |
3114 | + """Open a new 'app_name' window, returning the window instance or None. |
3115 | + |
3116 | + Raises an AssertionError if this creates more than one window. |
3117 | + |
3118 | + """ |
3119 | + existing_windows = self.get_open_windows_by_application(app_name) |
3120 | + |
3121 | + if locale: |
3122 | + os.putenv("LC_ALL", locale) |
3123 | + addCleanup(os.unsetenv, "LC_ALL") |
3124 | + logger.info("Starting application '%s' with files %r in locale %s", app_name, files, locale) |
3125 | + else: |
3126 | + logger.info("Starting application '%s' with files %r", app_name, files) |
3127 | + |
3128 | + |
3129 | + app = self.KNOWN_APPS[app_name] |
3130 | + self.launch_application(app['desktop-file'], files) |
3131 | + apps = self.get_running_applications_by_desktop_file(app['desktop-file']) |
3132 | + |
3133 | + for i in range(10): |
3134 | + try: |
3135 | + new_windows = [] |
3136 | + [new_windows.extend(a.get_windows()) for a in apps] |
3137 | + filter_fn = lambda w: w.x_id not in [c.x_id for c in existing_windows] |
3138 | + new_wins = filter(filter_fn, new_windows) |
3139 | + if new_wins: |
3140 | + assert len(new_wins) == 1 |
3141 | + return new_wins[0] |
3142 | + except DBusException: |
3143 | + pass |
3144 | + sleep(1) |
3145 | + return None |
3146 | + |
3147 | + def get_open_windows_by_application(self, app_name): |
3148 | + """Get a list of ~autopilot.process.Window` instances |
3149 | + for the given application name. |
3150 | + |
3151 | + :param app_name: The name of one of the well-known applications. |
3152 | + :returns: A list of :class:`~autopilot.process.Window` |
3153 | + instances. |
3154 | + |
3155 | + """ |
3156 | + existing_windows = [] |
3157 | + [existing_windows.extend(a.get_windows()) for a in self.get_app_instances(app_name)] |
3158 | + return existing_windows |
3159 | + |
3160 | + def close_all_app(self, app_name): |
3161 | + """Close all instances of the application 'app_name'.""" |
3162 | + app = self.KNOWN_APPS[app_name] |
3163 | + try: |
3164 | + pids = check_output(["pidof", app['process-name']]).split() |
3165 | + if len(pids): |
3166 | + call(["kill"] + pids) |
3167 | + except CalledProcessError: |
3168 | + logger.warning("Tried to close applicaton '%s' but it wasn't running.", app_name) |
3169 | + |
3170 | + def get_app_instances(self, app_name): |
3171 | + """Get `~autopilot.process.Application` instances for app_name.""" |
3172 | + desktop_file = self.KNOWN_APPS[app_name]['desktop-file'] |
3173 | + return self.get_running_applications_by_desktop_file(desktop_file) |
3174 | + |
3175 | + def app_is_running(self, app_name): |
3176 | + """Return true if an instance of the application is running.""" |
3177 | + apps = self.get_app_instances(app_name) |
3178 | + return len(apps) > 0 |
3179 | + |
3180 | def get_running_applications(self, user_visible_only=True): |
3181 | """Get a list of the currently running applications. |
3182 | |
3183 | @@ -72,7 +196,7 @@ |
3184 | visible to the user in the switcher will be returned. |
3185 | |
3186 | """ |
3187 | - apps = [BamfApplication(p) for p in self.matcher_interface.RunningApplications()] |
3188 | + apps = [Application(p) for p in self.matcher_interface.RunningApplications()] |
3189 | if user_visible_only: |
3190 | return filter(_filter_user_visible, apps) |
3191 | return apps |
3192 | @@ -80,7 +204,7 @@ |
3193 | def get_running_applications_by_desktop_file(self, desktop_file): |
3194 | """Return a list of applications that have the desktop file *desktop_file*. |
3195 | |
3196 | - This method may return an empty list, if no applications |
3197 | + This method will return an empty list if no applications |
3198 | are found with the specified desktop file. |
3199 | |
3200 | """ |
3201 | @@ -93,14 +217,6 @@ |
3202 | pass |
3203 | return apps |
3204 | |
3205 | - def get_application_by_xid(self, xid): |
3206 | - """Return the application that has a child with the requested xid or None.""" |
3207 | - |
3208 | - app_path = self.matcher_interface.ApplicationForXid(xid) |
3209 | - if len(app_path): |
3210 | - return BamfApplication(app_path) |
3211 | - return None |
3212 | - |
3213 | def get_open_windows(self, user_visible_only=True): |
3214 | """Get a list of currently open windows. |
3215 | |
3216 | @@ -111,7 +227,7 @@ |
3217 | |
3218 | """ |
3219 | |
3220 | - windows = [BamfWindow(w) for w in self.matcher_interface.WindowStackForMonitor(-1)] |
3221 | + windows = [Window(w) for w in self.matcher_interface.WindowStackForMonitor(-1)] |
3222 | if user_visible_only: |
3223 | windows = filter(_filter_user_visible, windows) |
3224 | # Now sort on stacking order. |
3225 | @@ -119,11 +235,6 @@ |
3226 | # try and use len() on return values from these methods. |
3227 | return list(reversed(windows)) |
3228 | |
3229 | - def get_window_by_xid(self, xid): |
3230 | - """Get the BamfWindow that matches the provided *xid*.""" |
3231 | - windows = [BamfWindow(w) for w in self.matcher_interface.WindowPaths() if BamfWindow(w).x_id == xid] |
3232 | - return windows[0] if windows else None |
3233 | - |
3234 | def wait_until_application_is_running(self, desktop_file, timeout): |
3235 | """Wait until a given application is running. |
3236 | |
3237 | @@ -147,7 +258,7 @@ |
3238 | # No, so define a callback to watch the ViewOpened signal: |
3239 | def on_view_added(bamf_path, name): |
3240 | if bamf_path.split('/')[-1].startswith('application'): |
3241 | - app = BamfApplication(bamf_path) |
3242 | + app = Application(bamf_path) |
3243 | if desktop_file == os.path.split(app.desktop_file)[1]: |
3244 | gobject_loop.quit() |
3245 | |
3246 | @@ -192,7 +303,7 @@ |
3247 | return proc |
3248 | |
3249 | |
3250 | -class BamfApplication(object): |
3251 | +class Application(ApplicationBase): |
3252 | """Represents an application, with information as returned by Bamf. |
3253 | |
3254 | .. important:: Don't instantiate this class yourself. instead, use the |
3255 | @@ -265,20 +376,20 @@ |
3256 | |
3257 | def get_windows(self): |
3258 | """Get a list of the application windows.""" |
3259 | - return [BamfWindow(w) for w in self._view_iface.Children()] |
3260 | + return [Window(w) for w in self._view_iface.Children()] |
3261 | |
3262 | def __repr__(self): |
3263 | - return "<BamfApplication '%s'>" % (self.name) |
3264 | + return "<Application '%s'>" % (self.name) |
3265 | |
3266 | def __eq__(self, other): |
3267 | return self.desktop_file == other.desktop_file |
3268 | |
3269 | |
3270 | -class BamfWindow(object): |
3271 | +class Window(WindowBase): |
3272 | """Represents an application window, as returned by Bamf. |
3273 | |
3274 | .. important:: Don't instantiate this class yourself. Instead, use the |
3275 | - appropriate methods in BamfApplication. |
3276 | + appropriate methods in Application. |
3277 | |
3278 | """ |
3279 | def __init__(self, window_path): |
3280 | @@ -364,7 +475,7 @@ |
3281 | # associated application. For these windows we return none. |
3282 | parents = self._view_iface.Parents() |
3283 | if parents: |
3284 | - return BamfApplication(parents[0]) |
3285 | + return Application(parents[0]) |
3286 | else: |
3287 | return None |
3288 | |
3289 | @@ -423,7 +534,7 @@ |
3290 | self._x_win.configure(stack_mode=X.Above) |
3291 | |
3292 | def __repr__(self): |
3293 | - return "<BamfWindow '%s' Xid: %d>" % (self.title if self._x_win else '', self.x_id) |
3294 | + return "<Window '%s' Xid: %d>" % (self.title if self._x_win else '', self.x_id) |
3295 | |
3296 | def _getProperty(self, _type): |
3297 | """Get an X11 property. |
3298 | |
3299 | === modified file 'autopilot/testcase.py' |
3300 | --- autopilot/testcase.py 2013-04-05 14:02:59 +0000 |
3301 | +++ autopilot/testcase.py 2013-04-15 21:43:32 +0000 |
3302 | @@ -11,40 +11,35 @@ |
3303 | from __future__ import absolute_import |
3304 | |
3305 | from dbus import DBusException |
3306 | +from gi.repository import Gio |
3307 | import logging |
3308 | import os |
3309 | -from StringIO import StringIO |
3310 | +import signal |
3311 | from subprocess import ( |
3312 | call, |
3313 | CalledProcessError, |
3314 | check_output, |
3315 | - Popen, |
3316 | - PIPE, |
3317 | - STDOUT, |
3318 | ) |
3319 | |
3320 | from testscenarios import TestWithScenarios |
3321 | from testtools import TestCase |
3322 | from testtools.content import text_content |
3323 | from testtools.matchers import Equals |
3324 | -import time |
3325 | +from time import sleep |
3326 | |
3327 | -from autopilot.compizconfig import get_global_context |
3328 | -from autopilot.emulators.bamf import Bamf |
3329 | -from autopilot.emulators.zeitgeist import Zeitgeist |
3330 | -from autopilot.emulators.processmanager import ProcessManager |
3331 | -from autopilot.emulators.X11 import ScreenGeometry, reset_display |
3332 | -from autopilot.emulators.input import get_keyboard, get_mouse |
3333 | -from autopilot.glibrunner import AutopilotTestRunner |
3334 | -from autopilot.globals import (get_log_verbose, |
3335 | - get_video_recording_enabled, |
3336 | - get_video_record_directory, |
3337 | +from autopilot.process import ProcessManager |
3338 | +from autopilot.input import Keyboard, Mouse |
3339 | +from autopilot.introspection import ( |
3340 | + get_application_launcher, |
3341 | + get_autopilot_proxy_object_for_process, |
3342 | + launch_application, |
3343 | + launch_process, |
3344 | ) |
3345 | +from autopilot.display import Display |
3346 | +from autopilot.globals import on_test_started |
3347 | from autopilot.keybindings import KeybindingsHelper |
3348 | from autopilot.matchers import Eventually |
3349 | -from autopilot.utilities import (get_compiz_setting, |
3350 | - LogFormatter, |
3351 | - ) |
3352 | + |
3353 | |
3354 | logger = logging.getLogger(__name__) |
3355 | |
3356 | @@ -74,108 +69,7 @@ |
3357 | return result |
3358 | |
3359 | |
3360 | -class LoggedTestCase(TestWithScenarios, TestCase): |
3361 | - """Initialize the logging for the test case.""" |
3362 | - |
3363 | - def setUp(self): |
3364 | - self._setUpTestLogging() |
3365 | - # The reason that the super setup is done here is due to making sure |
3366 | - # that the logging is properly set up prior to calling it. |
3367 | - super(LoggedTestCase, self).setUp() |
3368 | - if get_log_verbose(): |
3369 | - logger.info("*" * 60) |
3370 | - logger.info("Starting test %s", self.shortDescription()) |
3371 | - |
3372 | - def _setUpTestLogging(self): |
3373 | - self._log_buffer = StringIO() |
3374 | - root_logger = logging.getLogger() |
3375 | - root_logger.setLevel(logging.DEBUG) |
3376 | - formatter = LogFormatter() |
3377 | - self._log_handler = logging.StreamHandler(stream=self._log_buffer) |
3378 | - self._log_handler.setFormatter(formatter) |
3379 | - root_logger.addHandler(self._log_handler) |
3380 | - |
3381 | - #Tear down logging in a cleanUp handler, so it's done after all other |
3382 | - # tearDown() calls and cleanup handlers. |
3383 | - self.addCleanup(self._tearDownLogging) |
3384 | - |
3385 | - def _tearDownLogging(self): |
3386 | - root_logger = logging.getLogger() |
3387 | - self._log_handler.flush() |
3388 | - self._log_buffer.seek(0) |
3389 | - self.addDetail('test-log', text_content(self._log_buffer.getvalue())) |
3390 | - root_logger.removeHandler(self._log_handler) |
3391 | - # Calling del to remove the handler and flush the buffer. We are |
3392 | - # abusing the log handlers here a little. |
3393 | - del self._log_buffer |
3394 | - |
3395 | - |
3396 | - |
3397 | -class VideoCapturedTestCase(LoggedTestCase): |
3398 | - """Video capture autopilot tests, saving the results if the test failed.""" |
3399 | - |
3400 | - _recording_app = '/usr/bin/recordmydesktop' |
3401 | - _recording_opts = ['--no-sound', '--no-frame', '-o',] |
3402 | - |
3403 | - def setUp(self): |
3404 | - super(VideoCapturedTestCase, self).setUp() |
3405 | - global video_recording_enabled |
3406 | - if get_video_recording_enabled() and not self._have_recording_app(): |
3407 | - video_recording_enabled = False |
3408 | - logger.warning("Disabling video capture since '%s' is not present", self._recording_app) |
3409 | - |
3410 | - if get_video_recording_enabled(): |
3411 | - self._test_passed = True |
3412 | - self.addOnException(self._on_test_failed) |
3413 | - self.addCleanup(self._stop_video_capture) |
3414 | - self._start_video_capture() |
3415 | - |
3416 | - def _have_recording_app(self): |
3417 | - return os.path.exists(self._recording_app) |
3418 | - |
3419 | - def _start_video_capture(self): |
3420 | - args = self._get_capture_command_line() |
3421 | - self._capture_file = self._get_capture_output_file() |
3422 | - self._ensure_directory_exists_but_not_file(self._capture_file) |
3423 | - args.append(self._capture_file) |
3424 | - logger.debug("Starting: %r", args) |
3425 | - self._capture_process = Popen(args, stdout=PIPE, stderr=STDOUT) |
3426 | - |
3427 | - def _stop_video_capture(self): |
3428 | - """Stop the video capture. If the test failed, save the resulting file.""" |
3429 | - |
3430 | - if self._test_passed: |
3431 | - # We use kill here because we don't want the recording app to start |
3432 | - # encoding the video file (since we're removing it anyway.) |
3433 | - self._capture_process.kill() |
3434 | - self._capture_process.wait() |
3435 | - else: |
3436 | - self._capture_process.terminate() |
3437 | - self._capture_process.wait() |
3438 | - if self._capture_process.returncode != 0: |
3439 | - self.addDetail('video capture log', text_content(self._capture_process.stdout.read())) |
3440 | - self._capture_process = None |
3441 | - |
3442 | - def _get_capture_command_line(self): |
3443 | - return [self._recording_app] + self._recording_opts |
3444 | - |
3445 | - def _get_capture_output_file(self): |
3446 | - return os.path.join(get_video_record_directory(), '%s.ogv' % (self.shortDescription())) |
3447 | - |
3448 | - def _ensure_directory_exists_but_not_file(self, file_path): |
3449 | - dirpath = os.path.dirname(file_path) |
3450 | - if not os.path.exists(dirpath): |
3451 | - os.makedirs(dirpath) |
3452 | - elif os.path.exists(file_path): |
3453 | - logger.warning("Video capture file '%s' already exists, deleting.", file_path) |
3454 | - os.remove(file_path) |
3455 | - |
3456 | - def _on_test_failed(self, ex_info): |
3457 | - """Called when a test fails.""" |
3458 | - self._test_passed = False |
3459 | - |
3460 | - |
3461 | -class AutopilotTestCase(VideoCapturedTestCase, KeybindingsHelper): |
3462 | +class AutopilotTestCase(TestWithScenarios, TestCase, KeybindingsHelper): |
3463 | """Wrapper around testtools.TestCase that adds significant functionality. |
3464 | |
3465 | This class should be the base class for all autopilot test case classes. Not |
3466 | @@ -190,19 +84,11 @@ |
3467 | :meth:`~autopilot.testcase.AutopilotTestCase.start_app` and |
3468 | :meth:`~autopilot.testcase.AutopilotTestCase.start_app_window` which will |
3469 | launch one of the well-known applications and return a |
3470 | - :class:`~autopilot.emulators.bamf.BamfApplication` or |
3471 | - :class:`~autopilot.emulators.bamf.BamfWindow` instance to the launched |
3472 | + :class:`~autopilot.process.Application` or |
3473 | + :class:`~autopilot.process.Window` instance to the launched |
3474 | process respectively. All applications launched in this way will be closed |
3475 | when the test ends. |
3476 | |
3477 | - **Set Unity & Compiz Options** |
3478 | - |
3479 | - The :meth:`~autopilot.testcase.AutopilotTestCase.set_unity_option` and |
3480 | - :meth:`~autopilot.testcase.AutopilotTestCase.set_compiz_option` methods set a |
3481 | - unity or compiz setting to a particular value for the duration of the |
3482 | - current test only. This is useful if you want the window manager to behave |
3483 | - in a particular fashion for a particular test, while being assured that any |
3484 | - chances are non-destructive. |
3485 | |
3486 | **Patch Process Environment** |
3487 | |
3488 | @@ -213,269 +99,40 @@ |
3489 | |
3490 | """ |
3491 | |
3492 | - run_tests_with = AutopilotTestRunner |
3493 | - |
3494 | - KNOWN_APPS = { |
3495 | - 'Character Map' : { |
3496 | - 'desktop-file': 'gucharmap.desktop', |
3497 | - 'process-name': 'gucharmap', |
3498 | - }, |
3499 | - 'Calculator' : { |
3500 | - 'desktop-file': 'gcalctool.desktop', |
3501 | - 'process-name': 'gnome-calculator', |
3502 | - }, |
3503 | - 'Mahjongg' : { |
3504 | - 'desktop-file': 'mahjongg.desktop', |
3505 | - 'process-name': 'gnome-mahjongg', |
3506 | - }, |
3507 | - 'Remmina' : { |
3508 | - 'desktop-file': 'remmina.desktop', |
3509 | - 'process-name': 'remmina', |
3510 | - }, |
3511 | - 'System Settings' : { |
3512 | - 'desktop-file': 'gnome-control-center.desktop', |
3513 | - 'process-name': 'gnome-control-center', |
3514 | - }, |
3515 | - 'Text Editor' : { |
3516 | - 'desktop-file': 'gedit.desktop', |
3517 | - 'process-name': 'gedit', |
3518 | - }, |
3519 | - 'Terminal' : { |
3520 | - 'desktop-file': 'gnome-terminal.desktop', |
3521 | - 'process-name': 'gnome-terminal', |
3522 | - }, |
3523 | - } |
3524 | - |
3525 | - |
3526 | def setUp(self): |
3527 | super(AutopilotTestCase, self).setUp() |
3528 | - |
3529 | - self._process_manager = ProcessManager() |
3530 | - self._process_manager.snapshot_running_apps() |
3531 | - self.addCleanup(self._process_manager.compare_system_with_snapshot) |
3532 | - |
3533 | - self.bamf = Bamf() |
3534 | - self.keyboard = get_keyboard() |
3535 | - self.mouse = get_mouse() |
3536 | - self.zeitgeist = Zeitgeist() |
3537 | - |
3538 | - self.screen_geo = ScreenGeometry() |
3539 | + on_test_started(self) |
3540 | + |
3541 | + self.process_manager = ProcessManager.create() |
3542 | + self._app_snapshot = self.process_manager.get_running_applications() |
3543 | + self.addCleanup(self._compare_system_with_app_snapshot) |
3544 | + |
3545 | + self.keyboard = Keyboard.create() |
3546 | + self.mouse = Mouse.create() |
3547 | + |
3548 | + self.screen_geo = Display.create() |
3549 | self.addCleanup(self.keyboard.cleanup) |
3550 | self.addCleanup(self.mouse.cleanup) |
3551 | |
3552 | - def call_gsettings_cmd(self, command, schema, *args): |
3553 | - """Set a desktop wide gsettings option |
3554 | - |
3555 | - Using the gsettings command because there is a bug with importing |
3556 | - from gobject introspection and pygtk2 simultaneously, and the Xlib |
3557 | - keyboard layout bits are very unwieldy. This seems like the best |
3558 | - solution, even a little bit brutish. |
3559 | - """ |
3560 | - cmd = ['gsettings', command, schema] + list(args) |
3561 | - # strip to remove the trailing \n. |
3562 | - ret = check_output(cmd).strip() |
3563 | - time.sleep(5) |
3564 | - reset_display() |
3565 | - return ret |
3566 | - |
3567 | - def set_unity_option(self, option_name, option_value): |
3568 | - """Set an option in the unity compiz plugin options. |
3569 | - |
3570 | - .. note:: The value will be set for the current test only, and |
3571 | - automatically undone when the test ends. |
3572 | - |
3573 | - :param option_name: The name of the unity option. |
3574 | - :param option_value: The value you want to set. |
3575 | - :raises: **KeyError** if the option named does not exist. |
3576 | - |
3577 | - """ |
3578 | - self.set_compiz_option("unityshell", option_name, option_value) |
3579 | - |
3580 | - def set_compiz_option(self, plugin_name, option_name, option_value): |
3581 | - """Set a compiz option for the duration of this test only. |
3582 | - |
3583 | - .. note:: The value will be set for the current test only, and |
3584 | - automatically undone when the test ends. |
3585 | - |
3586 | - :param plugin_name: The name of the compiz plugin where the option is |
3587 | - registered. If the option is not in a plugin, the string "core" should |
3588 | - be used as the plugin name. |
3589 | - :param option_name: The name of the unity option. |
3590 | - :param option_value: The value you want to set. |
3591 | - :raises: **KeyError** if the option named does not exist. |
3592 | - |
3593 | - """ |
3594 | - old_value = self._set_compiz_option(plugin_name, option_name, option_value) |
3595 | - # Cleanup is LIFO, during clean-up also allow unity to respond |
3596 | - self.addCleanup(time.sleep, 0.5) |
3597 | - self.addCleanup(self._set_compiz_option, plugin_name, option_name, old_value) |
3598 | - # Allow unity time to respond to the new setting. |
3599 | - time.sleep(0.5) |
3600 | - |
3601 | - def _set_compiz_option(self, plugin_name, option_name, option_value): |
3602 | - logger.info("Setting compiz option '%s' in plugin '%s' to %r", |
3603 | - option_name, plugin_name, option_value) |
3604 | - setting = get_compiz_setting(plugin_name, option_name) |
3605 | - old_value = setting.Value |
3606 | - setting.Value = option_value |
3607 | - get_global_context().Write() |
3608 | - return old_value |
3609 | - |
3610 | - @classmethod |
3611 | - def register_known_application(cls, name, desktop_file, process_name): |
3612 | - """Register an application with autopilot. |
3613 | - |
3614 | - After calling this method, you may call :meth:`start_app` or |
3615 | - :meth:`start_app_window` with the `name` parameter to start this |
3616 | - application. |
3617 | - You need only call this once within a test run - the application will |
3618 | - remain registerred until the test run ends. |
3619 | - |
3620 | - :param name: The name to be used when launching the application. |
3621 | - :param desktop_file: The filename (without path component) of the desktop file used to launch the application. |
3622 | - :param process_name: The name of the executable process that gets run. |
3623 | - :raises: **KeyError** if application has been registered already |
3624 | - |
3625 | - """ |
3626 | - if name in cls.KNOWN_APPS: |
3627 | - raise KeyError("Application has been registered already") |
3628 | - else: |
3629 | - cls.KNOWN_APPS[name] = { |
3630 | - "desktop-file" : desktop_file, |
3631 | - "process-name" : process_name |
3632 | - } |
3633 | - |
3634 | - @classmethod |
3635 | - def unregister_known_application(cls, name): |
3636 | - """Unregister an application with the known_apps dictionary. |
3637 | - |
3638 | - :param name: The name to be used when launching the application. |
3639 | - :raises: **KeyError** if the application has not been registered. |
3640 | - |
3641 | - """ |
3642 | - if name in cls.KNOWN_APPS: |
3643 | - del cls.KNOWN_APPS[name] |
3644 | - else: |
3645 | - raise KeyError("Application has not been registered") |
3646 | - |
3647 | - def start_app(self, app_name, files=[], locale=None): |
3648 | - """Start one of the known applications, and kill it on tear down. |
3649 | - |
3650 | - .. warning:: This method will clear all instances of this application on |
3651 | - tearDown, not just the one opened by this method! We recommend that |
3652 | - you use the :meth:`start_app_window` method instead, as it is generally |
3653 | - safer. |
3654 | - |
3655 | - :param app_name: The application name. *This name must either already |
3656 | - be registered as one of the built-in applications that are supported |
3657 | - by autopilot, or must have been registered using* |
3658 | - :meth:`register_known_application` *beforehand.* |
3659 | - :param files: (Optional) A list of paths to open with the |
3660 | - given application. *Not all applications support opening files in this |
3661 | - way.* |
3662 | - :param locale: (Optional) The locale will to set when the application |
3663 | - is launched. *If you want to launch an application without any |
3664 | - localisation being applied, set this parameter to 'C'.* |
3665 | - :returns: A :class:`~autopilot.emulators.bamf.BamfApplication` instance. |
3666 | - |
3667 | - """ |
3668 | - window = self._open_window(app_name, files, locale) |
3669 | - if window: |
3670 | - self.addCleanup(self.close_all_app, app_name) |
3671 | - return window.application |
3672 | - |
3673 | - raise AssertionError("No new application window was opened.") |
3674 | - |
3675 | - def start_app_window(self, app_name, files=[], locale=None): |
3676 | - """Open a single window for one of the known applications, and close it |
3677 | - at the end of the test. |
3678 | - |
3679 | - :param app_name: The application name. *This name must either already |
3680 | - be registered as one of the built-in applications that are supported |
3681 | - by autopilot, or must have been registered with* |
3682 | - :meth:`register_known_application` *beforehand.* |
3683 | - :param files: (Optional) Should be a list of paths to open with the |
3684 | - given application. *Not all applications support opening files in this |
3685 | - way.* |
3686 | - :param locale: (Optional) The locale will to set when the application |
3687 | - is launched. *If you want to launch an application without any |
3688 | - localisation being applied, set this parameter to 'C'.* |
3689 | - :raises: **AssertionError** if no window was opened, or more than one |
3690 | - window was opened. |
3691 | - :returns: A :class:`~autopilot.emulators.bamf.BamfWindow` instance. |
3692 | - |
3693 | - """ |
3694 | - window = self._open_window(app_name, files, locale) |
3695 | - if window: |
3696 | - self.addCleanup(window.close) |
3697 | - return window |
3698 | - raise AssertionError("No window was opened.") |
3699 | - |
3700 | - def _open_window(self, app_name, files, locale): |
3701 | - """Open a new 'app_name' window, returning the window instance or None. |
3702 | - |
3703 | - Raises an AssertionError if this creates more than one window. |
3704 | - |
3705 | - """ |
3706 | - existing_windows = self.get_open_windows_by_application(app_name) |
3707 | - |
3708 | - if locale: |
3709 | - os.putenv("LC_ALL", locale) |
3710 | - self.addCleanup(os.unsetenv, "LC_ALL") |
3711 | - logger.info("Starting application '%s' with files %r in locale %s", app_name, files, locale) |
3712 | - else: |
3713 | - logger.info("Starting application '%s' with files %r", app_name, files) |
3714 | - |
3715 | - |
3716 | - app = self.KNOWN_APPS[app_name] |
3717 | - self.bamf.launch_application(app['desktop-file'], files) |
3718 | - apps = self.bamf.get_running_applications_by_desktop_file(app['desktop-file']) |
3719 | - |
3720 | + def _compare_system_with_app_snapshot(self): |
3721 | + """Compare the currently running application with the last snapshot. |
3722 | + |
3723 | + This method will raise an AssertionError if there are any new applications |
3724 | + currently running that were not running when the snapshot was taken. |
3725 | + """ |
3726 | + if self._app_snapshot is None: |
3727 | + raise RuntimeError("No snapshot to match against.") |
3728 | + |
3729 | + new_apps = [] |
3730 | for i in range(10): |
3731 | - try: |
3732 | - new_windows = [] |
3733 | - [new_windows.extend(a.get_windows()) for a in apps] |
3734 | - filter_fn = lambda w: w.x_id not in [c.x_id for c in existing_windows] |
3735 | - new_wins = filter(filter_fn, new_windows) |
3736 | - if new_wins: |
3737 | - assert len(new_wins) == 1 |
3738 | - return new_wins[0] |
3739 | - except DBusException: |
3740 | - pass |
3741 | - time.sleep(1) |
3742 | - return None |
3743 | - |
3744 | - def get_open_windows_by_application(self, app_name): |
3745 | - """Get a list of BamfWindow instances for the given application name. |
3746 | - |
3747 | - :param app_name: The name of one of the well-known applications. |
3748 | - :returns: A list of :class:`~autopilot.emulators.bamf.BamfWindow` |
3749 | - instances. |
3750 | - |
3751 | - """ |
3752 | - existing_windows = [] |
3753 | - [existing_windows.extend(a.get_windows()) for a in self.get_app_instances(app_name)] |
3754 | - return existing_windows |
3755 | - |
3756 | - def close_all_app(self, app_name): |
3757 | - """Close all instances of the application 'app_name'.""" |
3758 | - app = self.KNOWN_APPS[app_name] |
3759 | - try: |
3760 | - pids = check_output(["pidof", app['process-name']]).split() |
3761 | - if len(pids): |
3762 | - call(["kill"] + pids) |
3763 | - except CalledProcessError: |
3764 | - logger.warning("Tried to close applicaton '%s' but it wasn't running.", app_name) |
3765 | - |
3766 | - def get_app_instances(self, app_name): |
3767 | - """Get BamfApplication instances for app_name.""" |
3768 | - desktop_file = self.KNOWN_APPS[app_name]['desktop-file'] |
3769 | - return self.bamf.get_running_applications_by_desktop_file(desktop_file) |
3770 | - |
3771 | - def app_is_running(self, app_name): |
3772 | - """Return true if an instance of the application is running.""" |
3773 | - apps = self.get_app_instances(app_name) |
3774 | - return len(apps) > 0 |
3775 | + current_apps = self.process_manager.get_running_applications() |
3776 | + new_apps = filter(lambda i: i not in self._app_snapshot, current_apps) |
3777 | + if not new_apps: |
3778 | + self._app_snapshot = None |
3779 | + return |
3780 | + sleep(1) |
3781 | + self._app_snapshot = None |
3782 | + raise AssertionError("The following apps were started during the test and not closed: %r", new_apps) |
3783 | |
3784 | def patch_environment(self, key, value): |
3785 | """Patch the process environment, setting *key* with value *value*. |
3786 | @@ -513,12 +170,13 @@ |
3787 | |
3788 | .. note:: Minimised windows are skipped. |
3789 | |
3790 | - :param stack_start: An iterable of BamfWindow instances. |
3791 | + :param stack_start: An iterable of |
3792 | + `~autopilot.process.Window` instances. |
3793 | :raises: **AssertionError** if the top of the window stack does not |
3794 | match the contents of the stack_start parameter. |
3795 | |
3796 | """ |
3797 | - stack = [win for win in self.bamf.get_open_windows() if not win.is_hidden] |
3798 | + stack = [win for win in self.process_manager.get_open_windows() if not win.is_hidden] |
3799 | for pos, win in enumerate(stack_start): |
3800 | self.assertThat(stack[pos].x_id, Equals(win.x_id), |
3801 | "%r at %d does not equal %r" % (stack[pos], pos, win)) |
3802 | @@ -531,7 +189,7 @@ |
3803 | the autopilot DBus interface). |
3804 | |
3805 | For example, from within a test, to assert certain properties on a |
3806 | - BamfWindow instance:: |
3807 | + `~autopilot.process.Window` instance:: |
3808 | |
3809 | self.assertProperty(my_window, is_maximized=True) |
3810 | |
3811 | @@ -561,3 +219,83 @@ |
3812 | self.assertThat(lambda: getattr(obj, prop_name), Eventually(Equals(desired_value))) |
3813 | |
3814 | assertProperties = assertProperty |
3815 | + |
3816 | + def launch_test_application(self, application, *arguments, **kwargs): |
3817 | + """Launch ``application`` and return a proxy object for the application. |
3818 | + |
3819 | + Use this method to launch an application and start testing it. The |
3820 | + positional arguments are used as arguments to the application to lanch. |
3821 | + Keyword arguments are used to control the manner in which the application |
3822 | + is launched. |
3823 | + |
3824 | + This method is designed to be flexible enough to launch all supported |
3825 | + types of applications. For example, to launch a traditional Gtk application, |
3826 | + a test might start with:: |
3827 | + |
3828 | + app_proxy = self.launch_test_application('gedit') |
3829 | + |
3830 | + ... a Qt4 Qml application might be launched like this:: |
3831 | + |
3832 | + app_proxy = self.launch_test_application('qmlviewer', 'my_scene.qml') |
3833 | + |
3834 | + ... a Qt5 Qml application is launched in a similar fashion:: |
3835 | + |
3836 | + app_proxy = self.launch_test_application('qmlscene', 'my_scene.qml') |
3837 | + |
3838 | + :param application: The application to launch. The application can be |
3839 | + specified as: |
3840 | + |
3841 | + * A full, absolute path to an executable file. (``/usr/bin/gedit``) |
3842 | + * A relative path to an executable file. (``./build/my_app``) |
3843 | + * An app name, which will be searched for in $PATH (``my_app``) |
3844 | + |
3845 | + :keyword launch_dir: If set to a directory that exists the process will be |
3846 | + launched from that directory. |
3847 | + |
3848 | + :keyword capture_output: If set to True (the default), the process output |
3849 | + will be captured and attached to the test as test detail. |
3850 | + |
3851 | + :raises: **ValueError** if unknown keyword arguments are passed. |
3852 | + :return: A proxy object that represents the application. Introspection |
3853 | + data is retrievable via this object. |
3854 | + |
3855 | + """ |
3856 | + # first, we get a launcher. Tests can override this if they need: |
3857 | + launcher = self.pick_app_launcher(application) |
3858 | + if launcher is None: |
3859 | + raise RuntimeError("Autopilot could not determine the correct \ |
3860 | + introspection type to use. You can specify one by overriding \ |
3861 | + the AutopilotTestCase.pick_app_launcher method.") |
3862 | + process = launch_application(launcher, application, *arguments, **kwargs) |
3863 | + self.addCleanup(self._kill_process_and_attach_logs, process) |
3864 | + return get_autopilot_proxy_object_for_process(process) |
3865 | + |
3866 | + def pick_app_launcher(self, app_path): |
3867 | + """Given an application path, return an object suitable for launching |
3868 | + the application. |
3869 | + |
3870 | + This function attempts to guess what kind of application you are |
3871 | + launching. If, for some reason the default implementation returns the |
3872 | + wrong launcher, test authors may override this method to provide their |
3873 | + own implemetnation. |
3874 | + |
3875 | + The default implementation calls |
3876 | + :py:func:`autopilot.introspection.get_application_launcher` |
3877 | + |
3878 | + """ |
3879 | + # default implementation is in autopilot.introspection: |
3880 | + return get_application_launcher(app_path) |
3881 | + |
3882 | + def _kill_process_and_attach_logs(self, process): |
3883 | + process.kill() |
3884 | + logger.info("waiting for process to exit.") |
3885 | + for i in range(10): |
3886 | + if process.returncode is not None: |
3887 | + break |
3888 | + if i == 9: |
3889 | + logger.info("Terminating process group, since it hasn't exited after 10 seconds.") |
3890 | + os.killpg(process.pid, signal.SIGTERM) |
3891 | + sleep(1) |
3892 | + stdout, stderr = process.communicate() |
3893 | + self.addDetail('process-stdout', text_content(stdout)) |
3894 | + self.addDetail('process-stderr', text_content(stderr)) |
3895 | |
3896 | === modified file 'autopilot/tests/test_ap_apps.py' |
3897 | --- autopilot/tests/test_ap_apps.py 2013-02-22 03:59:51 +0000 |
3898 | +++ autopilot/tests/test_ap_apps.py 2013-04-15 21:43:32 +0000 |
3899 | @@ -13,8 +13,7 @@ |
3900 | from textwrap import dedent |
3901 | |
3902 | from autopilot.testcase import AutopilotTestCase |
3903 | -from autopilot.introspection.gtk import GtkIntrospectionTestMixin |
3904 | -from autopilot.introspection.qt import QtIntrospectionTestMixin |
3905 | +from autopilot.introspection.gtk import GtkApplicationLauncher |
3906 | |
3907 | |
3908 | class ApplicationTests(AutopilotTestCase): |
3909 | @@ -33,15 +32,20 @@ |
3910 | return path |
3911 | |
3912 | |
3913 | -class QtTests(ApplicationTests, QtIntrospectionTestMixin): |
3914 | +class QtTests(ApplicationTests): |
3915 | |
3916 | def setUp(self): |
3917 | super(QtTests, self).setUp() |
3918 | |
3919 | try: |
3920 | - self.app_path = subprocess.check_output(['which','qmlviewer']).strip() |
3921 | + self.app_path = subprocess.check_output(['which','qmlscene']).strip() |
3922 | except subprocess.CalledProcessError: |
3923 | - self.skip("qmlviewer not found.") |
3924 | + self.skip("qmlscene not found.") |
3925 | + |
3926 | + def pick_app_launcher(self, app_path): |
3927 | + # force Qt app introspection: |
3928 | + from autopilot.introspection.qt import QtApplicationLauncher |
3929 | + return QtApplicationLauncher() |
3930 | |
3931 | def test_can_launch_qt_app(self): |
3932 | app_proxy = self.launch_test_application(self.app_path) |
3933 | @@ -83,7 +87,7 @@ |
3934 | self.assertTrue(app_proxy is not None) |
3935 | |
3936 | |
3937 | -class GtkTests(ApplicationTests, GtkIntrospectionTestMixin): |
3938 | +class GtkTests(ApplicationTests): |
3939 | |
3940 | def setUp(self): |
3941 | super(GtkTests, self).setUp() |
3942 | |
3943 | === modified file 'autopilot/tests/test_application_mixin.py' |
3944 | --- autopilot/tests/test_application_mixin.py 2012-09-13 02:28:04 +0000 |
3945 | +++ autopilot/tests/test_application_mixin.py 2013-04-15 21:43:32 +0000 |
3946 | @@ -8,45 +8,33 @@ |
3947 | |
3948 | from __future__ import absolute_import |
3949 | |
3950 | -from testtools import TestCase |
3951 | -from testtools.matchers import Equals, Is, Not, raises |
3952 | -from mock import patch |
3953 | - |
3954 | -from autopilot.introspection.qt import QtIntrospectionTestMixin |
3955 | - |
3956 | +from autopilot.testcase import AutopilotTestCase |
3957 | +from testtools.matchers import Is, Not, raises |
3958 | |
3959 | def dummy_addCleanup(*args, **kwargs): |
3960 | pass |
3961 | |
3962 | |
3963 | -class ApplicationSupportTests(TestCase): |
3964 | - |
3965 | - def test_can_create(self): |
3966 | - mixin = QtIntrospectionTestMixin() |
3967 | - self.assertThat(mixin, Not(Is(None))) |
3968 | +class ApplicationSupportTests(AutopilotTestCase): |
3969 | |
3970 | def test_launch_with_bad_types_raises_typeerror(self): |
3971 | """Calling launch_test_application with something other than a string must |
3972 | raise a TypeError""" |
3973 | |
3974 | - mixin = QtIntrospectionTestMixin() |
3975 | - mixin.addCleanup = dummy_addCleanup |
3976 | - |
3977 | - self.assertThat(lambda: mixin.launch_test_application(1), raises(TypeError)) |
3978 | - self.assertThat(lambda: mixin.launch_test_application(True), raises(TypeError)) |
3979 | - self.assertThat(lambda: mixin.launch_test_application(1.0), raises(TypeError)) |
3980 | - self.assertThat(lambda: mixin.launch_test_application(object()), raises(TypeError)) |
3981 | - self.assertThat(lambda: mixin.launch_test_application(None), raises(TypeError)) |
3982 | - self.assertThat(lambda: mixin.launch_test_application([]), raises(TypeError)) |
3983 | - self.assertThat(lambda: mixin.launch_test_application((None,)), raises(TypeError)) |
3984 | + self.assertThat(lambda: self.launch_test_application(1), raises(TypeError)) |
3985 | + self.assertThat(lambda: self.launch_test_application(True), raises(TypeError)) |
3986 | + self.assertThat(lambda: self.launch_test_application(1.0), raises(TypeError)) |
3987 | + self.assertThat(lambda: self.launch_test_application(object()), raises(TypeError)) |
3988 | + self.assertThat(lambda: self.launch_test_application(None), raises(TypeError)) |
3989 | + self.assertThat(lambda: self.launch_test_application([]), raises(TypeError)) |
3990 | + self.assertThat(lambda: self.launch_test_application((None,)), raises(TypeError)) |
3991 | |
3992 | def test_launch_raises_ValueError_on_unknown_kwargs(self): |
3993 | """launch_test_application must raise ValueError when given unknown |
3994 | keyword arguments. |
3995 | |
3996 | """ |
3997 | - mixin = QtIntrospectionTestMixin() |
3998 | - fn = lambda: mixin.launch_test_application('gedit', arg1=123, arg2='asd') |
3999 | + fn = lambda: self.launch_test_application('gedit', arg1=123, arg2='asd') |
4000 | self.assertThat(fn, raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'."))) |
4001 | |
4002 | def test_launch_raises_ValueError_on_unknown_kwargs_with_known(self): |
4003 | @@ -54,6 +42,5 @@ |
4004 | keyword arguments. |
4005 | |
4006 | """ |
4007 | - mixin = QtIntrospectionTestMixin() |
4008 | - fn = lambda: mixin.launch_test_application('gedit', arg1=123, arg2='asd', launch_dir='/') |
4009 | + fn = lambda: self.launch_test_application('gedit', arg1=123, arg2='asd', launch_dir='/') |
4010 | self.assertThat(fn, raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'."))) |
4011 | |
4012 | === removed file 'autopilot/tests/test_application_registration.py' |
4013 | --- autopilot/tests/test_application_registration.py 2012-09-30 22:39:30 +0000 |
4014 | +++ autopilot/tests/test_application_registration.py 1970-01-01 00:00:00 +0000 |
4015 | @@ -1,94 +0,0 @@ |
4016 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
4017 | -# Copyright 2012 Canonical |
4018 | -# Author: Thomi Richards |
4019 | -# |
4020 | -# This program is free software: you can redistribute it and/or modify it |
4021 | -# under the terms of the GNU General Public License version 3, as published |
4022 | -# by the Free Software Foundation. |
4023 | - |
4024 | -from __future__ import absolute_import |
4025 | - |
4026 | -from testtools import TestCase |
4027 | -from testtools.matchers import Equals, Is, Not, raises, Contains |
4028 | -from mock import patch |
4029 | - |
4030 | -from autopilot.testcase import AutopilotTestCase |
4031 | - |
4032 | - |
4033 | -def safe_unregister_application(test_case_class, app_name): |
4034 | - if app_name in test_case_class.KNOWN_APPS: |
4035 | - test_case_class.unregister_known_application(app_name) |
4036 | - |
4037 | -class ApplicationRegistrationTests(TestCase): |
4038 | - |
4039 | - def test_can_register_new_application(self): |
4040 | - AutopilotTestCase.register_known_application( |
4041 | - "NewApplicationName", |
4042 | - "newapp.desktop", |
4043 | - "newapp") |
4044 | - self.addCleanup(safe_unregister_application, |
4045 | - AutopilotTestCase, |
4046 | - "NewApplicationName") |
4047 | - |
4048 | - app_details = AutopilotTestCase.KNOWN_APPS['NewApplicationName'] |
4049 | - |
4050 | - self.assertThat(AutopilotTestCase.KNOWN_APPS, Contains("NewApplicationName")) |
4051 | - self.assertTrue(type(app_details) is dict) |
4052 | - self.assertThat(app_details, Contains('desktop-file')) |
4053 | - self.assertThat(app_details, Contains('process-name')) |
4054 | - self.assertThat(app_details['desktop-file'], Equals('newapp.desktop')) |
4055 | - self.assertThat(app_details['process-name'], Equals('newapp')) |
4056 | - |
4057 | - def test_registering_app_twice_raises_KeyError(self): |
4058 | - """Registering an application with the same app name as one that's |
4059 | - already in the dictionary must raise KeyError and not change the |
4060 | - dictionary. |
4061 | - |
4062 | - """ |
4063 | - AutopilotTestCase.register_known_application( |
4064 | - "NewApplicationName", |
4065 | - "newapp.desktop", |
4066 | - "newapp") |
4067 | - self.addCleanup(safe_unregister_application, |
4068 | - AutopilotTestCase, |
4069 | - "NewApplicationName") |
4070 | - |
4071 | - app_details = AutopilotTestCase.KNOWN_APPS['NewApplicationName'] |
4072 | - register_fn = lambda: AutopilotTestCase.register_known_application( |
4073 | - "NewApplicationName", |
4074 | - "newapp2.desktop", |
4075 | - "newapp2") |
4076 | - |
4077 | - self.assertThat(register_fn, raises( |
4078 | - KeyError("Application has been registered already"))) |
4079 | - self.assertThat(AutopilotTestCase.KNOWN_APPS, Contains("NewApplicationName")) |
4080 | - self.assertTrue(type(app_details) is dict) |
4081 | - self.assertThat(app_details, Contains('desktop-file')) |
4082 | - self.assertThat(app_details, Contains('process-name')) |
4083 | - self.assertThat(app_details['desktop-file'], Equals('newapp.desktop')) |
4084 | - self.assertThat(app_details['process-name'], Equals('newapp')) |
4085 | - |
4086 | - def test_can_unregister_application(self): |
4087 | - AutopilotTestCase.register_known_application( |
4088 | - "NewApplicationName", |
4089 | - "newapp.desktop", |
4090 | - "newapp") |
4091 | - self.addCleanup(safe_unregister_application, |
4092 | - AutopilotTestCase, |
4093 | - "NewApplicationName") |
4094 | - |
4095 | - AutopilotTestCase.unregister_known_application("NewApplicationName") |
4096 | - |
4097 | - self.assertThat(AutopilotTestCase.KNOWN_APPS, |
4098 | - Not(Contains("NewApplicationName"))) |
4099 | - |
4100 | - def test_unregistering_unknown_application_raises_KeyError(self): |
4101 | - """Trying to unregister an application that is not already registered |
4102 | - must raise a KeyError. |
4103 | - |
4104 | - """ |
4105 | - |
4106 | - unregister_fn = lambda: AutopilotTestCase.unregister_known_application("FooBarBaz") |
4107 | - |
4108 | - self.assertThat(unregister_fn, raises(KeyError("Application has not been registered"))) |
4109 | - |
4110 | |
4111 | === removed file 'autopilot/tests/test_compiz_key_translate.py' |
4112 | --- autopilot/tests/test_compiz_key_translate.py 2012-05-08 16:14:53 +0000 |
4113 | +++ autopilot/tests/test_compiz_key_translate.py 1970-01-01 00:00:00 +0000 |
4114 | @@ -1,69 +0,0 @@ |
4115 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
4116 | -# Copyright 2012 Canonical |
4117 | -# Author: Thomi Richards |
4118 | -# |
4119 | -# This program is free software: you can redistribute it and/or modify it |
4120 | -# under the terms of the GNU General Public License version 3, as published |
4121 | -# by the Free Software Foundation. |
4122 | - |
4123 | -from __future__ import absolute_import |
4124 | - |
4125 | -from testscenarios import TestWithScenarios |
4126 | -from testtools import TestCase |
4127 | -from testtools.matchers import raises, Equals |
4128 | - |
4129 | -from autopilot.keybindings import _translate_compiz_keystroke_string as translate_func |
4130 | - |
4131 | -class KeyTranslateArgumentTests(TestWithScenarios, TestCase): |
4132 | - """Tests that the compizconfig keycode translation routes work as advertised.""" |
4133 | - |
4134 | - scenarios = [ |
4135 | - ('bool', {'input': True}), |
4136 | - ('int', {'input': 42}), |
4137 | - ('float', {'input': 0.321}), |
4138 | - ('none', {'input': None}), |
4139 | - ] |
4140 | - |
4141 | - def test_requires_string_instance(self): |
4142 | - """Function must raise TypeError unless given an instance of basestring.""" |
4143 | - self.assertThat(lambda: translate_func(self.input), raises(TypeError)) |
4144 | - |
4145 | - |
4146 | -class TranslationTests(TestWithScenarios, TestCase): |
4147 | - """Test that we get the result we expect, with the given input.""" |
4148 | - |
4149 | - scenarios = [ |
4150 | - ('empty string', dict(input='', expected='')), |
4151 | - ('single simpe letter', dict(input='a', expected='a')), |
4152 | - ('trailing space', dict(input='d ', expected='d')), |
4153 | - ('only whitespace', dict(input='\t\n ', expected='')), |
4154 | - ('special key: Ctrl', dict(input='<Control>', expected='Ctrl')), |
4155 | - ('special key: Primary', dict(input='<Primary>', expected='Ctrl')), |
4156 | - ('special key: Alt', dict(input='<Alt>', expected='Alt')), |
4157 | - ('special key: Shift', dict(input='<Shift>', expected='Shift')), |
4158 | - ('direction key up', dict(input='Up', expected='Up')), |
4159 | - ('direction key down', dict(input='Down', expected='Down')), |
4160 | - ('direction key left', dict(input='Left', expected='Left')), |
4161 | - ('direction key right', dict(input='Right', expected='Right')), |
4162 | - ('Ctrl+a', dict(input='<Control>a', expected='Ctrl+a')), |
4163 | - ('Primary+a', dict(input='<Control>a', expected='Ctrl+a')), |
4164 | - ('Shift+s', dict(input='<Shift>s', expected='Shift+s')), |
4165 | - ('Alt+d', dict(input='<Alt>d', expected='Alt+d')), |
4166 | - ('Super+w', dict(input='<Super>w', expected='Super+w')), |
4167 | - ('Ctrl+Up', dict(input='<Control>Up', expected='Ctrl+Up')), |
4168 | - ('Primary+Down', dict(input='<Control>Down', expected='Ctrl+Down')), |
4169 | - ('Alt+Left', dict(input='<Alt>Left', expected='Alt+Left')), |
4170 | - ('Shift+F3', dict(input='<Shift>F3', expected='Shift+F3')), |
4171 | - ('duplicate keys Ctrl+Ctrl', dict(input='<Control><Control>', expected='Ctrl')), |
4172 | - ('duplicate keys Ctrl+Primary', dict(input='<Control><Primary>', expected='Ctrl')), |
4173 | - ('duplicate keys Ctrl+Primary', dict(input='<Primary><Control>', expected='Ctrl')), |
4174 | - ('duplicate keys Alt+Alt', dict(input='<Alt><Alt>', expected='Alt')), |
4175 | - ('duplicate keys Ctrl+Primary+left', dict(input='<Control><Primary>Left', expected='Ctrl+Left')), |
4176 | - ('first key wins', dict(input='<Control><Alt>Down<Alt>', expected='Ctrl+Alt+Down')), |
4177 | - ('Getting silly now', dict(input='<Control><Primary><Shift><Shift><Alt>Left', expected='Ctrl+Shift+Alt+Left')), |
4178 | - ] |
4179 | - |
4180 | - def test_translation(self): |
4181 | - self.assertThat(translate_func(self.input), Equals(self.expected)) |
4182 | - |
4183 | - |
4184 | |
4185 | === removed file 'autopilot/tests/test_compiz_option_support.py' |
4186 | --- autopilot/tests/test_compiz_option_support.py 2012-08-20 03:59:45 +0000 |
4187 | +++ autopilot/tests/test_compiz_option_support.py 1970-01-01 00:00:00 +0000 |
4188 | @@ -1,31 +0,0 @@ |
4189 | -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
4190 | -# Copyright 2012 Canonical |
4191 | -# Author: Thomi Richards |
4192 | -# |
4193 | -# This program is free software: you can redistribute it and/or modify it |
4194 | -# under the terms of the GNU General Public License version 3, as published |
4195 | -# by the Free Software Foundation. |
4196 | - |
4197 | -from __future__ import absolute_import |
4198 | - |
4199 | -from testtools.matchers import Equals, raises, Not |
4200 | - |
4201 | -from autopilot.testcase import AutopilotTestCase |
4202 | -import logging |
4203 | -logger = logging.getLogger(__name__) |
4204 | - |
4205 | - |
4206 | - |
4207 | -class CompizConfigOptionTests(AutopilotTestCase): |
4208 | - |
4209 | - def test_set_option_raises_KeyError_on_bad_plugin_name(self): |
4210 | - """set_compiz_option must raise KeyError when given a bad plugin name.""" |
4211 | - fn = lambda: self.set_compiz_option('rubbishpluginname', 'rubbishsettingname', 'settingvalue') |
4212 | - self.assertThat(fn, raises(KeyError("Compiz plugin 'rubbishpluginname' does not exist."))) |
4213 | - |
4214 | - def test_set_option_raises_KeyError_on_bad_setting_name(self): |
4215 | - """set_compiz_option must raise KeyError when called with a bad setting name.""" |
4216 | - fn = lambda: self.set_compiz_option('core', 'rubbishsettingname', 'settingvalue') |
4217 | - self.assertThat(fn, raises(KeyError("Compiz setting 'rubbishsettingname' does not exist in plugin 'core'."))) |
4218 | - |
4219 | - |
4220 | |
4221 | === modified file 'autopilot/tests/test_keyboard.py' |
4222 | --- autopilot/tests/test_keyboard.py 2012-12-04 01:37:07 +0000 |
4223 | +++ autopilot/tests/test_keyboard.py 2013-04-15 21:43:32 +0000 |
4224 | @@ -30,7 +30,7 @@ |
4225 | |
4226 | def test_keyboard_types_correct_characters(self): |
4227 | """Verify that the keyboard.type method types what we expect.""" |
4228 | - self.start_app_window('Terminal') |
4229 | + self.process_manager.start_app_window('Terminal') |
4230 | filename = mktemp() |
4231 | self.keyboard.type('''python -c "open('%s','w').write(raw_input())"''' % filename) |
4232 | self.keyboard.press_and_release('Enter') |
4233 | @@ -46,7 +46,7 @@ |
4234 | expect. |
4235 | |
4236 | """ |
4237 | - self.start_app_window('Terminal') |
4238 | + self.process_manager.start_app_window('Terminal') |
4239 | filename = mktemp() |
4240 | self.keyboard.type('''python -c "open('%s','w').write(raw_input())"''' % filename) |
4241 | self.keyboard.press_and_release('Enter') |
4242 | |
4243 | === modified file 'autopilot/tests/test_mouse_emulator.py' |
4244 | --- autopilot/tests/test_mouse_emulator.py 2013-02-28 04:16:14 +0000 |
4245 | +++ autopilot/tests/test_mouse_emulator.py 2013-04-15 21:43:32 +0000 |
4246 | @@ -10,7 +10,7 @@ |
4247 | from testtools.matchers import Equals, raises |
4248 | from mock import patch |
4249 | |
4250 | -from autopilot.emulators.input import get_mouse |
4251 | +from autopilot.input import Mouse |
4252 | |
4253 | class Empty(object): |
4254 | pass |
4255 | @@ -31,7 +31,7 @@ |
4256 | |
4257 | def setUp(self): |
4258 | super(MouseEmulatorTests, self).setUp() |
4259 | - self.mouse = get_mouse() |
4260 | + self.mouse = Mouse.create() |
4261 | |
4262 | def tearDown(self): |
4263 | super(MouseEmulatorTests, self).tearDown() |
4264 | |
4265 | === modified file 'autopilot/tests/test_open_window.py' |
4266 | --- autopilot/tests/test_open_window.py 2012-07-08 23:29:30 +0000 |
4267 | +++ autopilot/tests/test_open_window.py 2013-04-15 21:43:32 +0000 |
4268 | @@ -11,23 +11,24 @@ |
4269 | from testtools.matchers import Equals |
4270 | |
4271 | from autopilot.testcase import AutopilotTestCase |
4272 | +from autopilot.process import ProcessManager |
4273 | import logging |
4274 | logger = logging.getLogger(__name__) |
4275 | |
4276 | |
4277 | class OpenWindowTests(AutopilotTestCase): |
4278 | |
4279 | - scenarios = [(k, {'app_name': k}) for k in AutopilotTestCase.KNOWN_APPS.iterkeys()] |
4280 | + scenarios = [(k, {'app_name': k}) for k in ProcessManager.KNOWN_APPS.iterkeys()] |
4281 | |
4282 | def test_open_window(self): |
4283 | """self.start_app_window must open a new window of the given app.""" |
4284 | - existing_apps = self.get_app_instances(self.app_name) |
4285 | + existing_apps = self.process_manager.get_app_instances(self.app_name) |
4286 | old_wins = [] |
4287 | for app in existing_apps: |
4288 | old_wins.extend(app.get_windows()) |
4289 | logger.debug("Old windows: %r", old_wins) |
4290 | |
4291 | - win = self.start_app_window(self.app_name) |
4292 | + win = self.process_manager.start_app_window(self.app_name) |
4293 | logger.debug("New window: %r", win) |
4294 | is_new = win.x_id not in [w.x_id for w in old_wins] |
4295 | self.assertThat(is_new, Equals(True)) |
4296 | |
4297 | === added file 'autopilot/tests/test_out_of_test_addcleanup.py' |
4298 | --- autopilot/tests/test_out_of_test_addcleanup.py 1970-01-01 00:00:00 +0000 |
4299 | +++ autopilot/tests/test_out_of_test_addcleanup.py 2013-04-15 21:43:32 +0000 |
4300 | @@ -0,0 +1,34 @@ |
4301 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
4302 | +# Copyright 2013 Canonical |
4303 | +# Author: Thomi Richards |
4304 | +# |
4305 | +# This program is free software: you can redistribute it and/or modify it |
4306 | +# under the terms of the GNU General Public License version 3, as published |
4307 | +# by the Free Software Foundation. |
4308 | + |
4309 | +from testtools import TestCase |
4310 | +from testtools.matchers import Equals |
4311 | + |
4312 | +from autopilot.testcase import AutopilotTestCase |
4313 | +from autopilot.utilities import addCleanup |
4314 | + |
4315 | +log = '' |
4316 | + |
4317 | +class AddCleanupTests(TestCase): |
4318 | + |
4319 | + def test_addCleanup_called_with_args_and_kwargs(self): |
4320 | + """Test that out-of-test addClenaup works as expected, and is passed both |
4321 | + args and kwargs. |
4322 | + |
4323 | + """ |
4324 | + class InnerTest(AutopilotTestCase): |
4325 | + def write_to_log(self, *args, **kwargs): |
4326 | + global log |
4327 | + log = "Hello %r %r" % (args, kwargs) |
4328 | + |
4329 | + def test_foo(self): |
4330 | + addCleanup(self.write_to_log, "arg1", 2, foo='bar') |
4331 | + |
4332 | + InnerTest('test_foo').run() |
4333 | + self.assertThat(log, Equals("Hello ('arg1', 2) {'foo': 'bar'}")) |
4334 | + |
4335 | |
4336 | === added file 'autopilot/tests/test_platform.py' |
4337 | --- autopilot/tests/test_platform.py 1970-01-01 00:00:00 +0000 |
4338 | +++ autopilot/tests/test_platform.py 2013-04-15 21:43:32 +0000 |
4339 | @@ -0,0 +1,134 @@ |
4340 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
4341 | +# Copyright 2013 Canonical |
4342 | +# Author: Thomi Richards |
4343 | +# |
4344 | +# This program is free software: you can redistribute it and/or modify it |
4345 | +# under the terms of the GNU General Public License version 3, as published |
4346 | +# by the Free Software Foundation. |
4347 | + |
4348 | +"Tests for the autopilot platform code." |
4349 | + |
4350 | +import autopilot.platform as platform |
4351 | + |
4352 | +from StringIO import StringIO |
4353 | +from testtools import TestCase |
4354 | +from testtools.matchers import Equals |
4355 | + |
4356 | +from mock import patch |
4357 | + |
4358 | +class PlatformDetectorTests(TestCase): |
4359 | + |
4360 | + def tearDown(self): |
4361 | + super(PlatformDetectorTests, self).tearDown() |
4362 | + # platform detector is cached, so destroy the cache at the end of each |
4363 | + # test: |
4364 | + platform._PlatformDetector._cached_detector = None |
4365 | + |
4366 | + def test_platform_detector_is_cached(self): |
4367 | + """Test that the platform detector is only created once.""" |
4368 | + detector1 = platform._PlatformDetector.create() |
4369 | + detector2 = platform._PlatformDetector.create() |
4370 | + self.assertThat(id(detector1), Equals(id(detector2))) |
4371 | + |
4372 | + @patch('autopilot.platform._get_property_file') |
4373 | + def test_default_model(self, mock_get_property_file): |
4374 | + """The default model name must be 'Desktop'.""" |
4375 | + mock_get_property_file.return_value = None |
4376 | + |
4377 | + detector = platform._PlatformDetector.create() |
4378 | + self.assertThat(detector.model, Equals('Desktop')) |
4379 | + |
4380 | + @patch('autopilot.platform._get_property_file') |
4381 | + def test_default_image_codename(self, mock_get_property_file): |
4382 | + """The default image codename must be 'Desktop'.""" |
4383 | + mock_get_property_file.return_value = None |
4384 | + |
4385 | + detector = platform._PlatformDetector.create() |
4386 | + self.assertThat(detector.image_codename, Equals('Desktop')) |
4387 | + |
4388 | + @patch('autopilot.platform._get_property_file') |
4389 | + def test_model_is_set_from_property_file(self, mock_get_property_file): |
4390 | + """Detector must read product model from android properties file.""" |
4391 | + mock_get_property_file.return_value = StringIO("ro.product.model=test123") |
4392 | + |
4393 | + detector = platform._PlatformDetector.create() |
4394 | + self.assertThat(detector.model, Equals('test123')) |
4395 | + |
4396 | + @patch('autopilot.platform._get_property_file', new=lambda: StringIO("")) |
4397 | + def test_model_has_default_when_not_in_property_file(self): |
4398 | + """Detector must use 'Desktop' as a default value for the model name |
4399 | + when the property file exists, but does not contain a model description. |
4400 | + |
4401 | + """ |
4402 | + detector = platform._PlatformDetector.create() |
4403 | + self.assertThat(detector.model, Equals('Desktop')) |
4404 | + |
4405 | + @patch('autopilot.platform._get_property_file') |
4406 | + def test_product_codename_is_set_from_property_file(self, mock_get_property_file): |
4407 | + """Detector must read product model from android properties file.""" |
4408 | + mock_get_property_file.return_value = StringIO("ro.product.name=test123") |
4409 | + |
4410 | + detector = platform._PlatformDetector.create() |
4411 | + self.assertThat(detector.image_codename, Equals('test123')) |
4412 | + |
4413 | + @patch('autopilot.platform._get_property_file', new=lambda: StringIO("")) |
4414 | + def test_product_codename_has_default_when_not_in_property_file(self): |
4415 | + """Detector must use 'Desktop' as a default value for the product codename |
4416 | + when the property file exists, but does not contain a model description. |
4417 | + |
4418 | + """ |
4419 | + detector = platform._PlatformDetector.create() |
4420 | + self.assertThat(detector.image_codename, Equals('Desktop')) |
4421 | + |
4422 | + |
4423 | +class BuildPropertyParserTests(TestCase): |
4424 | + |
4425 | + """Tests for the android build properties file parser.""" |
4426 | + |
4427 | + def test_empty_file_returns_empty_dictionary(self): |
4428 | + """An empty file must result in an empty dictionary.""" |
4429 | + prop_file = StringIO("") |
4430 | + properties = platform._parse_build_properties_file(prop_file) |
4431 | + self.assertThat(len(properties), Equals(0)) |
4432 | + |
4433 | + def test_whitespace_is_ignored(self): |
4434 | + """Whitespace in build file must be ignored.""" |
4435 | + prop_file = StringIO("\n\n\n\n\n") |
4436 | + properties = platform._parse_build_properties_file(prop_file) |
4437 | + self.assertThat(len(properties), Equals(0)) |
4438 | + |
4439 | + def test_comments_are_ignored(self): |
4440 | + """Comments in build file must be ignored.""" |
4441 | + prop_file = StringIO("# Hello World\n #Hello Again\n#####") |
4442 | + properties = platform._parse_build_properties_file(prop_file) |
4443 | + self.assertThat(len(properties), Equals(0)) |
4444 | + |
4445 | + def test_invalid_lines_are_ignored(self): |
4446 | + """lines without ana ssignment must be ignored.""" |
4447 | + prop_file = StringIO("Hello") |
4448 | + properties = platform._parse_build_properties_file(prop_file) |
4449 | + self.assertThat(len(properties), Equals(0)) |
4450 | + |
4451 | + def test_simple_value(self): |
4452 | + """Test a simple a=b expression.""" |
4453 | + prop_file = StringIO("a=b") |
4454 | + properties = platform._parse_build_properties_file(prop_file) |
4455 | + self.assertThat(properties, Equals(dict(a='b'))) |
4456 | + |
4457 | + def test_multiple_values(self): |
4458 | + """Test several expressions over multiple lines.""" |
4459 | + prop_file = StringIO("a=b\nb=23") |
4460 | + properties = platform._parse_build_properties_file(prop_file) |
4461 | + self.assertThat(properties, Equals(dict(a='b',b='23'))) |
4462 | + |
4463 | + def test_values_with_equals_in_them(self): |
4464 | + """Test that we can parse values with a '=' in them.""" |
4465 | + prop_file = StringIO("a=b=c") |
4466 | + properties = platform._parse_build_properties_file(prop_file) |
4467 | + self.assertThat(properties, Equals(dict(a='b=c'))) |
4468 | + |
4469 | + def test_dotted_values_work(self): |
4470 | + """Test that we can use dotted values as the keys.""" |
4471 | + prop_file = StringIO("ro.product.model=maguro") |
4472 | + properties = platform._parse_build_properties_file(prop_file) |
4473 | + self.assertThat(properties, Equals({'ro.product.model':'maguro'})) |
4474 | |
4475 | === renamed file 'autopilot/tests/test_bamf_emulator.py' => 'autopilot/tests/test_process_emulator.py' |
4476 | --- autopilot/tests/test_bamf_emulator.py 2013-01-06 21:39:32 +0000 |
4477 | +++ autopilot/tests/test_process_emulator.py 2013-04-15 21:43:32 +0000 |
4478 | @@ -15,11 +15,11 @@ |
4479 | from time import sleep, time |
4480 | |
4481 | |
4482 | -class BamfEmulatorTests(AutopilotTestCase): |
4483 | +class ProcessEmulatorTests(AutopilotTestCase): |
4484 | |
4485 | def ensure_gedit_not_running(self): |
4486 | """Close any open gedit applications.""" |
4487 | - apps = self.bamf.get_running_applications_by_desktop_file('gedit.desktop') |
4488 | + apps = self.process_manager.get_running_applications_by_desktop_file('gedit.desktop') |
4489 | if apps: |
4490 | # this is a bit brutal, but easier in this context than the alternative. |
4491 | call(['killall', 'gedit']) |
4492 | @@ -34,7 +34,7 @@ |
4493 | start = time() |
4494 | t = Thread(target=start_gedit()) |
4495 | t.start() |
4496 | - ret = self.bamf.wait_until_application_is_running('gedit.desktop', 10) |
4497 | + ret = self.process_manager.wait_until_application_is_running('gedit.desktop', 10) |
4498 | end = time() |
4499 | t.join() |
4500 | |
4501 | @@ -46,7 +46,7 @@ |
4502 | self.ensure_gedit_not_running() |
4503 | |
4504 | start = time() |
4505 | - ret = self.bamf.wait_until_application_is_running('gedit.desktop', 5) |
4506 | + ret = self.process_manager.wait_until_application_is_running('gedit.desktop', 5) |
4507 | end = time() |
4508 | |
4509 | self.assertThat(abs(end - start - 5.0), LessThan(1)) |
4510 | |
4511 | === modified file 'autopilot/utilities.py' |
4512 | --- autopilot/utilities.py 2013-03-03 21:21:08 +0000 |
4513 | +++ autopilot/utilities.py 2013-04-15 21:43:32 +0000 |
4514 | @@ -14,103 +14,26 @@ |
4515 | |
4516 | from __future__ import absolute_import |
4517 | |
4518 | +import inspect |
4519 | import logging |
4520 | import os |
4521 | -import sys |
4522 | import time |
4523 | from functools import wraps |
4524 | -from Xlib import X, display, protocol |
4525 | - |
4526 | -_display = None |
4527 | - |
4528 | -def get_display(): |
4529 | - """Get a Xlib display object. Creating the display prints garbage to stdout.""" |
4530 | - global _display |
4531 | - if _display is None: |
4532 | - with Silence(): |
4533 | - _display = display.Display() |
4534 | - return _display |
4535 | - |
4536 | - |
4537 | - |
4538 | -def make_window_skip_taskbar(window, set_flag=True): |
4539 | - """Set the skip-taskbar kint on an X11 window. |
4540 | - |
4541 | - 'window' should be an Xlib window object. |
4542 | - set_flag should be 'True' to set the flag, 'False' to clear it. |
4543 | - |
4544 | - """ |
4545 | - state = get_display().get_atom('_NET_WM_STATE_SKIP_TASKBAR', 1) |
4546 | - action = int(set_flag) |
4547 | - if action == 0: |
4548 | - print "Clearing flag" |
4549 | - elif action == 1: |
4550 | - print "Setting flag" |
4551 | - _setProperty('_NET_WM_STATE', [action, state, 0, 1], window) |
4552 | - get_display().sync() |
4553 | - |
4554 | - |
4555 | -def get_desktop_viewport(): |
4556 | - """Get the x,y coordinates for the current desktop viewport top-left corner.""" |
4557 | - return _getProperty('_NET_DESKTOP_VIEWPORT') |
4558 | - |
4559 | - |
4560 | -def get_desktop_geometry(): |
4561 | - """Get the full width and height of the desktop, including all the viewports.""" |
4562 | - return _getProperty('_NET_DESKTOP_GEOMETRY') |
4563 | - |
4564 | - |
4565 | -def _setProperty(_type, data, win=None, mask=None): |
4566 | - """ Send a ClientMessage event to a window""" |
4567 | - if not win: |
4568 | - win = get_display().screen().root |
4569 | - if type(data) is str: |
4570 | - dataSize = 8 |
4571 | - else: |
4572 | - # data length must be 5 - pad with 0's if it's short, truncate otherwise. |
4573 | - data = (data + [0] * (5 - len(data)))[:5] |
4574 | - dataSize = 32 |
4575 | - |
4576 | - ev = protocol.event.ClientMessage(window=win, |
4577 | - client_type=get_display().get_atom(_type), |
4578 | - data=(dataSize, data)) |
4579 | - |
4580 | - if not mask: |
4581 | - mask = (X.SubstructureRedirectMask | X.SubstructureNotifyMask) |
4582 | - get_display().screen().root.send_event(ev, event_mask=mask) |
4583 | - |
4584 | - |
4585 | -def _getProperty(_type, win=None): |
4586 | - if not win: |
4587 | - win = get_display().screen().root |
4588 | - atom = win.get_full_property(get_display().get_atom(_type), X.AnyPropertyType) |
4589 | - if atom: return atom.value |
4590 | - |
4591 | - |
4592 | -def get_compiz_setting(plugin_name, setting_name): |
4593 | - """Get a compiz setting object. |
4594 | - |
4595 | - 'plugin_name' is the name of the plugin (e.g. 'core' or 'unityshell') |
4596 | - 'setting_name' is the name of the setting (e.g. 'alt_tab_timeout') |
4597 | - |
4598 | - This function will raise KeyError if the plugin or setting named does not |
4599 | - exist. |
4600 | - |
4601 | - """ |
4602 | - # circular dependancy: |
4603 | - from autopilot.compizconfig import get_setting |
4604 | - return get_setting(plugin_name, setting_name) |
4605 | - |
4606 | - |
4607 | -def get_compiz_option(plugin_name, setting_name): |
4608 | - """Get a compiz setting value. |
4609 | - |
4610 | - This is the same as calling: |
4611 | - |
4612 | - >>> get_compiz_setting(plugin_name, setting_name).Value |
4613 | - |
4614 | - """ |
4615 | - return get_compiz_setting(plugin_name, setting_name).Value |
4616 | + |
4617 | + |
4618 | +def _pick_variant(variants, preferred_variant): |
4619 | + possible_backends = variants.keys() |
4620 | + get_debug_logger().debug("Possible variants: %s", ','.join(possible_backends)) |
4621 | + if preferred_variant in possible_backends: |
4622 | + possible_backends.sort(lambda a,b: -1 if a == preferred_variant else 0) |
4623 | + failure_reasons = [] |
4624 | + for be in possible_backends: |
4625 | + try: |
4626 | + return variants[be]() |
4627 | + except Exception as e: |
4628 | + get_debug_logger().warning("Can't create variant %s: %r", be, e) |
4629 | + failure_reasons.append('%s: %r' % (be, e)) |
4630 | + raise RuntimeError("Unable to instantiate any backends\n%s" % '\n'.join(failure_reasons)) |
4631 | |
4632 | |
4633 | # Taken from http://code.activestate.com/recipes/577564-context-manager-for-low-level-redirection-of-stdou/ |
4634 | @@ -226,7 +149,33 @@ |
4635 | def fdec(fn): |
4636 | @wraps(fn) |
4637 | def wrapped(*args, **kwargs): |
4638 | - sys.stderr.write("WARNING: This function is deprecated. Please use '%s' instead.\n" % alternative) |
4639 | + import sys |
4640 | + outerframe_details = inspect.getouterframes(inspect.currentframe())[1] |
4641 | + filename, line_number, function_name = outerframe_details[1:4] |
4642 | + sys.stderr.write("WARNING: in file \"{0}\", line {1} in {2}\n".format(filename, line_number, function_name)) |
4643 | + sys.stderr.write("This function is deprecated. Please use '%s' instead.\n" % alternative) |
4644 | return fn(*args, **kwargs) |
4645 | return wrapped |
4646 | return fdec |
4647 | + |
4648 | + |
4649 | +class _CleanupWrapper(object): |
4650 | + """Support for calling 'addCleanup' outside the test case.""" |
4651 | + |
4652 | + def __init__(self): |
4653 | + self._test_instance = None |
4654 | + |
4655 | + def __call__(self, callable, *args, **kwargs): |
4656 | + if self._test_instance is None: |
4657 | + raise RuntimeError("Out-of-test addCleanup can only be called while an autopilot test case is running!") |
4658 | + self._test_instance.addCleanup(callable, *args, **kwargs) |
4659 | + |
4660 | + def set_test_instance(self, test_instance): |
4661 | + self._test_instance = test_instance |
4662 | + test_instance.addCleanup(self._on_test_ended) |
4663 | + |
4664 | + def _on_test_ended(self): |
4665 | + self._test_instance = None |
4666 | + |
4667 | + |
4668 | +addCleanup = _CleanupWrapper() |
4669 | |
4670 | === modified file 'bin/autopilot' |
4671 | --- bin/autopilot 2013-02-21 02:37:04 +0000 |
4672 | +++ bin/autopilot 2013-04-15 21:43:32 +0000 |
4673 | @@ -27,13 +27,13 @@ |
4674 | from argparse import ArgumentParser |
4675 | from unittest.loader import TestLoader |
4676 | from unittest import TestSuite |
4677 | -from autopilot.introspection.gtk import GtkIntrospectionTestMixin |
4678 | -from autopilot.introspection.qt import QtIntrospectionTestMixin |
4679 | +from autopilot.introspection import launch_application |
4680 | +from autopilot.introspection.gtk import GtkApplicationLauncher |
4681 | +from autopilot.introspection.qt import QtApplicationLauncher |
4682 | |
4683 | # list autopilot depends here, with the form: |
4684 | # ('python module name', 'ubuntu package name'), |
4685 | DEPENDS = [ |
4686 | - ('compizconfig', 'python-compizconfig'), |
4687 | ('dbus', 'python-dbus'), |
4688 | ('gi.repository.GConf', 'gir1.2-gconf-2.0'), |
4689 | ('gi.repository.IBus', 'gir1.2-ibus-1.0'), |
4690 | @@ -345,27 +345,19 @@ |
4691 | exit(1) |
4692 | |
4693 | # We now have a full path to the application. |
4694 | - IntrospectionBase = None |
4695 | + launcher = None |
4696 | if args.interface == 'Auto': |
4697 | - IntrospectionBase = get_application_introspection_base(app_name) |
4698 | + launcher = get_application_introspection_base(app_name) |
4699 | elif args.interface == 'Gtk': |
4700 | - IntrospectionBase = GtkIntrospectionTestMixin |
4701 | + launcher = GtkApplicationLauncher() |
4702 | elif args.interface == 'Qt': |
4703 | - IntrospectionBase = QtIntrospectionTestMixin |
4704 | - if IntrospectionBase is None: |
4705 | + launcher = QtApplicationLauncher() |
4706 | + if launcher is None: |
4707 | print "Error: Could not determine introspection type to use for application '%s'." % app_name |
4708 | exit(1) |
4709 | |
4710 | - # IntrospectionBase is supposed to be a mixin class with a TestCase, and makes |
4711 | - # use of addCleanup to shut down the app after the test has completed. We |
4712 | - # patch that function here so we don't error... |
4713 | - def fake_cleanup(self, *args, **kwargs): |
4714 | - pass |
4715 | - IntrospectionBase.addCleanup = fake_cleanup |
4716 | - |
4717 | - b = IntrospectionBase() |
4718 | try: |
4719 | - b.launch_test_application(app_name, capture_output=False) |
4720 | + launch_application(launcher, app_name, capture_output=False) |
4721 | except RuntimeError as e: |
4722 | print "Error: " + e.message |
4723 | exit(1) |
4724 | @@ -387,9 +379,9 @@ |
4725 | print "Use the '-i' argument to specify an interface." |
4726 | exit(1) |
4727 | if 'libqtcore' in ldd_output: |
4728 | - return QtIntrospectionTestMixin |
4729 | + return QtApplicationLauncher |
4730 | elif 'libgtk' in ldd_output: |
4731 | - return GtkIntrospectionTestMixin |
4732 | + return GtkApplicationLauncher |
4733 | return None |
4734 | |
4735 | |
4736 | |
4737 | === modified file 'debian/changelog' |
4738 | --- debian/changelog 2013-03-26 00:02:00 +0000 |
4739 | +++ debian/changelog 2013-04-15 21:43:32 +0000 |
4740 | @@ -1,3 +1,9 @@ |
4741 | +autopilot (1.3) UNRELEASED; urgency=low |
4742 | + |
4743 | + * Create version 1.3 (LP: #1168971) |
4744 | + |
4745 | + -- Thomi Richards <thomi.richards@canonical.com> Mon, 15 Apr 2013 09:33:21 +1200 |
4746 | + |
4747 | autopilot (1.2daily13.03.26-0ubuntu1) raring; urgency=low |
4748 | |
4749 | * Automatic snapshot from revision 155 |
4750 | |
4751 | === modified file 'debian/control' |
4752 | --- debian/control 2012-12-04 11:43:20 +0000 |
4753 | +++ debian/control 2013-04-15 21:43:32 +0000 |
4754 | @@ -9,6 +9,7 @@ |
4755 | gir1.2-gtk-2.0, |
4756 | python (>= 2.6), |
4757 | python-dbus, |
4758 | + python-debian, |
4759 | python-setuptools, |
4760 | python-sphinx, |
4761 | python-testtools, |
4762 | @@ -28,7 +29,6 @@ |
4763 | gir1.2-glib-2.0, |
4764 | gir1.2-gtk-2.0, |
4765 | gir1.2-ibus-1.0, |
4766 | - python-compizconfig, |
4767 | python-dbus, |
4768 | python-junitxml, |
4769 | python-qt4, |
4770 | |
4771 | === added file 'docs/_templates/indexcontent.html' |
4772 | --- docs/_templates/indexcontent.html 1970-01-01 00:00:00 +0000 |
4773 | +++ docs/_templates/indexcontent.html 2013-04-15 21:43:32 +0000 |
4774 | @@ -0,0 +1,45 @@ |
4775 | +{% extends "defindex.html" %} |
4776 | +{% block tables %} |
4777 | +<p>Autopilot is a tool for writing <i>functional tests</i> for <i>GUI</i> applications. It works out-of-the-box for Several GUI toolkits, including Gtk2, Gtk3, Qt4, and Qt5/Qml.</p> |
4778 | + <p><strong>Parts of the documentation:</strong></p> |
4779 | + <table class="contentstable" align="center"> |
4780 | + <tr> |
4781 | + <td width="50%"> |
4782 | + <p class="biglink"> |
4783 | + <a class="biglink" href="{{ pathto("tutorial/tutorial") }}">Getting started with Autopilot</a><br/> |
4784 | + <span class="linkdescr">How to write your first Autopilot test.</span> |
4785 | + </p> |
4786 | + </td> |
4787 | + <td width="50%"> |
4788 | + <p class="biglink"> |
4789 | + <a class="biglink" href="{{ pathto("api/autopilot") }}">API Reference</a><br/> |
4790 | + <span class="linkdescr">API reference documentation for Autopilot.</span> |
4791 | + </p> |
4792 | + </td> |
4793 | + </tr> |
4794 | + <tr> |
4795 | + <td> |
4796 | + <p class="biglink"> |
4797 | + <a class="biglink" href="{{ pathto("faq/faq") }}">Frequently Asked Questions</a><br/> |
4798 | + <span class="linkdescr">...with answers!</span> |
4799 | + </p> |
4800 | + </td> |
4801 | + <td> |
4802 | + <p class="biglink"> |
4803 | + <a class="biglink" href="{{ pathto("porting/porting") }}">Porting Autopilot Tests</a><br/> |
4804 | + <span class="linkdescr">How to port your tests from earlier versions of Autopilot.</span> |
4805 | + </p> |
4806 | + </td> |
4807 | + </tr> |
4808 | + </table> |
4809 | + |
4810 | + <p><strong>Indices and tables:</strong></p> |
4811 | + <table class="contentstable" align="center"><tr> |
4812 | + <td width="50%"> |
4813 | + <p class="biglink"><a class="biglink" href="{{ pathto("py-modindex") }}">Module Index</a><br/> |
4814 | + <span class="linkdescr">quick access to all modules</span></p> |
4815 | +{# <p class="biglink"><a class="biglink" href="{{ pathto("contents") }}">Complete Table of Contents</a><br/> |
4816 | + <span class="linkdescr">lists all sections and subsections</span></p> #} |
4817 | + </td></tr> |
4818 | + </table> |
4819 | +{% endblock %} |
4820 | |
4821 | === modified file 'docs/api/autopilot.rst' |
4822 | --- docs/api/autopilot.rst 2013-02-28 01:01:11 +0000 |
4823 | +++ docs/api/autopilot.rst 2013-04-15 21:43:32 +0000 |
4824 | @@ -1,103 +1,10 @@ |
4825 | +:orphan: |
4826 | + |
4827 | Autopilot API Documentation |
4828 | =========================== |
4829 | |
4830 | -Autopilot Utility Modules |
4831 | -+++++++++++++++++++++++++ |
4832 | - |
4833 | -:mod:`testcase` Module |
4834 | ----------------------- |
4835 | - |
4836 | -.. autoclass:: autopilot.testcase.AutopilotTestCase |
4837 | - :members: |
4838 | - |
4839 | -.. autofunction:: autopilot.testcase.multiply_scenarios |
4840 | - |
4841 | -:mod:`keybindings` Module |
4842 | -------------------------- |
4843 | - |
4844 | -.. automodule:: autopilot.keybindings |
4845 | - :members: |
4846 | - :undoc-members: |
4847 | - :show-inheritance: |
4848 | - |
4849 | -Emulators Package |
4850 | -+++++++++++++++++ |
4851 | - |
4852 | -:mod:`input` Module |
4853 | -------------------- |
4854 | - |
4855 | -.. automodule:: autopilot.emulators.input |
4856 | - :members: |
4857 | - :undoc-members: |
4858 | - :show-inheritance: |
4859 | - |
4860 | -:mod:`bamf` Module |
4861 | ------------------- |
4862 | - |
4863 | -.. automodule:: autopilot.emulators.bamf |
4864 | - :members: |
4865 | - :undoc-members: |
4866 | - :show-inheritance: |
4867 | - |
4868 | -:mod:`ibus` Module |
4869 | ------------------- |
4870 | - |
4871 | -.. automodule:: autopilot.emulators.ibus |
4872 | - :members: |
4873 | - :undoc-members: |
4874 | - :show-inheritance: |
4875 | - |
4876 | -:mod:`zeitgeist` Module |
4877 | ------------------------ |
4878 | - |
4879 | -.. automodule:: autopilot.emulators.zeitgeist |
4880 | - :members: |
4881 | - :undoc-members: |
4882 | - :show-inheritance: |
4883 | - |
4884 | -Introspection Package |
4885 | -+++++++++++++++++++++ |
4886 | - |
4887 | -:mod:`introspection` Package |
4888 | ----------------------------- |
4889 | - |
4890 | -.. automodule:: autopilot.introspection |
4891 | - :members: |
4892 | - :undoc-members: |
4893 | - :show-inheritance: |
4894 | - |
4895 | -:mod:`dbus` Module |
4896 | -------------------- |
4897 | - |
4898 | -.. automodule:: autopilot.introspection.dbus |
4899 | - :members: |
4900 | - :undoc-members: |
4901 | - :show-inheritance: |
4902 | - |
4903 | -:mod:`qt` Module |
4904 | -------------------- |
4905 | - |
4906 | -.. automodule:: autopilot.introspection.qt |
4907 | - :members: |
4908 | - :undoc-members: |
4909 | - :show-inheritance: |
4910 | - |
4911 | -:mod:`gtk` Module |
4912 | -------------------- |
4913 | - |
4914 | -.. automodule:: autopilot.introspection.gtk |
4915 | - :members: |
4916 | - :undoc-members: |
4917 | - :show-inheritance: |
4918 | - |
4919 | -Matchers Package |
4920 | -++++++++++++++++ |
4921 | - |
4922 | -:mod:`matchers` Package |
4923 | ------------------------ |
4924 | - |
4925 | -.. automodule:: autopilot.matchers |
4926 | - :members: |
4927 | - :undoc-members: |
4928 | - :show-inheritance: |
4929 | - |
4930 | +.. toctree:: |
4931 | + :maxdepth: 1 |
4932 | + :glob: |
4933 | + |
4934 | + * |
4935 | |
4936 | === added file 'docs/api/display.rst' |
4937 | --- docs/api/display.rst 1970-01-01 00:00:00 +0000 |
4938 | +++ docs/api/display.rst 2013-04-15 21:43:32 +0000 |
4939 | @@ -0,0 +1,7 @@ |
4940 | +``display`` - Get information about the current display(s) |
4941 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4942 | + |
4943 | + |
4944 | +.. automodule:: autopilot.display |
4945 | + :members: |
4946 | + :undoc-members: |
4947 | |
4948 | === added file 'docs/api/emulators.rst' |
4949 | --- docs/api/emulators.rst 1970-01-01 00:00:00 +0000 |
4950 | +++ docs/api/emulators.rst 2013-04-15 21:43:32 +0000 |
4951 | @@ -0,0 +1,20 @@ |
4952 | +``emulators`` - Backwards compatibility for autopilot v1.2 |
4953 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4954 | + |
4955 | + |
4956 | +.. module autopilot.emulators |
4957 | + :synopsis: Backwards compatibility module to provide the 'emulators' namespace. |
4958 | + |
4959 | + |
4960 | +The emulators module exists for backwards compatibility only. |
4961 | + |
4962 | +This module exists to make it easier to upgrade from autopilot v1.2 to v1.3 by |
4963 | +providing the old 'emulators' namespace. However, it's a bad idea to rely on this |
4964 | +module continuing to exist. It contains several sub-modules: |
4965 | + |
4966 | + * :mod:`autopilot.display` |
4967 | + * autopilot.clipboard (deprecated) |
4968 | + * autopilot.dbus_handler (for internal use only) |
4969 | + * autopilot.ibus (deprecated) |
4970 | + * :mod:`autopilot.input` |
4971 | + |
4972 | |
4973 | === added file 'docs/api/gestures.rst' |
4974 | --- docs/api/gestures.rst 1970-01-01 00:00:00 +0000 |
4975 | +++ docs/api/gestures.rst 2013-04-15 21:43:32 +0000 |
4976 | @@ -0,0 +1,6 @@ |
4977 | +``gestures`` - Gestural and multi-touch support |
4978 | ++++++++++++++++++++++++++++++++++++++++++++++++ |
4979 | + |
4980 | + |
4981 | +.. automodule:: autopilot.gestures |
4982 | + :members: |
4983 | |
4984 | === added file 'docs/api/input.rst' |
4985 | --- docs/api/input.rst 1970-01-01 00:00:00 +0000 |
4986 | +++ docs/api/input.rst 2013-04-15 21:43:32 +0000 |
4987 | @@ -0,0 +1,7 @@ |
4988 | +``input`` - Generate keyboard, mouse, and touch input events |
4989 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4990 | + |
4991 | + |
4992 | +.. automodule:: autopilot.input |
4993 | + :members: |
4994 | + :undoc-members: |
4995 | |
4996 | === added file 'docs/api/introspection.rst' |
4997 | --- docs/api/introspection.rst 1970-01-01 00:00:00 +0000 |
4998 | +++ docs/api/introspection.rst 2013-04-15 21:43:32 +0000 |
4999 | @@ -0,0 +1,6 @@ |
5000 | +``introspection`` - Autopilot introspection internals |
just scrolled quickly through it (no real review yet) but stumbled across this:
3442 + ... a Qt4 Qml application might be launched like this:: test_applicatio n('qmlscene' , 'my_scene.qml')
3443 +
3444 + app_proxy = self.launch_
There is no qmlscene in Qt4. It was the QWidget based "qmlviewer" back then. qmlscene is the new Qt5 version which directly paints qml on a openGL scenegraph.