Merge lp:~thomir-deactivatedaccount/autopilot/merge-private-code into lp:autopilot

Proposed by Thomi Richards
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
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Sergio Schvezov Needs Fixing
Michael Zanetti (community) Needs Fixing
Review via email: mp+156425@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Michael Zanetti (mzanetti) wrote :

just scrolled quickly through it (no real review yet) but stumbled across this:

3442 + ... a Qt4 Qml application might be launched like this::
3443 +
3444 + app_proxy = self.launch_test_application('qmlscene', 'my_scene.qml')

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.

Revision history for this message
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 QtIntrospectionTestMixin

but can be easily fixed by simply removing this import, and not inheriting from QtIntrospectionTestMixin.

Running the tests fails with the following error:

_StringException: Traceback (most recent call last):
  File "/tmp/webbrowser-app/tests/autopilot/webbrowser_app/tests/test_mainwindow.py", line 263, in setUp
    super(TestMainWindowStartOpenRemotePageBase, self).setUp()
  File "/tmp/webbrowser-app/tests/autopilot/webbrowser_app/tests/__init__.py", line 161, in setUp
    super(BrowserTestCaseBaseWithHTTPServer, self).setUp()
  File "/tmp/webbrowser-app/tests/autopilot/webbrowser_app/tests/__init__.py", line 45, in setUp
    self.launch_test_local()
  File "/tmp/webbrowser-app/tests/autopilot/webbrowser_app/tests/__init__.py", line 62, in launch_test_local
    *self.ARGS)
  File "/usr/lib/python2.7/dist-packages/autopilot/testcase.py", line 450, in launch_test_application
    process = launch_application(launcher, application, *arguments, **kwargs)
  File "/usr/lib/python2.7/dist-packages/autopilot/introspection/__init__.py", line 80, in launch_application
    path, args = launcher.prepare_environment(application, list(arguments))
AttributeError: 'NoneType' object has no attribute 'prepare_environment'

Looks like a problem in autopilot itself.

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
Sergio Schvezov (sergiusens) wrote :

Packaging/docs need fixing

$ bzr bd
...
/home/sergiusens/projects/autopilot/build-area/autopilot-1.2daily13.03.26/docs/api/autopilot.rst:: WARNING: document isn't included in any toctree

make[1]: *** [override_dh_auto_build] Error 1
make[1]: Leaving directory `/home/sergiusens/projects/autopilot/build-area/autopilot-1.2daily13.03.26'
make: *** [build] Error 2

review: Needs Fixing
Revision history for this message
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://code.launchpad.net/~thomir/autopilot/merge-private-code/+merge/156425/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/autopilot-ci/10/
Executed test runs:
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-raring-amd64-ci/10/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-raring-armhf-ci/9/console

Click here to trigger a rebuild:
http://s-jenkins:8080/job/autopilot-ci/10/rebuild

review: Needs Fixing (continuous-integration)
185. By Thomi Richards

Bump current version to v1.3.

Revision history for this message
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.

Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :
Download full text (3.5 KiB)

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 QtIntrospectionTestMixin
>
> but can be easily fixed by simply removing this import, and not inheriting
> from QtIntrospectionTestMixin.

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/autopilot/webbrowser_app/tests/test_mainwindow.py", line 263, in
> setUp
> super(TestMainWindowStartOpenRemotePageBase, self).setUp()
> File "/tmp/webbrowser-app/tests/autopilot/webbrowser_app/tests/__init__.py",
> line 161, in setUp
> super(BrowserTestCaseBaseWithHTTPServer, self).setUp()
> File "/tmp/webbrowser-app/tests/autopilot/webbrowser_app/tests/__init__.py",
> line 45, in setUp
> self.launch_test_local()
> File "/tmp/webbrowser-app/tests/autopilot/webbrowser_app/tests/__init__.py",
> line 62, in launch_test_local
> *self.ARGS)
> File "/usr/lib/python2.7/dist-packages/autopilot/testcase.py", line 450, in
> launch_test_application
> process = launch_application(launcher, application, *arguments, **kwargs)
> File "/usr/lib/python2.7/dist-packages/autopilot/introspection/__init__.py",
> line 80, in launch_application
> path, args = launcher.prepare_environment(application, list(arguments))
> AttributeError: 'NoneType' object has no attribute 'prepare_environment'
>
> 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("Autopilot could not determine the correct \
                introspection type to use. You can specify one by overriding \
                the AutopilotTestCase.pick_app_launcher method.")

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/tests/test_ap_apps.py - I've pasted the relevant sections below:

class QtTests(ApplicationTests):

    def pick_app_launcher(self, app_path):
        # force Qt app introspection:
        from autopilot.introspection.qt import QtApplicationLauncher
        return QtApplicationLauncher()

    def test_can_launch_qt_script(self):
        path = self.write_script(dedent("""\
            #!/usr/bin/python
            from PyQt4.QtGui import QMainWindow, QAppli...

Read more...

Revision history for this message
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:

https://bugs.launchpad.net/autopilot/+bug/1168975

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.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
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.

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

Hi Sergio,

> Packaging/docs need fixing
>
> $ bzr bd
> ...
> /home/sergiusens/projects/autopilot/build-
> area/autopilot-1.2daily13.03.26/docs/api/autopilot.rst:: WARNING: document
> isn't included in any toctree
>

Fixed - this is because we have our own custom index page now.

Thanks,

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
189. By Thomi Richards

Merged chris's work with the process managaer.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
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.

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

Merged trunk, resolved conflicts.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
193. By Thomi Richards

More docs.

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

Added the start of a porting document.

195. By Thomi Richards

Merge in process test fixes.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
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.

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

Fix bullet list in porting document.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches