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
=== renamed file 'autopilot/emulators/clipboard.py' => 'autopilot/clipboard.py'
=== removed file 'autopilot/compizconfig.py'
--- autopilot/compizconfig.py 2012-08-27 21:26:45 +0000
+++ autopilot/compizconfig.py 1970-01-01 00:00:00 +0000
@@ -1,57 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2012 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This script is designed to run unity in a test drive manner. It will drive
10# X and test the GL calls that Unity makes, so that we can easily find out if
11# we are triggering graphics driver/X bugs.
12
13"""Functions that wrap compizconfig to avoid some unpleasantness in that module."""
14
15from __future__ import absolute_import
16
17
18from autopilot.utilities import Silence
19
20_global_context = None
21
22def get_global_context():
23 """Get the compizconfig global context object."""
24 global _global_context
25 if _global_context is None:
26 with Silence():
27 from compizconfig import Context
28 _global_context = Context()
29 return _global_context
30
31
32def get_plugin(plugin_name):
33 """Get a compizconfig plugin with the specified name.
34
35 Raises KeyError of the plugin named does not exist.
36
37 """
38 ctx = get_global_context()
39 with Silence():
40 try:
41 return ctx.Plugins[plugin_name]
42 except KeyError:
43 raise KeyError("Compiz plugin '%s' does not exist." % (plugin_name))
44
45
46def get_setting(plugin_name, setting_name):
47 """Get a compizconfig setting object, given a plugin name and setting name.
48
49 Raises KeyError if the plugin or setting is not found.
50
51 """
52 plugin = get_plugin(plugin_name)
53 with Silence():
54 try:
55 return plugin.Screen[setting_name]
56 except KeyError:
57 raise KeyError("Compiz setting '%s' does not exist in plugin '%s'." % (setting_name, plugin_name))
580
=== renamed file 'autopilot/emulators/dbus_handler.py' => 'autopilot/dbus_handler.py'
=== added directory 'autopilot/display'
=== added file 'autopilot/display/_X11.py'
--- autopilot/display/_X11.py 1970-01-01 00:00:00 +0000
+++ autopilot/display/_X11.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,50 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Christopher Lee
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9import logging
10
11from autopilot.display import Display as DisplayBase
12
13logger = logging.getLogger(__name__)
14
15class Display(DisplayBase):
16 def __init__(self):
17 # Note: MUST import these here, rather than at the top of the file. Why?
18 # Because sphinx imports these modules to build the API documentation,
19 # which in turn tries to import Gdk, which in turn fails because there's
20 # no DISPlAY environment set in the package builder.
21 from gi.repository import Gdk
22 self._default_screen = Gdk.Screen.get_default()
23 self._blacklisted_drivers = ["NVIDIA"]
24
25 def get_num_screens(self):
26 """Get the number of screens attached to the PC."""
27 return self._default_screen.get_n_monitors()
28
29 def get_primary_screen(self):
30 """Returns an integer of which screen is considered the primary"""
31 return self._default_screen.get_primary_monitor()
32
33 def get_screen_width(self, screen_number=0):
34 # return self._default_screen.get_width()
35 return self.get_screen_geometry(screen_number)[2]
36
37 def get_screen_height(self, screen_number=0):
38 #return self._default_screen.get_height()
39 return self.get_screen_geometry(screen_number)[3]
40
41 def get_screen_geometry(self, screen_number):
42 """Get the geometry for a particular screen.
43
44 :return: Tuple containing (x, y, width, height).
45
46 """
47 if screen_number < 0 or screen_number >= self.get_num_screens():
48 raise ValueError('Specified screen number is out of range.')
49 rect = self._default_screen.get_monitor_geometry(screen_number)
50 return (rect.x, rect.y, rect.width, rect.height)
051
=== added file 'autopilot/display/__init__.py'
--- autopilot/display/__init__.py 1970-01-01 00:00:00 +0000
+++ autopilot/display/__init__.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,136 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Christopher Lee
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9"""The display module contaions support for getting screen information."""
10
11from collections import OrderedDict
12from autopilot.utilities import _pick_variant
13from autopilot.input import Mouse
14
15
16def is_rect_on_screen(screen_number, rect):
17 """Returns True if *rect* is **entirely** on the specified screen, with no overlap."""
18 (x, y, w, h) = rect
19 (mx, my, mw, mh) = Display.create().get_screen_geometry(screen_number)
20 return (x >= mx and x + w <= mx + mw and y >= my and y + h <= my + mh)
21
22
23def is_point_on_screen(screen_number, point):
24 """Returns True if *point* is on the specified screen.
25
26 *point* must be an iterable type with two elements: (x, y)
27
28 """
29 x, y = point
30 (mx, my, mw, mh) = Display.create().get_screen_geometry(screen_number)
31 return (x >= mx and x < mx + mw and y >= my and y < my + mh)
32
33
34def is_point_on_any_screen(point):
35 """Returns true if *point* is on any currently configured screen."""
36 return any([is_point_on_screen(m, point) for m in range(Display.create().get_num_screens())])
37
38
39def move_mouse_to_screen(screen_number):
40 """Move the mouse to the center of the specified screen."""
41 geo = Display.create().get_screen_geometry(screen_number)
42 x = geo[0] + (geo[2] / 2)
43 y = geo[1] + (geo[3] / 2)
44 #dont animate this or it might not get there due to barriers
45 Mouse.create().move(x, y, False)
46
47
48# veebers TODO: Write this so it's usable.
49# def drag_window_to_screen(self, window, screen):
50# """Drags *window* to *screen*
51
52# :param BamfWindow window: The window to drag
53# :param integer screen: The screen to drag the *window* to
54# :raises: **TypeError** if *window* is not a BamfWindow
55
56# """
57# if not isinstance(window, BamfWindow):
58# raise TypeError("Window must be a BamfWindow")
59
60# if window.monitor == screen:
61# logger.debug("Window %r is already on screen %d." % (window.x_id, screen))
62# return
63
64# assert(not window.is_maximized)
65# (win_x, win_y, win_w, win_h) = window.geometry
66# (mx, my, mw, mh) = self.get_screen_geometry(screen)
67
68# logger.debug("Dragging window %r to screen %d." % (window.x_id, screen))
69
70# mouse = Mouse()
71# keyboard = Keyboard()
72# mouse.move(win_x + win_w/2, win_y + win_h/2)
73# keyboard.press("Alt")
74# mouse.press()
75# keyboard.release("Alt")
76
77# # We do the movements in two steps, to reduce the risk of being
78# # blocked by the pointer barrier
79# target_x = mx + mw/2
80# target_y = my + mh/2
81# mouse.move(win_x, target_y, rate=20, time_between_events=0.005)
82# mouse.move(target_x, target_y, rate=20, time_between_events=0.005)
83# mouse.release()
84
85
86class Display:
87 """The base class/inteface for the display devices"""
88
89 @staticmethod
90 def create(preferred_variant=''):
91 """Get an instance of the Display class.
92
93 If variant is specified, it should be a string that specifies a backend to
94 use. However, this hint can be ignored - autopilot will prefer to return a
95 variant other than the one requested, rather than fail to return anything at
96 all.
97
98 If autopilot cannot instantate any of the possible backends, a RuntimeError
99 will be raised.
100 """
101 def get_x11_display():
102 from autopilot.display._X11 import Display
103 return Display()
104
105 def get_upa_display():
106 from autopilot.display._upa import Display
107 return Display()
108
109 variants = OrderedDict()
110 variants['X11'] = get_x11_display
111 variants['UPA'] = get_upa_display
112 return _pick_variant(variants, preferred_variant)
113
114 class BlacklistedDriverError(RuntimeError):
115 """Cannot set primary monitor when running drivers listed in the driver blacklist."""
116
117 def get_num_screens(self):
118 """Get the number of screens attached to the PC."""
119 raise NotImplementedError("You cannot use this class directly.")
120
121 def get_primary_screen(self):
122 raise NotImplementedError("You cannot use this class directly.")
123
124 def get_screen_width(self, screen_number=0):
125 raise NotImplementedError("You cannot use this class directly.")
126
127 def get_screen_height(self, screen_number=0):
128 raise NotImplementedError("You cannot use this class directly.")
129
130 def get_screen_geometry(self, monitor_number):
131 """Get the geometry for a particular monitor.
132
133 :return: Tuple containing (x, y, width, height).
134
135 """
136 raise NotImplementedError("You cannot use this class directly.")
0137
=== added file 'autopilot/display/_upa.py'
--- autopilot/display/_upa.py 1970-01-01 00:00:00 +0000
+++ autopilot/display/_upa.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,40 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Christopher Lee
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9import logging
10
11from autopilot.display import Display as DisplayBase
12from upa import get_resolution
13
14logger = logging.getLogger(__name__)
15
16class Display(DisplayBase):
17 """The base class/inteface for the display devices"""
18
19 def get_num_screens(self):
20 """Get the number of screens attached to the PC."""
21 return 1
22
23 def get_primary_screen(self):
24 """Returns an integer of which screen is considered the primary"""
25 return 0
26
27 def get_screen_width(self):
28 return get_resolution()[0]
29
30 def get_screen_height(self):
31 return get_resolution()[1]
32
33 def get_screen_geometry(self, screen_number):
34 """Get the geometry for a particular screen.
35
36 :return: Tuple containing (x, y, width, height).
37
38 """
39 res = get_resolution()
40 return (0, 0, res[0], res[1])
041
=== removed directory 'autopilot/emulators'
=== added file 'autopilot/emulators.py'
--- autopilot/emulators.py 1970-01-01 00:00:00 +0000
+++ autopilot/emulators.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,21 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This script is designed to run unity in a test drive manner. It will drive
10# X and test the GL calls that Unity makes, so that we can easily find out if
11# we are triggering graphics driver/X bugs.
12
13import autopilot.display as display
14import autopilot.clipboard as clipboard
15import autopilot.dbus_handler as dbus_handler
16import autopilot.ibus as ibus
17import autopilot.input as input
18
19
20"""
21"""
022
=== removed file 'autopilot/emulators/X11.py'
--- autopilot/emulators/X11.py 2013-03-03 21:21:08 +0000
+++ autopilot/emulators/X11.py 1970-01-01 00:00:00 +0000
@@ -1,187 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2010 Canonical
3# Author: Alex Launi
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This script is designed to run unity in a test drive manner. It will drive
10# X and test the GL calls that Unity makes, so that we can easily find out if
11# we are triggering graphics driver/X bugs.
12
13"""A collection of emulators for X11 - namely keyboards and mice.
14
15In the future we may also need other devices.
16
17"""
18
19from __future__ import absolute_import
20
21import logging
22import os
23import subprocess
24
25from autopilot.emulators.bamf import BamfWindow
26from autopilot.emulators.input import get_keyboard, get_mouse
27from autopilot.utilities import deprecated
28
29
30logger = logging.getLogger(__name__)
31
32
33def reset_display():
34 from autopilot.emulators.input._X11 import reset_display
35 reset_display()
36
37
38# Keyboard and Mouse are no longer here. This is for backwards compatibility,
39# but will eventually dissapear.
40@deprecated('autopilot.emulators.input.get_keyboard')
41def Keyboard():
42 return get_keyboard()
43
44
45@deprecated('autopilot.emulators.input.get_mouse')
46def Mouse():
47 return get_mouse()
48
49
50class ScreenGeometry:
51 """Get details about screen geometry."""
52
53 class BlacklistedDriverError(RuntimeError):
54 """Cannot set primary monitor when running drivers listed in the driver blacklist."""
55
56 def __init__(self):
57 # Note: MUST import these here, rather than at the top of the file. Why?
58 # Because sphinx imports these modules to build the API documentation,
59 # which in turn tries to import Gdk, which in turn fails because there's
60 # no DISPlAY environment set in the package builder.
61 from gi.repository import Gdk
62 self._default_screen = Gdk.Screen.get_default()
63 self._blacklisted_drivers = ["NVIDIA"]
64
65 def get_num_monitors(self):
66 """Get the number of monitors attached to the PC."""
67 return self._default_screen.get_n_monitors()
68
69 def get_primary_monitor(self):
70 return self._default_screen.get_primary_monitor()
71
72 def set_primary_monitor(self, monitor):
73 """Set *monitor* to be the primary monitor.
74
75 :param int monitor: Must be between 0 and the number of configured
76 monitors.
77 :raises: **ValueError** if an invalid monitor is specified.
78 :raises: **BlacklistedDriverError** if your video driver does not
79 support this.
80
81 """
82 try:
83 glxinfo_out = subprocess.check_output("glxinfo")
84 except OSError, e:
85 raise OSError("Failed to run glxinfo: %s. (do you have mesa-utils installed?)" % e)
86
87 for dri in self._blacklisted_drivers:
88 if dri in glxinfo_out:
89 raise ScreenGeometry.BlacklistedDriverError('Impossible change the primary monitor for the given driver')
90
91 if monitor < 0 or monitor >= self.get_num_monitors():
92 raise ValueError('Monitor %d is not in valid range of 0 <= monitor < %d.' % (self.get_num_monitors()))
93
94 monitor_name = self._default_screen.get_monitor_plug_name(monitor)
95
96 if not monitor_name:
97 raise ValueError('Could not get monitor name from monitor number %d.' % (monitor))
98
99 ret = os.spawnlp(os.P_WAIT, "xrandr", "xrandr", "--output", monitor_name, "--primary")
100
101 if ret != 0:
102 raise RuntimeError('Xrandr can\'t set the primary monitor. error code: %d' % (ret))
103
104 def get_screen_width(self):
105 return self._default_screen.get_width()
106
107 def get_screen_height(self):
108 return self._default_screen.get_height()
109
110 def get_monitor_geometry(self, monitor_number):
111 """Get the geometry for a particular monitor.
112
113 :return: Tuple containing (x, y, width, height).
114
115 """
116 if monitor_number < 0 or monitor_number >= self.get_num_monitors():
117 raise ValueError('Specified monitor number is out of range.')
118 rect = self._default_screen.get_monitor_geometry(monitor_number)
119 return (rect.x, rect.y, rect.width, rect.height)
120
121 def is_rect_on_monitor(self, monitor_number, rect):
122 """Returns True if *rect* is **entirely** on the specified monitor, with no overlap."""
123
124 if type(rect) is not tuple or len(rect) != 4:
125 raise TypeError("rect must be a tuple of 4 int elements.")
126
127 (x, y, w, h) = rect
128 (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number)
129 return (x >= mx and x + w <= mx + mw and y >= my and y + h <= my + mh)
130
131 def is_point_on_monitor(self, monitor_number, point):
132 """Returns True if *point* is on the specified monitor.
133
134 *point* must be an iterable type with two elements: (x, y)
135
136 """
137 x,y = point
138 (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number)
139 return (x >= mx and x < mx + mw and y >= my and y < my + mh)
140
141 def is_point_on_any_monitor(self, point):
142 """Returns true if *point* is on any currently configured monitor."""
143 return any([self.is_point_on_monitor(m, point) for m in range(self.get_num_monitors())])
144
145 def move_mouse_to_monitor(self, monitor_number):
146 """Move the mouse to the center of the specified monitor."""
147 geo = self.get_monitor_geometry(monitor_number)
148 x = geo[0] + (geo[2] / 2)
149 y = geo[1] + (geo[3] / 2)
150 #dont animate this or it might not get there due to barriers
151 Mouse().move(x, y, False)
152
153 def drag_window_to_monitor(self, window, monitor):
154 """Drags *window* to *monitor*
155
156 :param BamfWindow window: The window to drag
157 :param integer monitor: The monitor to drag the *window* to
158 :raises: **TypeError** if *window* is not a BamfWindow
159
160 """
161 if not isinstance(window, BamfWindow):
162 raise TypeError("Window must be a BamfWindow")
163
164 if window.monitor == monitor:
165 logger.debug("Window %r is already on monitor %d." % (window.x_id, monitor))
166 return
167
168 assert(not window.is_maximized)
169 (win_x, win_y, win_w, win_h) = window.geometry
170 (mx, my, mw, mh) = self.get_monitor_geometry(monitor)
171
172 logger.debug("Dragging window %r to monitor %d." % (window.x_id, monitor))
173
174 mouse = Mouse()
175 keyboard = Keyboard()
176 mouse.move(win_x + win_w/2, win_y + win_h/2)
177 keyboard.press("Alt")
178 mouse.press()
179 keyboard.release("Alt")
180
181 # We do the movements in two steps, to reduce the risk of being
182 # blocked by the pointer barrier
183 target_x = mx + mw/2
184 target_y = my + mh/2
185 mouse.move(win_x, target_y, rate=20, time_between_events=0.005)
186 mouse.move(target_x, target_y, rate=20, time_between_events=0.005)
187 mouse.release()
1880
=== removed file 'autopilot/emulators/__init__.py'
--- autopilot/emulators/__init__.py 2012-08-20 23:57:20 +0000
+++ autopilot/emulators/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,11 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2012 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9"""
10A collection of emulators that make it easier to interact with X11 and Unity.
11"""
120
=== removed file 'autopilot/emulators/processmanager.py'
--- autopilot/emulators/processmanager.py 2012-06-07 04:52:04 +0000
+++ autopilot/emulators/processmanager.py 1970-01-01 00:00:00 +0000
@@ -1,66 +0,0 @@
1# Copyright 2012 Canonical
2# Author: Thomi Richards
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8"""The processmanager module contains utilities for starting, stopping, and
9generally managing processes during a test."""
10
11from __future__ import absolute_import
12import logging
13from time import sleep
14
15from autopilot.emulators.bamf import Bamf
16
17
18logger = logging.getLogger(__name__)
19
20class ProcessManager(object):
21 """Manage Processes during a test cycle."""
22
23 def __init__(self):
24 self._bamf = Bamf()
25 self.snapshot = None
26
27 def snapshot_running_apps(self):
28 """Make a list of all the running applications, and store it.
29
30 The stored list can later be used to detect any applications that have
31 been launched during a test and not shut down.
32
33 You may only call this method once before calling
34 compare_system_with_snapshot. Calling this method multiple times will
35 cause a RuntimeError to be raised.
36 """
37
38 if self.snapshot:
39 raise RuntimeError("You may only call snapshot_running_apps once \
40before calling compare_system_with_snapshot.")
41
42 self.snapshot = self._bamf.get_running_applications()
43
44 def compare_system_with_snapshot(self):
45 """Compare the currently running application with the last snapshot.
46
47 This method will raise an AssertionError if there are any new applications
48 currently running that were not running when the snapshot was taken.
49
50 This method should typically be called at the every end of a test.
51 """
52 if self.snapshot is None:
53 raise RuntimeError("No snapshot to match against.")
54
55 new_apps = []
56 for i in range(10):
57 current_apps = self._bamf.get_running_applications()
58 new_apps = filter(lambda i: i not in self.snapshot, current_apps)
59 if not new_apps:
60 self.snapshot = None
61 return
62 sleep(1)
63 self.snapshot = None
64 raise AssertionError("The following apps were started during the test and not closed: %r", new_apps)
65
66
670
=== removed file 'autopilot/emulators/zeitgeist.py'
--- autopilot/emulators/zeitgeist.py 2012-10-26 14:15:43 +0000
+++ autopilot/emulators/zeitgeist.py 1970-01-01 00:00:00 +0000
@@ -1,66 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2012 Canonical
3# Author: Brandon Schaefer
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9"""Provide ability to register text files with the file lens."""
10
11from __future__ import absolute_import
12
13import logging
14import os.path
15from zeitgeist.client import ZeitgeistClient
16from zeitgeist.datamodel import Event, Interpretation, Manifestation, ResultType
17
18
19class Zeitgeist(object):
20 """Provide access zeitgeist."""
21
22 def __init__(self):
23 self.zg = ZeitgeistClient()
24 self.logger = logging.getLogger(__name__)
25
26 def add_existing_file(self, path):
27 """Registers *file* with zeitgeist.
28
29 :param string file: full path to an existing text file to register.
30 :raises: **RuntimeError** if *file* does not exist.
31
32 """
33 if os.path.exists(path):
34 self.__add_text_file(path)
35 else:
36 raise RuntimeError("File not found on path: %s." % (path))
37
38 def __add_text_file(self, path):
39 """Takes a path to a file and creates an event for it then querys it."""
40 file_lens = "file://"
41 dir_path = os.path.dirname(path)
42 name = os.path.basename(path)
43
44 event = Event.new_for_values (interpretation=Interpretation.ACCESS_EVENT,
45 manifestation=Manifestation.USER_ACTIVITY,
46 subject_uri=file_lens + path,
47 subject_interpretation=Interpretation.TEXT_DOCUMENT,
48 subject_manifestation=Manifestation.FILE_DATA_OBJECT,
49 subject_origin=file_lens + dir_path,
50 subject_text=name)
51 self.zg.insert_event(event)
52
53 template = Event.new_for_values (interpretation=Interpretation.ACCESS_EVENT,
54 manifestation=Manifestation.USER_ACTIVITY)
55
56 self.zg.find_events_for_template (template,
57 self.__log_events_cb,
58 num_events=1,
59 result_type=ResultType.MostRecentSubjects)
60
61 def __log_events_cb(self, events):
62 """Callback to recive events, we are just using it to log each event."""
63 self.logger.info("Found Events")
64 for event in events:
65 for subject in event.subjects:
66 self.logger.info(" * %s" % (subject.uri))
670
=== added file 'autopilot/gestures.py'
--- autopilot/gestures.py 1970-01-01 00:00:00 +0000
+++ autopilot/gestures.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,62 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9"""Gestural support for autopilot.
10
11This module contains functions that can generate touch and multi-tuch gestures
12for you. This is a convenience for the test author - there is nothing to prevent
13you from generating your own gestures!
14
15"""
16
17from autopilot.input import Touch
18from time import sleep
19
20
21def pinch(center, vector_start, vector_end):
22 """Perform a two finger pinch (zoom) gesture.
23
24 :param center: The coordinates (x,y) of the center of the pinch gesture.
25 :param vector_start: The (x,y) values to move away from the center for the start.
26 :param vector_end: The (x,y) values to move away from the center for the end.
27
28 The fingers will move in 100 steps between the start and the end points.
29 If start is smaller than end, the gesture will zoom in, otherwise it
30 will zoom out.
31
32 """
33
34 finger_1_start = [center[0] - vector_start[0], center[1] - vector_start[1]]
35 finger_2_start = [center[0] + vector_start[0], center[1] + vector_start[1]]
36 finger_1_end = [center[0] - vector_end[0], center[1] - vector_end[1]]
37 finger_2_end = [center[0] + vector_end[0], center[1] + vector_end[1]]
38
39 dx = 1.0 * (finger_1_end[0] - finger_1_start[0]) / 100
40 dy = 1.0 * (finger_1_end[1] - finger_1_start[1]) / 100
41
42 finger_1 = Touch.create()
43 finger_2 = Touch.create()
44
45 finger_1.press(*finger_1_start)
46 finger_2.press(*finger_2_start)
47
48 finger_1_cur = [finger_1_start[0] + dx, finger_1_start[1] + dy]
49 finger_2_cur = [finger_2_start[0] - dx, finger_2_start[1] - dy]
50
51 for i in range(0, 100):
52 finger_1.move(*finger_1_cur)
53 finger_2.move(*finger_2_cur)
54 sleep(0.005)
55
56 finger_1_cur = [finger_1_cur[0] + dx, finger_1_cur[1] + dy]
57 finger_2_cur = [finger_2_cur[0] - dx, finger_2_cur[1] - dy]
58
59 finger_1.move(*finger_1_end)
60 finger_2.move(*finger_2_end)
61 finger_1.release()
62 finger_2.release()
063
=== removed file 'autopilot/glibrunner.py'
--- autopilot/glibrunner.py 2012-09-26 23:21:20 +0000
+++ autopilot/glibrunner.py 1970-01-01 00:00:00 +0000
@@ -1,26 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2012 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9from __future__ import absolute_import
10
11import testtools
12
13try:
14 import faulthandler
15 faulthandler.enable()
16except:
17 pass
18
19
20__all__ = [
21 'AutopilotTestRunner',
22 ]
23
24class AutopilotTestRunner(testtools.RunTest):
25 pass
26
270
=== modified file 'autopilot/globals.py'
--- autopilot/globals.py 2012-09-19 23:31:57 +0000
+++ autopilot/globals.py 2013-04-15 21:43:32 +0000
@@ -7,12 +7,16 @@
7# by the Free Software Foundation.7# by the Free Software Foundation.
88
9from __future__ import absolute_import9from __future__ import absolute_import
1010from StringIO import StringIO
11# this can be set to True, in which case tests will be recorded.11from autopilot.utilities import LogFormatter
12__video_recording_enabled = False12from testtools.content import text_content
1313import subprocess
14# this is where videos will be put after being encoded.14import os.path
15__video_record_directory = "/tmp/autopilot"15import logging
16from autopilot.utilities import addCleanup
17
18logger = logging.getLogger(__name__)
19
1620
17# if set to true, autopilot will output all pythong logging to stderr21# if set to true, autopilot will output all pythong logging to stderr
18__log_verbose = False22__log_verbose = False
@@ -23,12 +27,114 @@
23 return __log_verbose27 return __log_verbose
2428
2529
30class _TestLogger(object):
31 """A class that handles adding test logs as test result content."""
32
33 def __call__(self, test_instance):
34 self._setUpTestLogging(test_instance)
35 if get_log_verbose():
36 global logger
37 logger.info("*" * 60)
38 logger.info("Starting test %s", test_instance.shortDescription())
39
40 def _setUpTestLogging(self, test_instance):
41 self._log_buffer = StringIO()
42 root_logger = logging.getLogger()
43 root_logger.setLevel(logging.DEBUG)
44 formatter = LogFormatter()
45 self._log_handler = logging.StreamHandler(stream=self._log_buffer)
46 self._log_handler.setFormatter(formatter)
47 root_logger.addHandler(self._log_handler)
48 test_instance.addCleanup(self._tearDownLogging, test_instance)
49
50 def _tearDownLogging(self, test_instance):
51 root_logger = logging.getLogger()
52 self._log_handler.flush()
53 self._log_buffer.seek(0)
54 test_instance.addDetail('test-log', text_content(self._log_buffer.getvalue()))
55 root_logger.removeHandler(self._log_handler)
56 # Calling del to remove the handler and flush the buffer. We are
57 # abusing the log handlers here a little.
58 del self._log_buffer
59
60
26def set_log_verbose(verbose):61def set_log_verbose(verbose):
27 """Set whether or not we should log verbosely."""62 """Set whether or not we should log verbosely."""
63
28 if type(verbose) is not bool:64 if type(verbose) is not bool:
29 raise TypeError("Verbose flag must be a boolean.")65 raise TypeError("Verbose flag must be a boolean.")
30 global __log_verbose66 global __log_verbose
31 __log_verbose = verbose67 __log_verbose = verbose
68 if verbose:
69 logger = _TestLogger()
70 _on_test_started_call.append(logger)
71
72
73class _VideoCaptureTestCase(object):
74 """Video capture autopilot tests, saving the results if the test failed."""
75
76 _recording_app = '/usr/bin/recordmydesktop'
77 _recording_opts = ['--no-sound', '--no-frame', '-o',]
78
79 def __init__(self, recording_directory):
80 self.recording_directory = recording_directory
81
82 def __call__(self, test_instance):
83 if not self._have_recording_app():
84 logger.warning("Disabling video capture since '%s' is not present", self._recording_app)
85
86 self._test_passed = True
87 test_instance.addOnException(self._on_test_failed)
88 test_instance.addCleanup(self._stop_video_capture, test_instance)
89 self._start_video_capture(test_instance.shortDescription())
90
91 def _have_recording_app(self):
92 return os.path.exists(self._recording_app)
93
94 def _start_video_capture(self, test_id):
95 args = self._get_capture_command_line()
96 self._capture_file = os.path.join(
97 self.recording_directory,
98 '%s.ogv' % (test_id)
99 )
100 self._ensure_directory_exists_but_not_file(self._capture_file)
101 args.append(self._capture_file)
102 logger.debug("Starting: %r", args)
103 self._capture_process = subprocess.Popen(
104 args,
105 stdout=subprocess.PIPE,
106 stderr=subprocess.STDOUT
107 )
108
109 def _stop_video_capture(self, test_instance):
110 """Stop the video capture. If the test failed, save the resulting file."""
111
112 if self._test_passed:
113 # We use kill here because we don't want the recording app to start
114 # encoding the video file (since we're removing it anyway.)
115 self._capture_process.kill()
116 self._capture_process.wait()
117 else:
118 self._capture_process.terminate()
119 self._capture_process.wait()
120 if self._capture_process.returncode != 0:
121 test_instance.addDetail('video capture log', text_content(self._capture_process.stdout.read()))
122 self._capture_process = None
123
124 def _get_capture_command_line(self):
125 return [self._recording_app] + self._recording_opts
126
127 def _ensure_directory_exists_but_not_file(self, file_path):
128 dirpath = os.path.dirname(file_path)
129 if not os.path.exists(dirpath):
130 os.makedirs(dirpath)
131 elif os.path.exists(file_path):
132 logger.warning("Video capture file '%s' already exists, deleting.", file_path)
133 os.remove(file_path)
134
135 def _on_test_failed(self, ex_info):
136 """Called when a test fails."""
137 self._test_passed = False
32138
33139
34def configure_video_recording(enable_recording, record_dir):140def configure_video_recording(enable_recording, record_dir):
@@ -43,18 +149,14 @@
43 if not isinstance(record_dir, basestring):149 if not isinstance(record_dir, basestring):
44 raise TypeError("record_dir must be a string.")150 raise TypeError("record_dir must be a string.")
45151
46 global __video_recording_enabled152 if enable_recording:
47 global __video_record_directory153 recorder = _VideoCaptureTestCase(record_dir)
48154 _on_test_started_call.append(recorder)
49 __video_recording_enabled = enable_recording155
50 __video_record_directory = record_dir156
51157_on_test_started_call = [addCleanup.set_test_instance]
52158
53def get_video_recording_enabled():159def on_test_started(test_case_instance):
54 global __video_recording_enabled160 global _on_test_started_call
55 return __video_recording_enabled161 for fun in _on_test_started_call:
56162 fun(test_case_instance)
57
58def get_video_record_directory():
59 global __video_record_directory
60 return __video_record_directory
61163
=== renamed file 'autopilot/emulators/ibus.py' => 'autopilot/ibus.py'
=== renamed directory 'autopilot/emulators/input' => 'autopilot/input'
=== modified file 'autopilot/input/_X11.py'
--- autopilot/emulators/input/_X11.py 2013-02-28 03:18:15 +0000
+++ autopilot/input/_X11.py 2013-04-15 21:43:32 +0000
@@ -19,13 +19,11 @@
19from __future__ import absolute_import19from __future__ import absolute_import
2020
21import logging21import logging
22import os
23import subprocess
24from time import sleep22from time import sleep
2523
26from autopilot.emulators.bamf import BamfWindow24from autopilot.display import is_point_on_any_screen, move_mouse_to_screen
27from autopilot.utilities import Silence25from autopilot.utilities import Silence
28from autopilot.emulators.input import (26from autopilot.input import (
29 Keyboard as KeyboardBase,27 Keyboard as KeyboardBase,
30 Mouse as MouseBase,28 Mouse as MouseBase,
31 )29 )
@@ -321,7 +319,7 @@
321319
322 dest_x, dest_y = x, y320 dest_x, dest_y = x, y
323 curr_x, curr_y = self.position()321 curr_x, curr_y = self.position()
324 coordinate_valid = ScreenGeometry().is_point_on_any_monitor((x,y))322 coordinate_valid = is_point_on_any_screen((x,y))
325323
326 while curr_x != dest_x or curr_y != dest_y:324 while curr_x != dest_x or curr_y != dest_y:
327 dx = abs(dest_x - curr_x)325 dx = abs(dest_x - curr_x)
@@ -416,145 +414,4 @@
416 logger.debug("Releasing mouse button %d as part of cleanup", btn)414 logger.debug("Releasing mouse button %d as part of cleanup", btn)
417 fake_input(get_display(), X.ButtonRelease, btn)415 fake_input(get_display(), X.ButtonRelease, btn)
418 _PRESSED_MOUSE_BUTTONS = []416 _PRESSED_MOUSE_BUTTONS = []
419 sg = ScreenGeometry()417 move_mouse_to_screen(0)
420 sg.move_mouse_to_monitor(0)
421
422
423class ScreenGeometry:
424 """Get details about screen geometry."""
425
426 class BlacklistedDriverError(RuntimeError):
427 """Cannot set primary monitor when running drivers listed in the driver blacklist."""
428
429 def __init__(self):
430 # Note: MUST import these here, rather than at the top of the file. Why?
431 # Because sphinx imports these modules to build the API documentation,
432 # which in turn tries to import Gdk, which in turn fails because there's
433 # no DISPlAY environment set in the package builder.
434 from gi.repository import Gdk
435 self._default_screen = Gdk.Screen.get_default()
436 self._blacklisted_drivers = ["NVIDIA"]
437
438 def get_num_monitors(self):
439 """Get the number of monitors attached to the PC."""
440 return self._default_screen.get_n_monitors()
441
442 def get_primary_monitor(self):
443 return self._default_screen.get_primary_monitor()
444
445 def set_primary_monitor(self, monitor):
446 """Set *monitor* to be the primary monitor.
447
448 :param int monitor: Must be between 0 and the number of configured
449 monitors.
450 :raises: **ValueError** if an invalid monitor is specified.
451 :raises: **BlacklistedDriverError** if your video driver does not
452 support this.
453
454 """
455 try:
456 glxinfo_out = subprocess.check_output("glxinfo")
457 except OSError, e:
458 raise OSError("Failed to run glxinfo: %s. (do you have mesa-utils installed?)" % e)
459
460 for dri in self._blacklisted_drivers:
461 if dri in glxinfo_out:
462 raise ScreenGeometry.BlacklistedDriverError('Impossible change the primary monitor for the given driver')
463
464 if monitor < 0 or monitor >= self.get_num_monitors():
465 raise ValueError('Monitor %d is not in valid range of 0 <= monitor < %d.' % (self.get_num_monitors()))
466
467 monitor_name = self._default_screen.get_monitor_plug_name(monitor)
468
469 if not monitor_name:
470 raise ValueError('Could not get monitor name from monitor number %d.' % (monitor))
471
472 ret = os.spawnlp(os.P_WAIT, "xrandr", "xrandr", "--output", monitor_name, "--primary")
473
474 if ret != 0:
475 raise RuntimeError('Xrandr can\'t set the primary monitor. error code: %d' % (ret))
476
477 def get_screen_width(self):
478 return self._default_screen.get_width()
479
480 def get_screen_height(self):
481 return self._default_screen.get_height()
482
483 def get_monitor_geometry(self, monitor_number):
484 """Get the geometry for a particular monitor.
485
486 :return: Tuple containing (x, y, width, height).
487
488 """
489 if monitor_number < 0 or monitor_number >= self.get_num_monitors():
490 raise ValueError('Specified monitor number is out of range.')
491 rect = self._default_screen.get_monitor_geometry(monitor_number)
492 return (rect.x, rect.y, rect.width, rect.height)
493
494 def is_rect_on_monitor(self, monitor_number, rect):
495 """Returns True if *rect* is **entirely** on the specified monitor, with no overlap."""
496
497 if type(rect) is not tuple or len(rect) != 4:
498 raise TypeError("rect must be a tuple of 4 int elements.")
499
500 (x, y, w, h) = rect
501 (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number)
502 return (x >= mx and x + w <= mx + mw and y >= my and y + h <= my + mh)
503
504 def is_point_on_monitor(self, monitor_number, point):
505 """Returns True if *point* is on the specified monitor.
506
507 *point* must be an iterable type with two elements: (x, y)
508
509 """
510 x,y = point
511 (mx, my, mw, mh) = self.get_monitor_geometry(monitor_number)
512 return (x >= mx and x < mx + mw and y >= my and y < my + mh)
513
514 def is_point_on_any_monitor(self, point):
515 """Returns true if *point* is on any currently configured monitor."""
516 return any([self.is_point_on_monitor(m, point) for m in range(self.get_num_monitors())])
517
518 def move_mouse_to_monitor(self, monitor_number):
519 """Move the mouse to the center of the specified monitor."""
520 geo = self.get_monitor_geometry(monitor_number)
521 x = geo[0] + (geo[2] / 2)
522 y = geo[1] + (geo[3] / 2)
523 #dont animate this or it might not get there due to barriers
524 Mouse().move(x, y, False)
525
526 def drag_window_to_monitor(self, window, monitor):
527 """Drags *window* to *monitor*
528
529 :param BamfWindow window: The window to drag
530 :param integer monitor: The monitor to drag the *window* to
531 :raises: **TypeError** if *window* is not a BamfWindow
532
533 """
534 if not isinstance(window, BamfWindow):
535 raise TypeError("Window must be a BamfWindow")
536
537 if window.monitor == monitor:
538 logger.debug("Window %r is already on monitor %d." % (window.x_id, monitor))
539 return
540
541 assert(not window.is_maximized)
542 (win_x, win_y, win_w, win_h) = window.geometry
543 (mx, my, mw, mh) = self.get_monitor_geometry(monitor)
544
545 logger.debug("Dragging window %r to monitor %d." % (window.x_id, monitor))
546
547 mouse = Mouse()
548 keyboard = Keyboard()
549 mouse.move(win_x + win_w/2, win_y + win_h/2)
550 keyboard.press("Alt")
551 mouse.press()
552 keyboard.release("Alt")
553
554 # We do the movements in two steps, to reduce the risk of being
555 # blocked by the pointer barrier
556 target_x = mx + mw/2
557 target_y = my + mh/2
558 mouse.move(win_x, target_y, rate=20, time_between_events=0.005)
559 mouse.move(target_x, target_y, rate=20, time_between_events=0.005)
560 mouse.release()
561418
=== modified file 'autopilot/input/__init__.py'
--- autopilot/emulators/input/__init__.py 2013-02-28 03:18:15 +0000
+++ autopilot/input/__init__.py 2013-04-15 21:43:32 +0000
@@ -6,99 +6,81 @@
6# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.7# by the Free Software Foundation.
88
9from collections import OrderedDict9"""
10from autopilot.utilities import get_debug_logger10Autopilot unified input system.
1111===============================
12"""Autopilot unified input system.
1312
14This package provides input methods for various platforms. Autopilot aims to13This package provides input methods for various platforms. Autopilot aims to
15provide an appropriate implementation for the currently running system. For14provide an appropriate implementation for the currently running system. For
16example, not all systems have an X11 stack running: on those systems, autopilot15example, not all systems have an X11 stack running: on those systems, autopilot
17will instantiate a Keyboard class that uses something other than X11 to generate16will instantiate input classes class that use something other than X11 to generate
18key events (possibly UInput).17events (possibly UInput).
1918
20Test authors are encouraged to instantiate the input devices they need for their19Test authors should instantiate the appropriate class using the ``create`` method
21tests using the get_keyboard and get_mouse methods directly. In the case where20on each class. Tests can provide a hint to this method to suggest that a particular
22these methods don't do the right thing, authors may access the underlying input21subsystem be used. However, autopilot will prefer to return a subsystem other than
23systems directly. However, these are not documented, and are liable to change22the one specified, if the requested subsystem is unavailable.
24without notice.23
24There are three basic input types available:
25
26 * :class:`Keyboard` - traditional keyboard devices.
27 * :class:`Mouse` - traditional mouse devices.
28 * :class:`Touch` - single point-of-contact touch device.
29 * For multitouch capabilities, see the :mod:`autopilot.gestures` module.
2530
26"""31"""
2732
2833from collections import OrderedDict
29def get_keyboard(preferred_variant=""):34from autopilot.utilities import _pick_variant
30 """Get an instance of the Keyboard class.35
31
32 If variant is specified, it should be a string that specifies a backend to
33 use. However, this hint can be ignored - autopilot will prefer to return a
34 keyboard variant other than the one requested, rather than fail to return
35 anything at all.
36
37 If autopilot cannot instantate any of the possible backends, a RuntimeError
38 will be raised.
39 """
40 def get_x11_kb():
41 from autopilot.emulators.input._X11 import Keyboard
42 return Keyboard()
43 def get_uinput_kb():
44 from autopilot.emulators.input._uinput import Keyboard
45 return Keyboard()
46
47 variants = OrderedDict()
48 variants['X11'] = get_x11_kb
49 variants['UInput'] = get_uinput_kb
50 return _pick_variant(variants, preferred_variant)
51
52
53def get_mouse(preferred_variant=""):
54 """Get an instance of the Mouse class.
55
56 If variant is specified, it should be a string that specifies a backend to
57 use. However, this hint can be ignored - autopilot will prefer to return a
58 mouse variant other than the one requested, rather than fail to return
59 anything at all.
60
61 If autopilot cannot instantate any of the possible backends, a RuntimeError
62 will be raised.
63 """
64 def get_x11_mouse():
65 from autopilot.emulators.input._X11 import Mouse
66 return Mouse()
67
68 variants = OrderedDict()
69 variants['X11'] = get_x11_mouse
70 return _pick_variant(variants, preferred_variant)
71
72
73def _pick_variant(variants, preferred_variant):
74 possible_backends = variants.keys()
75 get_debug_logger().debug("Possible keyboard variants: %s", ','.join(possible_backends))
76 if preferred_variant in possible_backends:
77 possible_backends.sort(lambda a,b: -1 if a == preferred_variant else 0)
78 failure_reasons = []
79 for be in possible_backends:
80 try:
81 return variants[be]()
82 except Exception as e:
83 get_debug_logger().warning("Can't create keyboard variant %s: %r", be, e)
84 failure_reasons.append('%s: %r' % (be, e))
85 raise RuntimeError("Unable to instantiate any Keyboard backends\n%s" % '\n'.join(failure_reasons))
8636
8737
88class Keyboard(object):38class Keyboard(object):
8939
90 """A base class for all keyboard-type devices."""40 """A simple keyboard device class.
41
42 The keyboard class is used to generate key events while in an autopilot
43 test. This class should not be instantiated directly however. To get an
44 instance of the keyboard class, call :py:meth:`create` instead.
45
46 """
47
48 @staticmethod
49 def create(preferred_variant=''):
50 """Get an instance of the :py:class:`Keyboard` class.
51
52 :param preferred_variant: A string containing a hint as to which variant you
53 would like. However, this hint can be ignored - autopilot will prefer to
54 return a keyboard variant other than the one requested, rather than fail
55 to return anything at all.
56 :raises: a RuntimeError will be raised if autopilot cannot instantate any of
57 the possible backends.
58
59 """
60 def get_x11_kb():
61 from autopilot.input._X11 import Keyboard
62 return Keyboard()
63 def get_uinput_kb():
64 from autopilot.input._uinput import Keyboard
65 return Keyboard()
66
67 variants = OrderedDict()
68 variants['X11'] = get_x11_kb
69 variants['UInput'] = get_uinput_kb
70 return _pick_variant(variants, preferred_variant)
9171
92 def press(self, keys, delay=0.2):72 def press(self, keys, delay=0.2):
93 """Send key press events only.73 """Send key press events only.
9474
95 :param string keys: Keys you want pressed.75 :param keys: Keys you want pressed.
76 :param delay: The delay (in Seconds) after pressing the keys before
77 returning control to the caller.
9678
97 Example:79 Example:
9880
99 >>> press('Alt+F2')81 >>> press('Alt+F2')
10082
101 presses the 'Alt' and 'F2' keys.83 presses the 'Alt' and 'F2' keys, but does not release them.
10284
103 """85 """
104 raise NotImplementedError("You cannot use this class directly.")86 raise NotImplementedError("You cannot use this class directly.")
@@ -106,7 +88,9 @@
106 def release(self, keys, delay=0.2):88 def release(self, keys, delay=0.2):
107 """Send key release events only.89 """Send key release events only.
10890
109 :param string keys: Keys you want released.91 :param keys: Keys you want released.
92 :param delay: The delay (in Seconds) after releasing the keys before
93 returning control to the caller.
11094
111 Example:95 Example:
11296
@@ -122,7 +106,9 @@
122106
123 This is the same as calling 'press(keys);release(keys)'.107 This is the same as calling 'press(keys);release(keys)'.
124108
125 :param string keys: Keys you want pressed and released.109 :param keys: Keys you want pressed and released.
110 :param delay: The delay (in Seconds) after pressing and releasing each
111 key.
126112
127 Example:113 Example:
128114
@@ -137,6 +123,11 @@
137 def type(self, string, delay=0.1):123 def type(self, string, delay=0.1):
138 """Simulate a user typing a string of text.124 """Simulate a user typing a string of text.
139125
126 :param string: The string to text to type.
127 :param delay: The delay (in Seconds) after pressing and releasing each
128 key. Note that the default value here is shorter than for the press,
129 release and press_and_release methods.
130
140 .. note:: Only 'normal' keys can be typed with this method. Control131 .. note:: Only 'normal' keys can be typed with this method. Control
141 characters (such as 'Alt' will be interpreted as an 'A', and 'l',132 characters (such as 'Alt' will be interpreted as an 'A', and 'l',
142 and a 't').133 and a 't').
@@ -156,7 +147,40 @@
156147
157148
158class Mouse(object):149class Mouse(object):
159 """A base class for all mouse-type classes."""150
151 """A simple mouse device class.
152
153 The mouse class is used to generate mouse events while in an autopilot
154 test. This class should not be instantiated directly however. To get an
155 instance of the mouse class, call :py:meth:`create` instead.
156
157 For example, to create a mouse object and click at (100,50):
158
159 >>> mouse = autopilot.input.Mouse.create()
160 >>> mouse.move(100, 50)
161 >>> mouse.click()
162
163 """
164
165 @staticmethod
166 def create(preferred_variant=''):
167 """Get an instance of the :py:class:`Mouse` class.
168
169 :param preferred_variant: A string containing a hint as to which variant you
170 would like. However, this hint can be ignored - autopilot will prefer to
171 return a mouse variant other than the one requested, rather than fail
172 to return anything at all.
173 :raises: a RuntimeError will be raised if autopilot cannot instantate any of
174 the possible backends.
175
176 """
177 def get_x11_mouse():
178 from autopilot.input._X11 import Mouse
179 return Mouse()
180
181 variants = OrderedDict()
182 variants['X11'] = get_x11_mouse
183 return _pick_variant(variants, preferred_variant)
160184
161 @property185 @property
162 def x(self):186 def x(self):
@@ -222,3 +246,73 @@
222 def cleanup():246 def cleanup():
223 """Put mouse in a known safe state."""247 """Put mouse in a known safe state."""
224 raise NotImplementedError("You cannot use this class directly.")248 raise NotImplementedError("You cannot use this class directly.")
249
250
251class Touch(object):
252 """A simple touch driver class.
253
254 This class can be used for any touch events that require a single active
255 touch at once. If you want to do complex gestures (including multi-touch
256 gestures), look at the :py:mod:`autopilot.gestures` module.
257
258 """
259
260 @staticmethod
261 def create(preferred_variant=''):
262 """Get an instance of the :py:class:`Touch` class.
263
264 :param preferred_variant: A string containing a hint as to which variant you
265 would like. However, this hint can be ignored - autopilot will prefer to
266 return a touch variant other than the one requested, rather than fail
267 to return anything at all.
268 :raises: a RuntimeError will be raised if autopilot cannot instantate any of
269 the possible backends.
270
271 """
272 def get_uinput_touch():
273 from autopilot.input._uinput import Touch
274 return Touch()
275
276 variants = OrderedDict()
277 variants['UInput'] = get_uinput_touch
278 return _pick_variant(variants, preferred_variant)
279
280 @property
281 def pressed(self):
282 """Return True if this touch is currently in use (i.e.- pressed on the
283 'screen').
284
285 """
286 raise NotImplementedError("You cannot use this class directly.")
287
288 def tap(self, x, y):
289 """Click (or 'tap') at given x and y coordinates."""
290 raise NotImplementedError("You cannot use this class directly.")
291
292 def tap_object(self, object):
293 """Tap the center point of a given object.
294
295 It does this by looking for several attributes, in order. The first
296 attribute found will be used. The attributes used are (in order):
297
298 * globalRect (x,y,w,h)
299 * center_x, center_y
300 * x, y, w, h
301
302 :raises: **ValueError** if none of these attributes are found, or if an
303 attribute is of an incorrect type.
304
305 """
306 raise NotImplementedError("You cannot use this class directly.")
307
308 def press(self, x, y):
309 """Press and hold."""
310 raise NotImplementedError("You cannot use this class directly.")
311
312 def release(self):
313 """Release a previously pressed finger"""
314 raise NotImplementedError("You cannot use this class directly.")
315
316 def drag(self, x1, y1, x2, y2):
317 """Perform a drag gesture from (x1,y1) to (x2,y2)"""
318 raise NotImplementedError("You cannot use this class directly.")
225319
=== added file 'autopilot/input/_common.py'
--- autopilot/input/_common.py 1970-01-01 00:00:00 +0000
+++ autopilot/input/_common.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,46 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9"""Common, private utility code for input emulators."""
10
11import logging
12
13logger = logging.getLogger(__name__)
14
15
16def get_center_point(object_proxy):
17 """Get the center point of an object, searching for several different ways
18 of determining exactly where the center is.
19
20 """
21 try:
22 x,y,w,h = object_proxy.globalRect
23 logger.debug("Moving to object's globalRect coordinates.")
24 return x+w/2, y+h/2
25 except AttributeError:
26 pass
27 except (TypeError, ValueError):
28 raise ValueError("Object '%r' has globalRect attribute, but it is not of the correct type" % object_proxy)
29
30 try:
31 x,y = object_proxy.center_x, object_proxy.center_y
32 logger.debug("Moving to object's center_x, center_y coordinates.")
33 return x,y
34 except AttributeError:
35 pass
36 except (TypeError, ValueError):
37 raise ValueError("Object '%r' has center_x, center_y attributes, but they are not of the correct type" % object_proxy)
38
39 try:
40 x,y,w,h = object_proxy.x, object_proxy.y, object_proxy.w, object_proxy.h
41 logger.debug("Moving to object's center point calculated from x,y,w,h attributes.")
42 return x+w/2,y+h/2
43 except AttributeError:
44 raise ValueError("Object '%r' does not have any recognised position attributes" % object_proxy)
45 except (TypeError, ValueError):
46 raise ValueError("Object '%r' has x,y attribute, but they are not of the correct type" % object_proxy)
047
=== modified file 'autopilot/input/_uinput.py'
--- autopilot/emulators/input/_uinput.py 2013-02-28 01:00:50 +0000
+++ autopilot/input/_uinput.py 2013-04-15 21:43:32 +0000
@@ -9,11 +9,14 @@
99
10"""UInput device drivers."""10"""UInput device drivers."""
1111
12from autopilot.emulators.input import Keyboard as KeyboardBase12from autopilot.input import Keyboard as KeyboardBase
13from autopilot.input import Touch as TouchBase
14from autopilot.input._common import get_center_point
15import autopilot.platform
16
13import logging17import logging
14from time import sleep18from time import sleep
15from evdev import AbsData, InputDevice, UInput, ecodes as e19from evdev import AbsData, UInput, ecodes as e
16import os.path
1720
18logger = logging.getLogger(__name__)21logger = logging.getLogger(__name__)
1922
@@ -22,6 +25,7 @@
2225
23PRESSED_KEYS = []26PRESSED_KEYS = []
2427
28
25class Keyboard(KeyboardBase):29class Keyboard(KeyboardBase):
2630
27 def __init__(self):31 def __init__(self):
@@ -46,7 +50,7 @@
46 raise TypeError("'keys' argument must be a string.")50 raise TypeError("'keys' argument must be a string.")
4751
48 for key in keys.split('+'):52 for key in keys.split('+'):
49 for event in _get_events_for_key(key):53 for event in Keyboard._get_events_for_key(key):
50 self._emit(event, PRESS)54 self._emit(event, PRESS)
51 sleep(delay)55 sleep(delay)
5256
@@ -69,7 +73,7 @@
69 # # release keys in the reverse order they were pressed in.73 # # release keys in the reverse order they were pressed in.
70 # keys = self.__translate_keys(keys)74 # keys = self.__translate_keys(keys)
71 for key in reversed(keys.split('+')):75 for key in reversed(keys.split('+')):
72 for event in _get_events_for_key(key):76 for event in Keyboard._get_events_for_key(key):
73 self._emit(event, RELEASE)77 self._emit(event, RELEASE)
74 sleep(delay)78 sleep(delay)
7579
@@ -118,78 +122,58 @@
118 # fake_input(get_display(), X.KeyRelease, keycode)122 # fake_input(get_display(), X.KeyRelease, keycode)
119 # _PRESSED_KEYS = []123 # _PRESSED_KEYS = []
120124
125 @staticmethod
126 def _get_events_for_key(key):
127 """Return a list of events required to generate 'key' as an input.
128
129 Multiple keys will be returned when the key specified requires more than one
130 keypress to generate (for example, upper-case letters).
131
132 """
133 events = []
134 if key.isupper():
135 events.append(e.KEY_LEFTSHIFT)
136 keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
137 evt = getattr(e, 'KEY_' + keyname.upper(), None)
138 if evt is None:
139 raise ValueError("Unknown key name: '%s'" % key)
140 events.append(evt)
141 return events
142
143
121last_tracking_id = 0144last_tracking_id = 0
122def get_next_tracking_id():145def get_next_tracking_id():
123 global last_tracking_id146 global last_tracking_id
124 last_tracking_id += 1147 last_tracking_id += 1
125 return last_tracking_id148 return last_tracking_id
126149
150
127def create_touch_device(res_x=None, res_y=None):151def create_touch_device(res_x=None, res_y=None):
128 """Create and return a UInput touch device.152 """Create and return a UInput touch device.
129153
130 If res_x and res_y are not specified, they will be queried from X11.154 If res_x and res_y are not specified, they will be queried from the system.
131155
132 The following needs to go into /system/usr/idc/autopilot-finger.idc
133
134 # Copyright (C) 2011 The Android Open Source Project
135 #
136 # Licensed under the Apache License, Version 2.0 (the "License");
137 # you may not use this file except in compliance with the License.
138 # You may obtain a copy of the License at
139 #
140 # http://www.apache.org/licenses/LICENSE-2.0
141 #
142 # Unless required by applicable law or agreed to in writing, software
143 # distributed under the License is distributed on an "AS IS" BASIS,
144 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
145 # See the License for the specific language governing permissions and
146 # limitations under the License.
147
148 #
149 # Input Device Calibration File for the Tuna touch screen.
150 #
151
152 device.internal = 1
153
154 # Basic Parameters
155 touch.deviceType = touchScreen
156 touch.orientationAware = 1
157
158 # Size
159 touch.size.calibration = diameter
160 touch.size.scale = 10
161 touch.size.bias = 0
162 touch.size.isSummed = 0
163
164 # Pressure
165 # Driver reports signal strength as pressure.
166 #
167 # A normal thumb touch typically registers about 200 signal strength
168 # units although we don't expect these values to be accurate.
169 touch.pressure.calibration = amplitude
170 touch.pressure.scale = 0.005
171
172 # Orientation
173 touch.orientation.calibration = none
174 """156 """
175157
176 # FIXME: remove the harcoded values and determine ScreenGeometry without X11
177 res_x = 720
178 res_y = 1280
179
180 if res_x is None or res_y is None:158 if res_x is None or res_y is None:
181 from autopilot.emulators.X11 import ScreenGeometry159 from autopilot.display import Display
182 sg = ScreenGeometry()160 display = Display.create()
183 res_x = sg.get_screen_width()161 res_x = display.get_screen_width()
184 res_y = sg.get_screen_height()162 res_y = display.get_screen_height()
163
164 # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no
165 # idea why...
166 touch_tool = e.BTN_TOOL_FINGER
167 if autopilot.platform.model() == 'Desktop':
168 touch_tool = e.BTN_TOUCH
185169
186 cap_mt = {170 cap_mt = {
187 e.EV_ABS : [171 e.EV_ABS : [
188 (e.ABS_X, AbsData(0, res_x, 0, 0)),172 (e.ABS_X, AbsData(0, res_x, 0, 0)),
189 (e.ABS_Y, AbsData(0, res_y, 0, 0)),173 (e.ABS_Y, AbsData(0, res_y, 0, 0)),
190 (e.ABS_PRESSURE, AbsData(0, 65535, 0, 0)),174 (e.ABS_PRESSURE, AbsData(0, 65535, 0, 0)),
191 (e.ABS_DISTANCE, AbsData(0, 65535, 0, 0)),175 # (e.ABS_DISTANCE, AbsData(0, 65535, 0, 0)),
192 (e.ABS_TOOL_WIDTH, AbsData(0, 65535, 0, 0)),176 # (e.ABS_TOOL_WIDTH, AbsData(0, 65535, 0, 0)),
193 (e.ABS_MT_POSITION_X, AbsData(0, res_x, 0, 0)),177 (e.ABS_MT_POSITION_X, AbsData(0, res_x, 0, 0)),
194 (e.ABS_MT_POSITION_Y, AbsData(0, res_y, 0, 0)),178 (e.ABS_MT_POSITION_Y, AbsData(0, res_y, 0, 0)),
195 (e.ABS_MT_TOUCH_MAJOR, AbsData(0, 30, 0, 0)),179 (e.ABS_MT_TOUCH_MAJOR, AbsData(0, 30, 0, 0)),
@@ -198,147 +182,122 @@
198 (e.ABS_MT_SLOT, (0, 9, 0, 0)),182 (e.ABS_MT_SLOT, (0, 9, 0, 0)),
199 ],183 ],
200 e.EV_KEY: [184 e.EV_KEY: [
201 e.BTN_TOOL_FINGER,185 touch_tool,
202 ]186 ]
203 }187 }
204188
205 try:189 return UInput(cap_mt, name='autopilot-finger', version=0x2)
206 device = UInput(cap_mt, name='autopilot-finger', version=0x2)190
207 return device191_touch_device = create_touch_device()
208 except:192
209 logger.warning("Failed to open uinput device. Finger will not be available")193# Multiouch notes:
210 return None194# ----------------
211195
212196# We're simulating a class of device that can track multiple touches, and keep
213def find_touch_device():197# them separate. This is how most modern track devices work anyway. The device
214 """Return a list of touch devices found on the local machine.198# is created with a capability to track a certain number of distinct touches at
215199# once. This is the ABS_MT_SLOT capability. Since our target device can track 9
216 :returns: A list of evdev.inputDevice objects, possibly an empty list.200# separate touches, we'll do the same.
217201
218 """202# Each finger contact starts by registering a slot number (0-8) with a tracking
219 def device_cmp(dev_a, dev_b):203# Id. The Id should be unique for this touch - this can be an auto-inctrementing
220 def get_track_slot(dev):204# integer. The very first packets to tell the kernel that we have a touch happening
221 capabilities = dev.capabilities()205# should look like this:
222 for track_feature in capabilities[e.EV_ABS]:206
223 if track_feature[0] == e.ABS_MT_SLOT:207# ABS_MT_SLOT 0
224 return track_feature[1][1]208# ABS_MT_TRACKING_ID 45
225 return get_track_slot(dev_a) < get_track_slot(dev_b)209# ABS_MT_POSITION_X x[0]
226210# ABS_MT_POSITION_Y y[0]
227 i = 0211
228 touch_devices = []212# This associates Tracking id 45 (could be any number) with slot 0. Slot 0 can now
229 while True:213# not be use by any other touch until it is released.
230 path = '/dev/input/event%d' % (i)214
231 if not os.path.exists(path):215# If we want to move this contact's coordinates, we do this:
232 break216
233217# ABS_MT_SLOT 0
234 dev = InputDevice(path)218# ABS_MT_POSITION_X 123
235 capabilities = dev.capabilities()219# ABS_MT_POSITION_Y 234
236 if e.EV_ABS in capabilities:220
237 touch_devices.append(dev)221# Technically, the 'SLOT 0' part isn't needed, since we're already in slot 0, but
238 i += 1222# it doesn't hurt to have it there.
239 touch_devices.sort(device_cmp)223
240 return touch_devices224# To lift the contact, we simply specify a tracking Id of -1:
241225
242226# ABS_MT_SLOT 0
243"""227# ABS_MT_TRACKING_ID -1
244Multiouch notes:228
245----------------229# The initial association between slot and tracking Id is made when the 'finger'
246230# first makes contact with the device (well, not technically true, but close
247We're simulating a class of device that can track multiple touches, and keep231# enough). Multiple touches can be active simultaniously, as long as they all have
248them separate. This is how most modern track devices work anyway. The device232# unique slots, and tracking Ids. The simplest way to think about this is that the
249is created with a capability to track a certain number of distinct touches at233# SLOT refers to a finger number, and the TRACKING_ID identifies a unique touch
250once. This is the ABS_MT_SLOT capability. Since our target device can track 9234# for the duration of it's existance.
251separate touches, we'll do the same.235
252236_touch_fingers_in_use = []
253Each finger contact starts by registering a slot number (0-8) with a tracking237def _get_touch_finger():
254Id. The Id should be unique for this touch - this can be an auto-inctrementing238 """Claim a touch finger id for use.
255integer. The very first packets to tell the kernel that we have a touch happening239
256should look like this:240 :raises: RuntimeError if no more fingers are available.
257241
258 ABS_MT_SLOT 0242 """
259 ABS_MT_TRACKING_ID 45243 global _touch_fingers_in_use
260 ABS_MT_POSITION_X x[0]244
261 ABS_MT_POSITION_Y y[0]245 for i in range(9):
262246 if i not in _touch_fingers_in_use:
263This associates Tracking id 45 (could be any number) with slot 0. Slot 0 can now247 _touch_fingers_in_use.append(i)
264not be use by any other touch until it is released.248 return i
265249 raise RuntimeError("All available fingers have been used already.")
266If we want to move this contact's coordinates, we do this:250
267251def _release_touch_finger(finger_num):
268 ABS_MT_SLOT 0252 """Relase a previously-claimed finger id.
269 ABS_MT_POSITION_X 123253
270 ABS_MT_POSITION_Y 234254 :raises: RuntimeError if the finger given was never claimed, or was already
271255 released.
272Technically, the 'SLOT 0' part isn't needed, since we're already in slot 0, but256
273it doesn't hurt to have it there.257 """
274258 global _touch_fingers_in_use
275To lift the contact, we simply specify a tracking Id of -1:259
276260 if finger_num not in _touch_fingers_in_use:
277 ABS_MT_SLOT 0261 raise RuntimeError("Finger %d was never claimed, or has already been released." % (finger_num))
278 ABS_MT_TRACKING_ID -1262 _touch_fingers_in_use.remove(finger_num)
279263 assert(finger_num not in _touch_fingers_in_use)
280The initial association between slot and tracking Id is made when the 'finger'264
281first makes contact with the device (well, not technically true, but close265
282enough). Multiple touches can be active simultaniously, as long as they all have266class Touch(TouchBase):
283unique slots, and tracking Ids. The simplest way to think about this is that the
284SLOT refers to a finger number, and the TRACKING_ID identifies a unique touch
285for the duration of it's existance.
286
287"""
288
289class Finger(object):
290 """Low level interface to generate single finger touch events."""267 """Low level interface to generate single finger touch events."""
291268
292 def __init__(self):269 def __init__(self):
293 self.device = create_touch_device()270 super(TouchBase, self).__init__()
294 if self.device == None:271 self._touch_finger = None
295 raise UInputError272
296 self.current_x = 0273 @property
297 self.current_y = 0274 def pressed(self):
298275 return self._touch_finger is not None
299 def move(self, x, y):
300 """This is a convenience function to keep API compatibility with other pointer input methods"""
301 self.current_x = x
302 self.current_y = y
303
304 def move_to_object(self, object):
305 """This is a convenience function to keep API compatibility with other pointer input methods"""
306 self.current_x = object.globalRect[0] + object.globalRect[2] / 2
307 self.current_y = object.globalRect[1] + object.globalRect[3] / 2
308
309 def click(self):
310 self.tap(self.current_x, self.current_y)
311276
312 def tap(self, x, y):277 def tap(self, x, y):
313 """Click (or 'tap') at given x and y coordinates."""278 """Click (or 'tap') at given x and y coordinates."""
314 self._finger_down(0, x, y)279 self._finger_down(x, y)
315 sleep(0.1)280 sleep(0.1)
316 self._finger_up(0)281 self._finger_up()
317282
318 def tap_object(self, object):283 def tap_object(self, object):
319 """Click (or 'tap') a given object"""284 """Click (or 'tap') a given object"""
320 self.tap(object.globalRect[0] + object.globalRect[2] / 2, object.globalRect[1] + object.globalRect[3] / 2)285 x,y = get_center_point(object)
286 self.tap(x,y)
321287
322 def press(self, *args):288 def press(self, x, y):
323 """Press and hold a given object or at the given coordinates289 """Press and hold a given object or at the given coordinates
324 Call release() when the object has been pressed long enough"""290 Call release() when the object has been pressed long enough"""
325 if len(args) == 2:291 self._finger_down(x, y)
326 self.current_x = args[0]
327 self.current_y = args[1]
328 elif len(args) == 0:
329 pass
330 else:
331 raise InvalidArgCount
332 self._finger_down(0, self.current_x, self.current_y)
333292
334 def release(self):293 def release(self):
335 """Release a previously pressed finger"""294 """Release a previously pressed finger"""
336 self._finger_up(0)295 self._finger_up()
337296
338297
339 def drag(self, x1, y1, x2, y2):298 def drag(self, x1, y1, x2, y2):
340 """Perform a drag gesture from (x1,y1) to (x2,y2)"""299 """Perform a drag gesture from (x1,y1) to (x2,y2)"""
341 self._finger_down(0, x1, y1)300 self._finger_down(x1, y1)
342301
343 # Let's drag in 100 steps for now...302 # Let's drag in 100 steps for now...
344 dx = 1.0 * (x2 - x1) / 100303 dx = 1.0 * (x2 - x1) / 100
@@ -346,92 +305,49 @@
346 cur_x = x1 + dx305 cur_x = x1 + dx
347 cur_y = y1 + dy306 cur_y = y1 + dy
348 for i in range(0, 100):307 for i in range(0, 100):
349 self._finger_move(0, int(cur_x), int(cur_y))308 self._finger_move(int(cur_x), int(cur_y))
350 sleep(0.002)309 sleep(0.002)
351 cur_x += dx310 cur_x += dx
352 cur_y += dy311 cur_y += dy
353 # Make sure we actually end up at target312 # Make sure we actually end up at target
354 self._finger_move(0, x2, y2)313 self._finger_move(x2, y2)
355 self._finger_up(0)314 self._finger_up()
356315
357 def pinch(self, center, distance_start, distance_end):316
358 """Perform a two finger pinch (zoom) gesture317
359 "center" gives the coordinates [x,y] of the center between the two fingers318 def _finger_down(self, x, y):
360 "distance_start" [x,y] values to move away from the center for the start
361 "distance_end" [x,y] values to move away from the center for the end
362 The fingers will move in 100 steps between the start and the end points.
363 If start is smaller than end, the gesture will zoom in, otherwise it
364 will zoom out."""
365
366 finger_1_start = [center[0] - distance_start[0], center[1] - distance_start[1]]
367 finger_2_start = [center[0] + distance_start[0], center[1] + distance_start[1]]
368 finger_1_end = [center[0] - distance_end[0], center[1] - distance_end[1]]
369 finger_2_end = [center[0] + distance_end[0], center[1] + distance_end[1]]
370
371 dx = 1.0 * (finger_1_end[0] - finger_1_start[0]) / 100
372 dy = 1.0 * (finger_1_end[1] - finger_1_start[1]) / 100
373
374 self._finger_down(0, finger_1_start[0], finger_1_start[1])
375 self._finger_down(1, finger_2_start[0], finger_2_start[1])
376
377 finger_1_cur = [finger_1_start[0] + dx, finger_1_start[1] + dy]
378 finger_2_cur = [finger_2_start[0] - dx, finger_2_start[1] - dy]
379
380 for i in range(0, 100):
381 self._finger_move(0, finger_1_cur[0], finger_1_cur[1])
382 self._finger_move(1, finger_2_cur[0], finger_2_cur[1])
383 sleep(0.005)
384
385 finger_1_cur = [finger_1_cur[0] + dx, finger_1_cur[1] + dy]
386 finger_2_cur = [finger_2_cur[0] - dx, finger_2_cur[1] - dy]
387
388 self._finger_move(0, finger_1_end[0], finger_1_end[1])
389 self._finger_move(1, finger_2_end[0], finger_2_end[1])
390 self._finger_up(0)
391 self._finger_up(1)
392
393 def _finger_down(self, finger, x, y):
394 """Internal: moves finger "finger" down to the touchscreen at pos (x,y)"""319 """Internal: moves finger "finger" down to the touchscreen at pos (x,y)"""
395 self.device.write(e.EV_ABS, e.ABS_MT_SLOT, finger)320 if self._touch_finger is not None:
396 self.device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())321 raise RuntimeError("Cannot press finger: it's already pressed.")
397 self.device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)322 self._touch_finger = _get_touch_finger()
398 self.device.write(e.EV_ABS, e.ABS_MT_POSITION_X, x)323
399 self.device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, y)324 _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
400 self.device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)325 _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())
401 self.device.syn()326 _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)
402327 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
403328 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
404 def _finger_move(self, finger, x, y):329 _touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
330 _touch_device.syn()
331
332
333 def _finger_move(self, x, y):
405 """Internal: moves finger "finger" on the touchscreen to pos (x,y)334 """Internal: moves finger "finger" on the touchscreen to pos (x,y)
406 NOTE: The finger has to be down for this to have any effect."""335 NOTE: The finger has to be down for this to have any effect."""
407 self.device.write(e.EV_ABS, e.ABS_MT_SLOT, finger)336 if self._touch_finger is not None:
408 self.device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))337 _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
409 self.device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))338 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
410 self.device.syn()339 _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
411340 _touch_device.syn()
412341
413 def _finger_up(self, finger):342
343 def _finger_up(self):
414 """Internal: moves finger "finger" up from the touchscreen"""344 """Internal: moves finger "finger" up from the touchscreen"""
415 self.device.write(e.EV_ABS, e.ABS_MT_SLOT, finger)345 if self._touch_finger is None:
416 self.device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)346 raise RuntimeError("Cannot release finger: it's not pressed.")
417 self.device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)347 _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
418 self.device.syn()348 _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
419349 _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)
420350 _touch_device.syn()
421class MultiTouch(object):
422 """High level interface to generate multi-touch events."""
423
424 # Need to work out a good interface for generating multitouch events...
425 # Possibly get users to specify each individual finger tracking path, or
426 # possibly specify a gesture by name, with parameters or so?
427 #
428 # Probably need to specify X & Y resolution as well. Think we can ignore
429 # pressure right now.
430 #
431 # Multi-touch protocol documentation is here:
432 #
433 # http://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt
434
435351
436352
437_UINPUT_CODE_TRANSLATIONS = {353_UINPUT_CODE_TRANSLATIONS = {
@@ -441,23 +357,3 @@
441 'ALT': 'LEFTALT',357 'ALT': 'LEFTALT',
442 'SHIFT': 'LEFTSHIFT',358 'SHIFT': 'LEFTSHIFT',
443}359}
444
445
446def _get_events_for_key(key):
447 """Return a list of events required to generate 'key' as an input.
448
449 Multiple keys will be returned when the key specified requires more than one
450 keypress to generate (for example, upper-case letters).
451
452 """
453 events = []
454 if key.isupper():
455 events.append(e.KEY_LEFTSHIFT)
456 keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
457 evt = getattr(e, 'KEY_' + keyname.upper(), None)
458 if evt is None:
459 raise ValueError("Unknown key name: '%s'" % key)
460 events.append(evt)
461 return events
462
463
464360
=== modified file 'autopilot/introspection/__init__.py'
--- autopilot/introspection/__init__.py 2013-02-22 03:31:40 +0000
+++ autopilot/introspection/__init__.py 2013-04-15 21:43:32 +0000
@@ -7,16 +7,18 @@
7# by the Free Software Foundation.7# by the Free Software Foundation.
8#8#
99
10"""Package for introspection support."""10"""Package for introspection support.
11
12This package contains the internal implementation of the autopilot introspection
13mechanism, and probably isn't useful to most test authors.
14
15"""
1116
12import dbus17import dbus
13from gi.repository import Gio
14import logging18import logging
15import subprocess19import subprocess
16from testtools.content import text_content
17from time import sleep20from time import sleep
18import os21import os
19import signal
2022
2123
22from autopilot.introspection.constants import (24from autopilot.introspection.constants import (
@@ -31,62 +33,65 @@
31 object_passes_filters,33 object_passes_filters,
32 get_session_bus,34 get_session_bus,
33 )35 )
34from autopilot.utilities import get_debug_logger36from autopilot.utilities import get_debug_logger, addCleanup
3537
3638
37logger = logging.getLogger(__name__)39logger = logging.getLogger(__name__)
3840
3941
40class ApplicationIntrospectionTestMixin(object):42def get_application_launcher(app_path):
41 """A mix-in class to make launching applications for introsection easier.43 """Return an instance of :class:`ApplicationLauncher` that knows how to launch
4244 the application at 'app_path'.
43 .. important:: You should not instantiate this class directly. Instead, use45 """
44 one of the derived classes.46 # TODO: this is a teeny bit hacky - we call ldd to check whether this application
4547 # links to certain library. We're assuming that linking to libQt* or libGtk*
46 """48 # means the application is introspectable. This excludes any non-dynamically
4749 # linked executables, which we may need to fix further down the line.
48 def launch_test_application(self, application, *arguments, **kwargs):50 try:
49 """Launch *application* and retrieve a proxy object for the application.51 ldd_output = subprocess.check_output(["ldd", app_path]).strip().lower()
5052 except subprocess.CalledProcessError:
51 Use this method to launch a supported application and start testing it.53 print "Error: Cannot auto-detect introspection plugin to load."
52 The application can be specified as:54 print "Use the '-i' argument to specify an interface."
5355 exit(1) # TODO - don't exit, raise an exception, and handle it appropriately in parent code.
54 * A Desktop file, either with or without a path component.56 if 'libqtcore' in ldd_output:
55 * An executable file, either with a path, or one that is in the $PATH.57 from autopilot.introspection.qt import QtApplicationLauncher
5658 return QtApplicationLauncher()
57 This method supports the following keyword arguments:59 elif 'libgtk' in ldd_output:
5860 from autopilot.introspection.gtk import GtkApplicationLauncher
59 * *launch_dir*. If set to a directory that exists the process will be61 return GtkApplicationLauncher()
60 launched from that directory.62 return None
6163
62 * *capture_output*. If set to True (the default), the process output64
63 will be captured and attached to the test as test detail.65def launch_application(launcher, application, *arguments, **kwargs):
6466 """Launch an application, and return a process object.
65 :raises: **ValueError** if unknown keyword arguments are passed.67
66 :return: A proxy object that represents the application. Introspection68 :param launcher: An instance of the :class:`ApplicationLauncher` class to
67 data is retrievable via this object.69 prepare the environment before launching the application itself.
6870 """
69 """71
70 if not isinstance(application, basestring):72 if not isinstance(application, basestring):
71 raise TypeError("'application' parameter must be a string.")73 raise TypeError("'application' parameter must be a string.")
72 cwd = kwargs.pop('launch_dir', None)74 cwd = kwargs.pop('launch_dir', None)
73 capture_output = kwargs.pop('capture_output', True)75 capture_output = kwargs.pop('capture_output', True)
74 if kwargs:76 if kwargs:
75 raise ValueError("Unknown keyword arguments: %s." %77 raise ValueError("Unknown keyword arguments: %s." %
76 (', '.join( repr(k) for k in kwargs.keys())))78 (', '.join( repr(k) for k in kwargs.keys())))
7779
78 if application.endswith('.desktop'):80 path, args = launcher.prepare_environment(application, list(arguments))
79 proc = Gio.DesktopAppInfo.new(application)81
80 application = proc.get_executable()82 process = launch_process(path,
8183 args,
82 path, args = self.prepare_environment(application, list(arguments))84 capture_output,
8385 cwd=cwd
84 process = launch_autopilot_enabled_process(path,86 )
85 args,87 return process
86 capture_output,88
87 cwd=cwd)89
88 self.addCleanup(self._kill_process_and_attach_logs, process)90class ApplicationLauncher(object):
89 return get_autopilot_proxy_object_for_process(process)91 """A class that knows how to launch an application with a certain type of
92 introspection enabled.
93
94 """
9095
91 def prepare_environment(self, app_path, arguments):96 def prepare_environment(self, app_path, arguments):
92 """Prepare the application, or environment to launch with autopilot-support.97 """Prepare the application, or environment to launch with autopilot-support.
@@ -99,22 +104,9 @@
99 """104 """
100 raise NotImplementedError("Sub-classes must implement this method.")105 raise NotImplementedError("Sub-classes must implement this method.")
101106
102 def _kill_process_and_attach_logs(self, process):107
103 process.kill()108
104 logger.info("waiting for process to exit.")109def launch_process(application, args, capture_output, **kwargs):
105 for i in range(10):
106 if process.returncode is not None:
107 break
108 if i == 9:
109 logger.info("Terminating process group, since it hasn't exited after 10 seconds.")
110 os.killpg(process.pid, signal.SIGTERM)
111 sleep(1)
112 stdout, stderr = process.communicate()
113 self.addDetail('process-stdout', text_content(stdout))
114 self.addDetail('process-stderr', text_content(stderr))
115
116
117def launch_autopilot_enabled_process(application, args, capture_output, **kwargs):
118 """Launch an autopilot-enabled process and return the proxy object."""110 """Launch an autopilot-enabled process and return the proxy object."""
119 commandline = [application]111 commandline = [application]
120 commandline.extend(args)112 commandline.extend(args)
@@ -132,28 +124,6 @@
132 return process124 return process
133125
134126
135def get_child_pids(pid):
136 """Get a list of all child process Ids, for the given parent.
137
138 """
139 def get_children(pid):
140 command = ['ps', '-o', 'pid', '--ppid', str(pid), '--noheaders']
141 try:
142 raw_output = subprocess.check_output(command)
143 except subprocess.CalledProcessError:
144 return []
145 return [int(p) for p in raw_output.split()]
146
147 result = [pid]
148 data = get_children(pid)
149 while data:
150 pid = data.pop(0)
151 result.append(pid)
152 data.extend(get_children(pid))
153
154 return result
155
156
157def get_autopilot_proxy_object_for_process(process):127def get_autopilot_proxy_object_for_process(process):
158 """Return the autopilot proxy object for the given *process*.128 """Return the autopilot proxy object for the given *process*.
159129
@@ -199,6 +169,28 @@
199 raise RuntimeError("Unable to find Autopilot interface.")169 raise RuntimeError("Unable to find Autopilot interface.")
200170
201171
172def get_child_pids(pid):
173 """Get a list of all child process Ids, for the given parent.
174
175 """
176 def get_children(pid):
177 command = ['ps', '-o', 'pid', '--ppid', str(pid), '--noheaders']
178 try:
179 raw_output = subprocess.check_output(command)
180 except subprocess.CalledProcessError:
181 return []
182 return [int(p) for p in raw_output.split()]
183
184 result = [pid]
185 data = get_children(pid)
186 while data:
187 pid = data.pop(0)
188 result.append(pid)
189 data.extend(get_children(pid))
190
191 return result
192
193
202def make_proxy_object_from_service_name(service_name, obj_path):194def make_proxy_object_from_service_name(service_name, obj_path):
203 """Returns a root proxy object given a DBus service name."""195 """Returns a root proxy object given a DBus service name."""
204 # parameters can sometimes be dbus.String instances, sometimes QString instances.196 # parameters can sometimes be dbus.String instances, sometimes QString instances.
205197
=== modified file 'autopilot/introspection/dbus.py'
--- autopilot/introspection/dbus.py 2013-02-20 04:00:40 +0000
+++ autopilot/introspection/dbus.py 2013-04-15 21:43:32 +0000
@@ -23,7 +23,7 @@
23from time import sleep23from time import sleep
24from textwrap import dedent24from textwrap import dedent
2525
26from autopilot.emulators.dbus_handler import get_session_bus26from autopilot.dbus_handler import get_session_bus
27from autopilot.introspection.constants import AP_INTROSPECTION_IFACE27from autopilot.introspection.constants import AP_INTROSPECTION_IFACE
28from autopilot.utilities import Timer28from autopilot.utilities import Timer
2929
3030
=== modified file 'autopilot/introspection/gtk.py'
--- autopilot/introspection/gtk.py 2013-01-25 01:47:48 +0000
+++ autopilot/introspection/gtk.py 2013-04-15 21:43:32 +0000
@@ -8,10 +8,10 @@
88
9import os9import os
1010
11from autopilot.introspection import ApplicationIntrospectionTestMixin11from autopilot.introspection import ApplicationLauncher
1212
1313
14class GtkIntrospectionTestMixin(ApplicationIntrospectionTestMixin):14class GtkApplicationLauncher(ApplicationLauncher):
15 """A mix-in class to make Gtk application introspection easier."""15 """A mix-in class to make Gtk application introspection easier."""
1616
17 def prepare_environment(self, app_path, arguments):17 def prepare_environment(self, app_path, arguments):
1818
=== modified file 'autopilot/introspection/qt.py'
--- autopilot/introspection/qt.py 2013-03-14 21:37:17 +0000
+++ autopilot/introspection/qt.py 2013-04-15 21:43:32 +0000
@@ -10,14 +10,14 @@
10"""Classes and tools to support Qt introspection."""10"""Classes and tools to support Qt introspection."""
1111
1212
13__all__ = ['QtIntrospectionTestMixin']13__all__ = ['QtApplicationLauncher']
1414
15import dbus15import dbus
16import functools16import functools
1717
18import logging18import logging
1919
20from autopilot.introspection import ApplicationIntrospectionTestMixin20from autopilot.introspection import ApplicationLauncher
21from autopilot.introspection.constants import QT_AUTOPILOT_IFACE21from autopilot.introspection.constants import QT_AUTOPILOT_IFACE
22from autopilot.introspection.dbus import get_session_bus22from autopilot.introspection.dbus import get_session_bus
2323
@@ -25,7 +25,7 @@
25logger = logging.getLogger(__name__)25logger = logging.getLogger(__name__)
2626
2727
28class QtIntrospectionTestMixin(ApplicationIntrospectionTestMixin):28class QtApplicationLauncher(ApplicationLauncher):
29 """A mix-in class to make Qt application introspection easier.29 """A mix-in class to make Qt application introspection easier.
3030
31 Inherit from this class if you want to launch and test Qt application with31 Inherit from this class if you want to launch and test Qt application with
3232
=== modified file 'autopilot/keybindings.py'
--- autopilot/keybindings.py 2013-02-28 00:26:07 +0000
+++ autopilot/keybindings.py 2013-04-15 21:43:32 +0000
@@ -27,8 +27,8 @@
27from types import NoneType27from types import NoneType
28import re28import re
2929
30from autopilot.emulators.input import get_keyboard30from autopilot.input import Keyboard
31from autopilot.compizconfig import get_plugin, get_setting31from autopilot.utilities import Silence
3232
33logger = logging.getLogger(__name__)33logger = logging.getLogger(__name__)
3434
@@ -184,8 +184,8 @@
184184
185 """185 """
186 plugin_name, setting_name = compiz_tuple186 plugin_name, setting_name = compiz_tuple
187 plugin = get_plugin(plugin_name)187 plugin = _get_compiz_plugin(plugin_name)
188 setting = get_setting(plugin_name, setting_name)188 setting = _get_compiz_setting(plugin_name, setting_name)
189 if setting.Type != 'Key':189 if setting.Type != 'Key':
190 raise ValueError("Key binding maps to a compiz option that does not hold a keybinding.")190 raise ValueError("Key binding maps to a compiz option that does not hold a keybinding.")
191 if not plugin.Enabled:191 if not plugin.Enabled:
@@ -232,7 +232,7 @@
232232
233 @property233 @property
234 def _keyboard(self):234 def _keyboard(self):
235 return get_keyboard()235 return Keyboard.create()
236236
237 def keybinding(self, binding_name, delay=None):237 def keybinding(self, binding_name, delay=None):
238 """Press and release the keybinding with the given name.238 """Press and release the keybinding with the given name.
@@ -263,3 +263,48 @@
263 def keybinding_hold_part_then_tap(self, binding_name):263 def keybinding_hold_part_then_tap(self, binding_name):
264 self.keybinding_hold(binding_name)264 self.keybinding_hold(binding_name)
265 self.keybinding_tap(binding_name)265 self.keybinding_tap(binding_name)
266
267
268# Functions that wrap compizconfig to avoid some unpleasantness in that module.
269# Local to the this keybindings for now until their removal in the very near
270# future.
271_global_compiz_context = None
272
273def _get_global_compiz_context():
274 """Get the compizconfig global context object."""
275 global _global_compiz_context
276 if _global_compiz_context is None:
277 with Silence():
278 from compizconfig import Context
279 _global_compiz_context = Context()
280 return _global_compiz_context
281
282
283def _get_compiz_plugin(plugin_name):
284 """Get a compizconfig plugin with the specified name.
285
286 Raises KeyError of the plugin named does not exist.
287
288 """
289 ctx = _get_global_compiz_context()
290 with Silence():
291 try:
292 return ctx.Plugins[plugin_name]
293 except KeyError:
294 raise KeyError("Compiz plugin '%s' does not exist." % (plugin_name))
295
296
297def _get_compiz_setting(plugin_name, setting_name):
298 """Get a compizconfig setting object, given a plugin name and setting name.
299
300 Raises KeyError if the plugin or setting is not found.
301
302 """
303 plugin = _get_compiz_plugin(plugin_name)
304 with Silence():
305 try:
306 return plugin.Screen[setting_name]
307 except KeyError:
308 raise KeyError("Compiz setting '%s' does not exist in plugin '%s'." % (setting_name, plugin_name))
309
310
266311
=== modified file 'autopilot/matchers/__init__.py'
--- autopilot/matchers/__init__.py 2012-12-05 02:15:45 +0000
+++ autopilot/matchers/__init__.py 2013-04-15 21:43:32 +0000
@@ -18,9 +18,54 @@
18class Eventually(Matcher):18class Eventually(Matcher):
19 """Asserts that a value will eventually equal a given Matcher object.19 """Asserts that a value will eventually equal a given Matcher object.
2020
21 This works on objects that *either* have a :meth:`wait_for(expected)`21 This matcher wraps another testtools matcher object. It makes that other
22 function, *or* objects that are callable and return the most current value22 matcher work with a timeout. This is necessary for several reasons:
23 (i.e.- they refresh the objects value).23
24 1. Since most actions in a GUI applicaton take some time to complete, the
25 test may need to wait for the application to enter the expected state.
26
27 2. Since the test is running in a separate process to the application under
28 test, test authors cannot make any assumptions about when the application
29 under test will recieve CPU time to update to the expected state.
30
31 There are two main ways of using the Eventually matcher:
32
33 **Attributes from the application**::
34
35 self.assertThat(window.maximized, Eventually(Equals(True)))
36
37 Here, ``window`` is an object generated by autopilot from the applications
38 state. This pattern of usage will cover 90% (or more) of the assertions in
39 an autopilot test. Note that any matcher can be used - either from testtools
40 or any custom matcher that implements the matcher API::
41
42 self.assertThat(window.height, Eventually(GreaterThan(200)))
43
44 **Callable Objects**::
45
46 self.assertThat(autopilot.platform.model, Eventually(Equals("Galaxy Nexus")))
47
48 In this example we're using the :func:`autopilot.platform.model` function as
49 a callable. In this form, Eventually matches against the return value of the
50 callable.
51
52 This can also be used to use a regular python property inside an Eventually
53 matcher::
54
55 self.assertThat(lambda: self.mouse.x, Eventually(LessThan(10)))
56
57 .. note:: Using this form generally makes your tests less readabvle, and
58 should be used with great care. It also relies the test author to have
59 knowledge about the implementation of the object being matched against.
60 In this example, if ``self.mouse.x`` were ever to change to be a regular
61 python attribute, this test would likely break.
62
63 **Timeout**
64
65 By default timeout period is ten seconds. This can be altered by passing the
66 timeout keyword::
67
68 self.assertThat(foo.bar, Eventually(Equals(123), timeout=30))
2469
25 """70 """
2671
2772
=== added file 'autopilot/platform.py'
--- autopilot/platform.py 1970-01-01 00:00:00 +0000
+++ autopilot/platform.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,105 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9"""
10Platform identification utilities for Autopilot.
11================================================
12
13This module provides functions that give test authors hints as to which platform
14their tests are currently running on. This is useful when tests should only run
15on certain platforms.
16
17"""
18
19from os.path import exists
20
21
22def model():
23 """Get the model name of the current platform.
24
25 For desktop / laptop installations, this will return "Desktop".
26 Otherwise, the current hardware model will be returned. For example:
27
28 >>> autopilot.platform.model()
29 ... "Galaxy Nexus"
30
31 """
32 return _PlatformDetector.create().model
33
34
35def image_codename():
36 """Get the image codename.
37
38 For desktop / laptop installations this will return "Desktop".
39 Otherwise, the codename of the image that was installed will be
40 returned. For example:
41
42 >>> autopilot.platform.image_codename()
43 ... "maguro"
44
45 """
46 return _PlatformDetector.create().image_codename
47
48
49class _PlatformDetector(object):
50
51 _cached_detector = None
52
53 @staticmethod
54 def create():
55 """Create a platform detector object, or return one we baked earlier."""
56 if _PlatformDetector._cached_detector is None:
57 _PlatformDetector._cached_detector = _PlatformDetector()
58 return _PlatformDetector._cached_detector
59
60 def __init__(self):
61 self.model = "Desktop"
62 self.image_codename = "Desktop"
63
64 property_file = _get_property_file()
65 if property_file is not None:
66 self.update_values_from_build_file(property_file)
67
68 def update_values_from_build_file(self, property_file):
69 """Read build.prop file and parse it."""
70 properties = _parse_build_properties_file(property_file)
71 self.model = properties.get('ro.product.model', "Desktop")
72 self.image_codename = properties.get('ro.product.name', "Desktop")
73
74
75def _get_property_file():
76 """Return a file-like object that contains the contents of the build properties
77 file, if it exists, or None.
78
79 """
80 if exists('/system/build.prop'):
81 return open('/system/build.prop')
82 return None
83
84
85def _parse_build_properties_file(property_file):
86 """Parse 'property_file', which must be a file-like object containing the
87 system build properties.
88
89 Returns a dictionary of key,value pairs.
90
91 """
92 properties = {}
93 for line in property_file:
94 line = line.strip()
95 if not line or line.startswith('#') or line.isspace():
96 continue
97 split_location = line.find('=')
98 if split_location == -1:
99 continue
100 key = line[:split_location]
101 value = line[split_location + 1:]
102
103 properties[key] = value
104 return properties
105
0106
=== added directory 'autopilot/process'
=== added file 'autopilot/process/__init__.py'
--- autopilot/process/__init__.py 1970-01-01 00:00:00 +0000
+++ autopilot/process/__init__.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,403 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Christopher Lee
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9from collections import OrderedDict
10
11from autopilot.utilities import _pick_variant
12
13
14class ProcessManager(object):
15
16 """A simple process manager class.
17
18 The process manager is used to handle processes, windows and applications.
19 This class should not be instantiated directly however. To get an instance
20 of the keyboard class, call :py:meth:`create` instead.
21
22 """
23
24 KNOWN_APPS = {
25 'Character Map' : {
26 'desktop-file': 'gucharmap.desktop',
27 'process-name': 'gucharmap',
28 },
29 'Calculator' : {
30 'desktop-file': 'gcalctool.desktop',
31 'process-name': 'gnome-calculator',
32 },
33 'Mahjongg' : {
34 'desktop-file': 'mahjongg.desktop',
35 'process-name': 'gnome-mahjongg',
36 },
37 'Remmina' : {
38 'desktop-file': 'remmina.desktop',
39 'process-name': 'remmina',
40 },
41 'System Settings' : {
42 'desktop-file': 'gnome-control-center.desktop',
43 'process-name': 'gnome-control-center',
44 },
45 'Text Editor' : {
46 'desktop-file': 'gedit.desktop',
47 'process-name': 'gedit',
48 },
49 'Terminal' : {
50 'desktop-file': 'gnome-terminal.desktop',
51 'process-name': 'gnome-terminal',
52 },
53 }
54
55
56 @staticmethod
57 def create(preferred_variant=""):
58 """Get an instance of the :py:class:`ProcessManager` class.
59
60 :param preferred_variant: A string containing a hint as to which variant you
61 would like. However, this hint can be ignored - autopilot will prefer to
62 return a keyboard variant other than the one requested, rather than fail
63 to return anything at all.
64 :raises: a RuntimeError will be raised if autopilot cannot instantate any of
65 the possible backends.
66 """
67 def get_bamf_pm():
68 from autopilot.process._bamf import ProcessManager
69 return ProcessManager()
70
71 def get_upa_pm():
72 from autopilot.process._upa import ProcessManager
73 return ProcessManager()
74
75 variants = OrderedDict()
76 variants['BAMF'] = get_bamf_pm
77 return _pick_variant(variants, preferred_variant)
78
79 @classmethod
80 def register_known_application(cls, name, desktop_file, process_name):
81 """Register an application with autopilot.
82
83 After calling this method, you may call :meth:`start_app` or
84 :meth:`start_app_window` with the `name` parameter to start this
85 application.
86 You need only call this once within a test run - the application will
87 remain registerred until the test run ends.
88
89 :param name: The name to be used when launching the application.
90 :param desktop_file: The filename (without path component) of the desktop file used to launch the application.
91 :param process_name: The name of the executable process that gets run.
92 :raises: **KeyError** if application has been registered already
93
94 """
95 if name in cls.KNOWN_APPS:
96 raise KeyError("Application has been registered already")
97 else:
98 cls.KNOWN_APPS[name] = {
99 "desktop-file" : desktop_file,
100 "process-name" : process_name
101 }
102
103 @classmethod
104 def unregister_known_application(cls, name):
105 """Unregister an application with the known_apps dictionary.
106
107 :param name: The name to be used when launching the application.
108 :raises: **KeyError** if the application has not been registered.
109
110 """
111 if name in cls.KNOWN_APPS:
112 del cls.KNOWN_APPS[name]
113 else:
114 raise KeyError("Application has not been registered")
115
116 def start_app(self, app_name, files=[], locale=None):
117 """Start one of the known applications, and kill it on tear down.
118
119 .. warning:: This method will clear all instances of this application on
120 tearDown, not just the one opened by this method! We recommend that
121 you use the :meth:`start_app_window` method instead, as it is generally
122 safer.
123
124 :param app_name: The application name. *This name must either already
125 be registered as one of the built-in applications that are supported
126 by autopilot, or must have been registered using*
127 :meth:`register_known_application` *beforehand.*
128 :param files: (Optional) A list of paths to open with the
129 given application. *Not all applications support opening files in this
130 way.*
131 :param locale: (Optional) The locale will to set when the application
132 is launched. *If you want to launch an application without any
133 localisation being applied, set this parameter to 'C'.*
134 :returns: A :class:`~autopilot.process.Application` instance.
135
136 """
137 raise NotImplementedError("You cannot use this class directly.")
138
139 def start_app_window(self, app_name, files=[], locale=None):
140 """Open a single window for one of the known applications, and close it
141 at the end of the test.
142
143 :param app_name: The application name. *This name must either already
144 be registered as one of the built-in applications that are supported
145 by autopilot, or must have been registered with*
146 :meth:`register_known_application` *beforehand.*
147 :param files: (Optional) Should be a list of paths to open with the
148 given application. *Not all applications support opening files in this
149 way.*
150 :param locale: (Optional) The locale will to set when the application
151 is launched. *If you want to launch an application without any
152 localisation being applied, set this parameter to 'C'.*
153 :raises: **AssertionError** if no window was opened, or more than one
154 window was opened.
155 :returns: A :class:`~autopilot.process.Window` instance.
156
157 """
158 raise NotImplementedError("You cannot use this class directly.")
159
160 def get_open_windows_by_application(self, app_name):
161 """Get a list of ~autopilot.process.Window` instances
162 for the given application name.
163
164 :param app_name: The name of one of the well-known applications.
165 :returns: A list of :class:`~autopilot.process.Window`
166 instances.
167
168 """
169 raise NotImplementedError("You cannot use this class directly.")
170
171 def close_all_app(self, app_name):
172 raise NotImplementedError("You cannot use this class directly.")
173
174 def get_app_instances(self, app_name):
175 raise NotImplementedError("You cannot use this class directly.")
176
177 def app_is_running(self, app_name):
178 raise NotImplementedError("You cannot use this class directly.")
179
180 def get_running_applications(self, user_visible_only=True):
181 """Get a list of the currently running applications.
182
183 If user_visible_only is True (the default), only applications
184 visible to the user in the switcher will be returned.
185
186 """
187 raise NotImplementedError("You cannot use this class directly.")
188
189 def get_running_applications_by_desktop_file(self, desktop_file):
190 """Return a list of applications that have the desktop file *desktop_file*.
191
192 This method will return an empty list if no applications
193 are found with the specified desktop file.
194
195 """
196 raise NotImplementedError("You cannot use this class directly.")
197
198 def get_open_windows(self, user_visible_only=True):
199 """Get a list of currently open windows.
200
201 If *user_visible_only* is True (the default), only applications visible
202 to the user in the switcher will be returned.
203
204 The result is sorted to be in stacking order.
205
206 """
207 raise NotImplementedError("You cannot use this class directly.")
208
209 def wait_until_application_is_running(self, desktop_file, timeout):
210 """Wait until a given application is running.
211
212 :param string desktop_file: The name of the application desktop file.
213 :param integer timeout: The maximum time to wait, in seconds. *If set to
214 something less than 0, this method will wait forever.*
215
216 :return: true once the application is found, or false if the application
217 was not found until the timeout was reached.
218 """
219 raise NotImplementedError("You cannot use this class directly.")
220
221 def launch_application(self, desktop_file, files=[], wait=True):
222 """Launch an application by specifying a desktop file.
223
224 :param files: List of files to pass to the application. *Not all
225 apps support this.*
226 :type files: List of strings
227
228 .. note:: If `wait` is True, this method will wait up to 10 seconds for
229 the application to appear.
230
231 :raises: **TypeError** on invalid *files* parameter.
232 :return: The Gobject process object.
233 """
234 raise NotImplementedError("You cannot use this class directly.")
235
236
237class Application(object):
238 @property
239 def desktop_file(self):
240 """Get the application desktop file.
241
242 This returns just the filename, not the full path.
243 If the application no longer exists, this returns an empty string.
244 """
245 raise NotImplementedError("You cannot use this class directly.")
246
247 @property
248 def name(self):
249 """Get the application name.
250
251 .. note:: This may change according to the current locale. If you want a
252 unique string to match applications against, use desktop_file instead.
253
254 """
255 raise NotImplementedError("You cannot use this class directly.")
256
257 @property
258 def icon(self):
259 """Get the application icon.
260
261 :return: The name of the icon.
262
263 """
264 raise NotImplementedError("You cannot use this class directly.")
265
266 @property
267 def is_active(self):
268 """Is the application active (i.e. has keyboard focus)?"""
269 raise NotImplementedError("You cannot use this class directly.")
270
271 @property
272 def is_urgent(self):
273 """Is the application currently signalling urgency?"""
274 raise NotImplementedError("You cannot use this class directly.")
275
276 @property
277 def user_visible(self):
278 """Is this application visible to the user?
279
280 .. note:: Some applications (such as the panel) are hidden to the user
281 but may still be returned.
282
283 """
284 raise NotImplementedError("You cannot use this class directly.")
285
286 def get_windows(self):
287 """Get a list of the application windows."""
288 raise NotImplementedError("You cannot use this class directly.")
289
290
291
292class Window(object):
293 @property
294 def x_id(self):
295 """Get the X11 Window Id."""
296 raise NotImplementedError("You cannot use this class directly.")
297
298 @property
299 def x_win(self):
300 """Get the X11 window object of the underlying window."""
301 raise NotImplementedError("You cannot use this class directly.")
302
303 @property
304 def get_wm_state(self):
305 """Get the state of the underlying window."""
306 raise NotImplementedError("You cannot use this class directly.")
307
308 @property
309 def name(self):
310 """Get the window name.
311
312 .. note:: This may change according to the current locale. If you want a
313 unique string to match windows against, use the x_id instead.
314
315 """
316 raise NotImplementedError("You cannot use this class directly.")
317
318 @property
319 def title(self):
320 """Get the window title.
321
322 This may be different from the application name.
323
324 .. note:: This may change depending on the current locale.
325
326 """
327 raise NotImplementedError("You cannot use this class directly.")
328
329 @property
330 def geometry(self):
331 """Get the geometry for this window.
332
333 :return: Tuple containing (x, y, width, height).
334
335 """
336 raise NotImplementedError("You cannot use this class directly.")
337
338 @property
339 def is_maximized(self):
340 """Is the window maximized?
341
342 Maximized in this case means both maximized vertically and
343 horizontally. If a window is only maximized in one direction it is not
344 considered maximized.
345
346 """
347 raise NotImplementedError("You cannot use this class directly.")
348
349 @property
350 def application(self):
351 """Get the application that owns this window.
352
353 This method may return None if the window does not have an associated
354 application. The 'desktop' window is one such example.
355
356 """
357 raise NotImplementedError("You cannot use this class directly.")
358
359 @property
360 def user_visible(self):
361 """Is this window visible to the user in the switcher?"""
362 raise NotImplementedError("You cannot use this class directly.")
363
364 @property
365 def is_hidden(self):
366 """Is this window hidden?
367
368 Windows are hidden when the 'Show Desktop' mode is activated.
369
370 """
371 raise NotImplementedError("You cannot use this class directly.")
372
373 @property
374 def is_focused(self):
375 """Is this window focused?"""
376 raise NotImplementedError("You cannot use this class directly.")
377
378 @property
379 def is_valid(self):
380 """Is this window object valid?
381
382 Invalid windows are caused by windows closing during the construction of
383 this object instance.
384
385 """
386 raise NotImplementedError("You cannot use this class directly.")
387
388 @property
389 def monitor(self):
390 """Returns the monitor to which the windows belongs to"""
391 raise NotImplementedError("You cannot use this class directly.")
392
393 @property
394 def closed(self):
395 """Returns True if the window has been closed"""
396 raise NotImplementedError("You cannot use this class directly.")
397
398 def close(self):
399 """Close the window."""
400 raise NotImplementedError("You cannot use this class directly.")
401
402 def set_focus(self):
403 raise NotImplementedError("You cannot use this class directly.")
0404
=== renamed file 'autopilot/emulators/bamf.py' => 'autopilot/process/_bamf.py'
--- autopilot/emulators/bamf.py 2013-01-06 21:51:56 +0000
+++ autopilot/process/_bamf.py 2013-04-15 21:43:32 +0000
@@ -5,7 +5,7 @@
5# under the terms of the GNU General Public License version 3, as published5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.6# by the Free Software Foundation.
77
8"""Various classes for interacting with BAMF."""8"""BAMF implementation of the Process Management"""
99
10from __future__ import absolute_import10from __future__ import absolute_import
1111
@@ -13,21 +13,25 @@
13import dbus.glib13import dbus.glib
14from gi.repository import Gio14from gi.repository import Gio
15from gi.repository import GLib15from gi.repository import GLib
16import logging
16import os17import os
18from time import sleep
17from Xlib import display, X, protocol19from Xlib import display, X, protocol
1820
19from autopilot.emulators.dbus_handler import get_session_bus21from autopilot.dbus_handler import get_session_bus
20from autopilot.utilities import Silence22from autopilot.utilities import addCleanup, Silence
2123
22__all__ = [24from autopilot.process import (
23 "Bamf",25 ProcessManager as ProcessManagerBase,
24 "BamfApplication",26 Application as ApplicationBase,
25 "BamfWindow",27 Window as WindowBase
26 ]28 )
29
2730
28_BAMF_BUS_NAME = 'org.ayatana.bamf'31_BAMF_BUS_NAME = 'org.ayatana.bamf'
29_X_DISPLAY = None32_X_DISPLAY = None
3033
34logger = logging.getLogger(__name__)
3135
32def get_display():36def get_display():
33 """Create an Xlib display object (silently) and return it."""37 """Create an Xlib display object (silently) and return it."""
@@ -51,7 +55,7 @@
51 return False55 return False
5256
5357
54class Bamf(object):58class ProcessManager(ProcessManagerBase):
55 """High-level class for interacting with Bamf from within a test.59 """High-level class for interacting with Bamf from within a test.
5660
57 Use this class to inspect the state of running applications and open61 Use this class to inspect the state of running applications and open
@@ -65,6 +69,126 @@
65 self.matcher_proxy = get_session_bus().get_object(_BAMF_BUS_NAME, matcher_path)69 self.matcher_proxy = get_session_bus().get_object(_BAMF_BUS_NAME, matcher_path)
66 self.matcher_interface = dbus.Interface(self.matcher_proxy, self.matcher_interface_name)70 self.matcher_interface = dbus.Interface(self.matcher_proxy, self.matcher_interface_name)
6771
72 def start_app(self, app_name, files=[], locale=None):
73 """Start one of the known applications, and kill it on tear down.
74
75 .. warning:: This method will clear all instances of this application on
76 tearDown, not just the one opened by this method! We recommend that
77 you use the :meth:`start_app_window` method instead, as it is generally
78 safer.
79
80 :param app_name: The application name. *This name must either already
81 be registered as one of the built-in applications that are supported
82 by autopilot, or must have been registered using*
83 :meth:`register_known_application` *beforehand.*
84 :param files: (Optional) A list of paths to open with the
85 given application. *Not all applications support opening files in this
86 way.*
87 :param locale: (Optional) The locale will to set when the application
88 is launched. *If you want to launch an application without any
89 localisation being applied, set this parameter to 'C'.*
90 :returns: A :class:`~autopilot.process.Application` instance.
91
92 """
93 window = self._open_window(app_name, files, locale)
94 if window:
95 addCleanup(self.close_all_app, app_name)
96 return window.application
97
98 raise AssertionError("No new application window was opened.")
99
100 def start_app_window(self, app_name, files=[], locale=None):
101 """Open a single window for one of the known applications, and close it
102 at the end of the test.
103
104 :param app_name: The application name. *This name must either already
105 be registered as one of the built-in applications that are supported
106 by autopilot, or must have been registered with*
107 :meth:`register_known_application` *beforehand.*
108 :param files: (Optional) Should be a list of paths to open with the
109 given application. *Not all applications support opening files in this
110 way.*
111 :param locale: (Optional) The locale will to set when the application
112 is launched. *If you want to launch an application without any
113 localisation being applied, set this parameter to 'C'.*
114 :raises: **AssertionError** if no window was opened, or more than one
115 window was opened.
116 :returns: A :class:`~autopilot.process.Window` instance.
117
118 """
119 window = self._open_window(app_name, files, locale)
120 if window:
121 addCleanup(window.close)
122 return window
123 raise AssertionError("No window was opened.")
124
125 def _open_window(self, app_name, files, locale):
126 """Open a new 'app_name' window, returning the window instance or None.
127
128 Raises an AssertionError if this creates more than one window.
129
130 """
131 existing_windows = self.get_open_windows_by_application(app_name)
132
133 if locale:
134 os.putenv("LC_ALL", locale)
135 addCleanup(os.unsetenv, "LC_ALL")
136 logger.info("Starting application '%s' with files %r in locale %s", app_name, files, locale)
137 else:
138 logger.info("Starting application '%s' with files %r", app_name, files)
139
140
141 app = self.KNOWN_APPS[app_name]
142 self.launch_application(app['desktop-file'], files)
143 apps = self.get_running_applications_by_desktop_file(app['desktop-file'])
144
145 for i in range(10):
146 try:
147 new_windows = []
148 [new_windows.extend(a.get_windows()) for a in apps]
149 filter_fn = lambda w: w.x_id not in [c.x_id for c in existing_windows]
150 new_wins = filter(filter_fn, new_windows)
151 if new_wins:
152 assert len(new_wins) == 1
153 return new_wins[0]
154 except DBusException:
155 pass
156 sleep(1)
157 return None
158
159 def get_open_windows_by_application(self, app_name):
160 """Get a list of ~autopilot.process.Window` instances
161 for the given application name.
162
163 :param app_name: The name of one of the well-known applications.
164 :returns: A list of :class:`~autopilot.process.Window`
165 instances.
166
167 """
168 existing_windows = []
169 [existing_windows.extend(a.get_windows()) for a in self.get_app_instances(app_name)]
170 return existing_windows
171
172 def close_all_app(self, app_name):
173 """Close all instances of the application 'app_name'."""
174 app = self.KNOWN_APPS[app_name]
175 try:
176 pids = check_output(["pidof", app['process-name']]).split()
177 if len(pids):
178 call(["kill"] + pids)
179 except CalledProcessError:
180 logger.warning("Tried to close applicaton '%s' but it wasn't running.", app_name)
181
182 def get_app_instances(self, app_name):
183 """Get `~autopilot.process.Application` instances for app_name."""
184 desktop_file = self.KNOWN_APPS[app_name]['desktop-file']
185 return self.get_running_applications_by_desktop_file(desktop_file)
186
187 def app_is_running(self, app_name):
188 """Return true if an instance of the application is running."""
189 apps = self.get_app_instances(app_name)
190 return len(apps) > 0
191
68 def get_running_applications(self, user_visible_only=True):192 def get_running_applications(self, user_visible_only=True):
69 """Get a list of the currently running applications.193 """Get a list of the currently running applications.
70194
@@ -72,7 +196,7 @@
72 visible to the user in the switcher will be returned.196 visible to the user in the switcher will be returned.
73197
74 """198 """
75 apps = [BamfApplication(p) for p in self.matcher_interface.RunningApplications()]199 apps = [Application(p) for p in self.matcher_interface.RunningApplications()]
76 if user_visible_only:200 if user_visible_only:
77 return filter(_filter_user_visible, apps)201 return filter(_filter_user_visible, apps)
78 return apps202 return apps
@@ -80,7 +204,7 @@
80 def get_running_applications_by_desktop_file(self, desktop_file):204 def get_running_applications_by_desktop_file(self, desktop_file):
81 """Return a list of applications that have the desktop file *desktop_file*.205 """Return a list of applications that have the desktop file *desktop_file*.
82206
83 This method may return an empty list, if no applications207 This method will return an empty list if no applications
84 are found with the specified desktop file.208 are found with the specified desktop file.
85209
86 """210 """
@@ -93,14 +217,6 @@
93 pass217 pass
94 return apps218 return apps
95219
96 def get_application_by_xid(self, xid):
97 """Return the application that has a child with the requested xid or None."""
98
99 app_path = self.matcher_interface.ApplicationForXid(xid)
100 if len(app_path):
101 return BamfApplication(app_path)
102 return None
103
104 def get_open_windows(self, user_visible_only=True):220 def get_open_windows(self, user_visible_only=True):
105 """Get a list of currently open windows.221 """Get a list of currently open windows.
106222
@@ -111,7 +227,7 @@
111227
112 """228 """
113229
114 windows = [BamfWindow(w) for w in self.matcher_interface.WindowStackForMonitor(-1)]230 windows = [Window(w) for w in self.matcher_interface.WindowStackForMonitor(-1)]
115 if user_visible_only:231 if user_visible_only:
116 windows = filter(_filter_user_visible, windows)232 windows = filter(_filter_user_visible, windows)
117 # Now sort on stacking order.233 # Now sort on stacking order.
@@ -119,11 +235,6 @@
119 # try and use len() on return values from these methods.235 # try and use len() on return values from these methods.
120 return list(reversed(windows))236 return list(reversed(windows))
121237
122 def get_window_by_xid(self, xid):
123 """Get the BamfWindow that matches the provided *xid*."""
124 windows = [BamfWindow(w) for w in self.matcher_interface.WindowPaths() if BamfWindow(w).x_id == xid]
125 return windows[0] if windows else None
126
127 def wait_until_application_is_running(self, desktop_file, timeout):238 def wait_until_application_is_running(self, desktop_file, timeout):
128 """Wait until a given application is running.239 """Wait until a given application is running.
129240
@@ -147,7 +258,7 @@
147 # No, so define a callback to watch the ViewOpened signal:258 # No, so define a callback to watch the ViewOpened signal:
148 def on_view_added(bamf_path, name):259 def on_view_added(bamf_path, name):
149 if bamf_path.split('/')[-1].startswith('application'):260 if bamf_path.split('/')[-1].startswith('application'):
150 app = BamfApplication(bamf_path)261 app = Application(bamf_path)
151 if desktop_file == os.path.split(app.desktop_file)[1]:262 if desktop_file == os.path.split(app.desktop_file)[1]:
152 gobject_loop.quit()263 gobject_loop.quit()
153264
@@ -192,7 +303,7 @@
192 return proc303 return proc
193304
194305
195class BamfApplication(object):306class Application(ApplicationBase):
196 """Represents an application, with information as returned by Bamf.307 """Represents an application, with information as returned by Bamf.
197308
198 .. important:: Don't instantiate this class yourself. instead, use the309 .. important:: Don't instantiate this class yourself. instead, use the
@@ -265,20 +376,20 @@
265376
266 def get_windows(self):377 def get_windows(self):
267 """Get a list of the application windows."""378 """Get a list of the application windows."""
268 return [BamfWindow(w) for w in self._view_iface.Children()]379 return [Window(w) for w in self._view_iface.Children()]
269380
270 def __repr__(self):381 def __repr__(self):
271 return "<BamfApplication '%s'>" % (self.name)382 return "<Application '%s'>" % (self.name)
272383
273 def __eq__(self, other):384 def __eq__(self, other):
274 return self.desktop_file == other.desktop_file385 return self.desktop_file == other.desktop_file
275386
276387
277class BamfWindow(object):388class Window(WindowBase):
278 """Represents an application window, as returned by Bamf.389 """Represents an application window, as returned by Bamf.
279390
280 .. important:: Don't instantiate this class yourself. Instead, use the391 .. important:: Don't instantiate this class yourself. Instead, use the
281 appropriate methods in BamfApplication.392 appropriate methods in Application.
282393
283 """394 """
284 def __init__(self, window_path):395 def __init__(self, window_path):
@@ -364,7 +475,7 @@
364 # associated application. For these windows we return none.475 # associated application. For these windows we return none.
365 parents = self._view_iface.Parents()476 parents = self._view_iface.Parents()
366 if parents:477 if parents:
367 return BamfApplication(parents[0])478 return Application(parents[0])
368 else:479 else:
369 return None480 return None
370481
@@ -423,7 +534,7 @@
423 self._x_win.configure(stack_mode=X.Above)534 self._x_win.configure(stack_mode=X.Above)
424535
425 def __repr__(self):536 def __repr__(self):
426 return "<BamfWindow '%s' Xid: %d>" % (self.title if self._x_win else '', self.x_id)537 return "<Window '%s' Xid: %d>" % (self.title if self._x_win else '', self.x_id)
427538
428 def _getProperty(self, _type):539 def _getProperty(self, _type):
429 """Get an X11 property.540 """Get an X11 property.
430541
=== modified file 'autopilot/testcase.py'
--- autopilot/testcase.py 2013-04-05 14:02:59 +0000
+++ autopilot/testcase.py 2013-04-15 21:43:32 +0000
@@ -11,40 +11,35 @@
11from __future__ import absolute_import11from __future__ import absolute_import
1212
13from dbus import DBusException13from dbus import DBusException
14from gi.repository import Gio
14import logging15import logging
15import os16import os
16from StringIO import StringIO17import signal
17from subprocess import (18from subprocess import (
18 call,19 call,
19 CalledProcessError,20 CalledProcessError,
20 check_output,21 check_output,
21 Popen,
22 PIPE,
23 STDOUT,
24 )22 )
2523
26from testscenarios import TestWithScenarios24from testscenarios import TestWithScenarios
27from testtools import TestCase25from testtools import TestCase
28from testtools.content import text_content26from testtools.content import text_content
29from testtools.matchers import Equals27from testtools.matchers import Equals
30import time28from time import sleep
3129
32from autopilot.compizconfig import get_global_context30from autopilot.process import ProcessManager
33from autopilot.emulators.bamf import Bamf31from autopilot.input import Keyboard, Mouse
34from autopilot.emulators.zeitgeist import Zeitgeist32from autopilot.introspection import (
35from autopilot.emulators.processmanager import ProcessManager33 get_application_launcher,
36from autopilot.emulators.X11 import ScreenGeometry, reset_display34 get_autopilot_proxy_object_for_process,
37from autopilot.emulators.input import get_keyboard, get_mouse35 launch_application,
38from autopilot.glibrunner import AutopilotTestRunner36 launch_process,
39from autopilot.globals import (get_log_verbose,
40 get_video_recording_enabled,
41 get_video_record_directory,
42 )37 )
38from autopilot.display import Display
39from autopilot.globals import on_test_started
43from autopilot.keybindings import KeybindingsHelper40from autopilot.keybindings import KeybindingsHelper
44from autopilot.matchers import Eventually41from autopilot.matchers import Eventually
45from autopilot.utilities import (get_compiz_setting,42
46 LogFormatter,
47 )
4843
49logger = logging.getLogger(__name__)44logger = logging.getLogger(__name__)
5045
@@ -74,108 +69,7 @@
74 return result69 return result
7570
7671
77class LoggedTestCase(TestWithScenarios, TestCase):72class AutopilotTestCase(TestWithScenarios, TestCase, KeybindingsHelper):
78 """Initialize the logging for the test case."""
79
80 def setUp(self):
81 self._setUpTestLogging()
82 # The reason that the super setup is done here is due to making sure
83 # that the logging is properly set up prior to calling it.
84 super(LoggedTestCase, self).setUp()
85 if get_log_verbose():
86 logger.info("*" * 60)
87 logger.info("Starting test %s", self.shortDescription())
88
89 def _setUpTestLogging(self):
90 self._log_buffer = StringIO()
91 root_logger = logging.getLogger()
92 root_logger.setLevel(logging.DEBUG)
93 formatter = LogFormatter()
94 self._log_handler = logging.StreamHandler(stream=self._log_buffer)
95 self._log_handler.setFormatter(formatter)
96 root_logger.addHandler(self._log_handler)
97
98 #Tear down logging in a cleanUp handler, so it's done after all other
99 # tearDown() calls and cleanup handlers.
100 self.addCleanup(self._tearDownLogging)
101
102 def _tearDownLogging(self):
103 root_logger = logging.getLogger()
104 self._log_handler.flush()
105 self._log_buffer.seek(0)
106 self.addDetail('test-log', text_content(self._log_buffer.getvalue()))
107 root_logger.removeHandler(self._log_handler)
108 # Calling del to remove the handler and flush the buffer. We are
109 # abusing the log handlers here a little.
110 del self._log_buffer
111
112
113
114class VideoCapturedTestCase(LoggedTestCase):
115 """Video capture autopilot tests, saving the results if the test failed."""
116
117 _recording_app = '/usr/bin/recordmydesktop'
118 _recording_opts = ['--no-sound', '--no-frame', '-o',]
119
120 def setUp(self):
121 super(VideoCapturedTestCase, self).setUp()
122 global video_recording_enabled
123 if get_video_recording_enabled() and not self._have_recording_app():
124 video_recording_enabled = False
125 logger.warning("Disabling video capture since '%s' is not present", self._recording_app)
126
127 if get_video_recording_enabled():
128 self._test_passed = True
129 self.addOnException(self._on_test_failed)
130 self.addCleanup(self._stop_video_capture)
131 self._start_video_capture()
132
133 def _have_recording_app(self):
134 return os.path.exists(self._recording_app)
135
136 def _start_video_capture(self):
137 args = self._get_capture_command_line()
138 self._capture_file = self._get_capture_output_file()
139 self._ensure_directory_exists_but_not_file(self._capture_file)
140 args.append(self._capture_file)
141 logger.debug("Starting: %r", args)
142 self._capture_process = Popen(args, stdout=PIPE, stderr=STDOUT)
143
144 def _stop_video_capture(self):
145 """Stop the video capture. If the test failed, save the resulting file."""
146
147 if self._test_passed:
148 # We use kill here because we don't want the recording app to start
149 # encoding the video file (since we're removing it anyway.)
150 self._capture_process.kill()
151 self._capture_process.wait()
152 else:
153 self._capture_process.terminate()
154 self._capture_process.wait()
155 if self._capture_process.returncode != 0:
156 self.addDetail('video capture log', text_content(self._capture_process.stdout.read()))
157 self._capture_process = None
158
159 def _get_capture_command_line(self):
160 return [self._recording_app] + self._recording_opts
161
162 def _get_capture_output_file(self):
163 return os.path.join(get_video_record_directory(), '%s.ogv' % (self.shortDescription()))
164
165 def _ensure_directory_exists_but_not_file(self, file_path):
166 dirpath = os.path.dirname(file_path)
167 if not os.path.exists(dirpath):
168 os.makedirs(dirpath)
169 elif os.path.exists(file_path):
170 logger.warning("Video capture file '%s' already exists, deleting.", file_path)
171 os.remove(file_path)
172
173 def _on_test_failed(self, ex_info):
174 """Called when a test fails."""
175 self._test_passed = False
176
177
178class AutopilotTestCase(VideoCapturedTestCase, KeybindingsHelper):
179 """Wrapper around testtools.TestCase that adds significant functionality.73 """Wrapper around testtools.TestCase that adds significant functionality.
18074
181 This class should be the base class for all autopilot test case classes. Not75 This class should be the base class for all autopilot test case classes. Not
@@ -190,19 +84,11 @@
190 :meth:`~autopilot.testcase.AutopilotTestCase.start_app` and84 :meth:`~autopilot.testcase.AutopilotTestCase.start_app` and
191 :meth:`~autopilot.testcase.AutopilotTestCase.start_app_window` which will85 :meth:`~autopilot.testcase.AutopilotTestCase.start_app_window` which will
192 launch one of the well-known applications and return a86 launch one of the well-known applications and return a
193 :class:`~autopilot.emulators.bamf.BamfApplication` or87 :class:`~autopilot.process.Application` or
194 :class:`~autopilot.emulators.bamf.BamfWindow` instance to the launched88 :class:`~autopilot.process.Window` instance to the launched
195 process respectively. All applications launched in this way will be closed89 process respectively. All applications launched in this way will be closed
196 when the test ends.90 when the test ends.
19791
198 **Set Unity & Compiz Options**
199
200 The :meth:`~autopilot.testcase.AutopilotTestCase.set_unity_option` and
201 :meth:`~autopilot.testcase.AutopilotTestCase.set_compiz_option` methods set a
202 unity or compiz setting to a particular value for the duration of the
203 current test only. This is useful if you want the window manager to behave
204 in a particular fashion for a particular test, while being assured that any
205 chances are non-destructive.
20692
207 **Patch Process Environment**93 **Patch Process Environment**
20894
@@ -213,269 +99,40 @@
21399
214 """100 """
215101
216 run_tests_with = AutopilotTestRunner
217
218 KNOWN_APPS = {
219 'Character Map' : {
220 'desktop-file': 'gucharmap.desktop',
221 'process-name': 'gucharmap',
222 },
223 'Calculator' : {
224 'desktop-file': 'gcalctool.desktop',
225 'process-name': 'gnome-calculator',
226 },
227 'Mahjongg' : {
228 'desktop-file': 'mahjongg.desktop',
229 'process-name': 'gnome-mahjongg',
230 },
231 'Remmina' : {
232 'desktop-file': 'remmina.desktop',
233 'process-name': 'remmina',
234 },
235 'System Settings' : {
236 'desktop-file': 'gnome-control-center.desktop',
237 'process-name': 'gnome-control-center',
238 },
239 'Text Editor' : {
240 'desktop-file': 'gedit.desktop',
241 'process-name': 'gedit',
242 },
243 'Terminal' : {
244 'desktop-file': 'gnome-terminal.desktop',
245 'process-name': 'gnome-terminal',
246 },
247 }
248
249
250 def setUp(self):102 def setUp(self):
251 super(AutopilotTestCase, self).setUp()103 super(AutopilotTestCase, self).setUp()
252104 on_test_started(self)
253 self._process_manager = ProcessManager()105
254 self._process_manager.snapshot_running_apps()106 self.process_manager = ProcessManager.create()
255 self.addCleanup(self._process_manager.compare_system_with_snapshot)107 self._app_snapshot = self.process_manager.get_running_applications()
256108 self.addCleanup(self._compare_system_with_app_snapshot)
257 self.bamf = Bamf()109
258 self.keyboard = get_keyboard()110 self.keyboard = Keyboard.create()
259 self.mouse = get_mouse()111 self.mouse = Mouse.create()
260 self.zeitgeist = Zeitgeist()112
261113 self.screen_geo = Display.create()
262 self.screen_geo = ScreenGeometry()
263 self.addCleanup(self.keyboard.cleanup)114 self.addCleanup(self.keyboard.cleanup)
264 self.addCleanup(self.mouse.cleanup)115 self.addCleanup(self.mouse.cleanup)
265116
266 def call_gsettings_cmd(self, command, schema, *args):117 def _compare_system_with_app_snapshot(self):
267 """Set a desktop wide gsettings option118 """Compare the currently running application with the last snapshot.
268119
269 Using the gsettings command because there is a bug with importing120 This method will raise an AssertionError if there are any new applications
270 from gobject introspection and pygtk2 simultaneously, and the Xlib121 currently running that were not running when the snapshot was taken.
271 keyboard layout bits are very unwieldy. This seems like the best122 """
272 solution, even a little bit brutish.123 if self._app_snapshot is None:
273 """124 raise RuntimeError("No snapshot to match against.")
274 cmd = ['gsettings', command, schema] + list(args)125
275 # strip to remove the trailing \n.126 new_apps = []
276 ret = check_output(cmd).strip()
277 time.sleep(5)
278 reset_display()
279 return ret
280
281 def set_unity_option(self, option_name, option_value):
282 """Set an option in the unity compiz plugin options.
283
284 .. note:: The value will be set for the current test only, and
285 automatically undone when the test ends.
286
287 :param option_name: The name of the unity option.
288 :param option_value: The value you want to set.
289 :raises: **KeyError** if the option named does not exist.
290
291 """
292 self.set_compiz_option("unityshell", option_name, option_value)
293
294 def set_compiz_option(self, plugin_name, option_name, option_value):
295 """Set a compiz option for the duration of this test only.
296
297 .. note:: The value will be set for the current test only, and
298 automatically undone when the test ends.
299
300 :param plugin_name: The name of the compiz plugin where the option is
301 registered. If the option is not in a plugin, the string "core" should
302 be used as the plugin name.
303 :param option_name: The name of the unity option.
304 :param option_value: The value you want to set.
305 :raises: **KeyError** if the option named does not exist.
306
307 """
308 old_value = self._set_compiz_option(plugin_name, option_name, option_value)
309 # Cleanup is LIFO, during clean-up also allow unity to respond
310 self.addCleanup(time.sleep, 0.5)
311 self.addCleanup(self._set_compiz_option, plugin_name, option_name, old_value)
312 # Allow unity time to respond to the new setting.
313 time.sleep(0.5)
314
315 def _set_compiz_option(self, plugin_name, option_name, option_value):
316 logger.info("Setting compiz option '%s' in plugin '%s' to %r",
317 option_name, plugin_name, option_value)
318 setting = get_compiz_setting(plugin_name, option_name)
319 old_value = setting.Value
320 setting.Value = option_value
321 get_global_context().Write()
322 return old_value
323
324 @classmethod
325 def register_known_application(cls, name, desktop_file, process_name):
326 """Register an application with autopilot.
327
328 After calling this method, you may call :meth:`start_app` or
329 :meth:`start_app_window` with the `name` parameter to start this
330 application.
331 You need only call this once within a test run - the application will
332 remain registerred until the test run ends.
333
334 :param name: The name to be used when launching the application.
335 :param desktop_file: The filename (without path component) of the desktop file used to launch the application.
336 :param process_name: The name of the executable process that gets run.
337 :raises: **KeyError** if application has been registered already
338
339 """
340 if name in cls.KNOWN_APPS:
341 raise KeyError("Application has been registered already")
342 else:
343 cls.KNOWN_APPS[name] = {
344 "desktop-file" : desktop_file,
345 "process-name" : process_name
346 }
347
348 @classmethod
349 def unregister_known_application(cls, name):
350 """Unregister an application with the known_apps dictionary.
351
352 :param name: The name to be used when launching the application.
353 :raises: **KeyError** if the application has not been registered.
354
355 """
356 if name in cls.KNOWN_APPS:
357 del cls.KNOWN_APPS[name]
358 else:
359 raise KeyError("Application has not been registered")
360
361 def start_app(self, app_name, files=[], locale=None):
362 """Start one of the known applications, and kill it on tear down.
363
364 .. warning:: This method will clear all instances of this application on
365 tearDown, not just the one opened by this method! We recommend that
366 you use the :meth:`start_app_window` method instead, as it is generally
367 safer.
368
369 :param app_name: The application name. *This name must either already
370 be registered as one of the built-in applications that are supported
371 by autopilot, or must have been registered using*
372 :meth:`register_known_application` *beforehand.*
373 :param files: (Optional) A list of paths to open with the
374 given application. *Not all applications support opening files in this
375 way.*
376 :param locale: (Optional) The locale will to set when the application
377 is launched. *If you want to launch an application without any
378 localisation being applied, set this parameter to 'C'.*
379 :returns: A :class:`~autopilot.emulators.bamf.BamfApplication` instance.
380
381 """
382 window = self._open_window(app_name, files, locale)
383 if window:
384 self.addCleanup(self.close_all_app, app_name)
385 return window.application
386
387 raise AssertionError("No new application window was opened.")
388
389 def start_app_window(self, app_name, files=[], locale=None):
390 """Open a single window for one of the known applications, and close it
391 at the end of the test.
392
393 :param app_name: The application name. *This name must either already
394 be registered as one of the built-in applications that are supported
395 by autopilot, or must have been registered with*
396 :meth:`register_known_application` *beforehand.*
397 :param files: (Optional) Should be a list of paths to open with the
398 given application. *Not all applications support opening files in this
399 way.*
400 :param locale: (Optional) The locale will to set when the application
401 is launched. *If you want to launch an application without any
402 localisation being applied, set this parameter to 'C'.*
403 :raises: **AssertionError** if no window was opened, or more than one
404 window was opened.
405 :returns: A :class:`~autopilot.emulators.bamf.BamfWindow` instance.
406
407 """
408 window = self._open_window(app_name, files, locale)
409 if window:
410 self.addCleanup(window.close)
411 return window
412 raise AssertionError("No window was opened.")
413
414 def _open_window(self, app_name, files, locale):
415 """Open a new 'app_name' window, returning the window instance or None.
416
417 Raises an AssertionError if this creates more than one window.
418
419 """
420 existing_windows = self.get_open_windows_by_application(app_name)
421
422 if locale:
423 os.putenv("LC_ALL", locale)
424 self.addCleanup(os.unsetenv, "LC_ALL")
425 logger.info("Starting application '%s' with files %r in locale %s", app_name, files, locale)
426 else:
427 logger.info("Starting application '%s' with files %r", app_name, files)
428
429
430 app = self.KNOWN_APPS[app_name]
431 self.bamf.launch_application(app['desktop-file'], files)
432 apps = self.bamf.get_running_applications_by_desktop_file(app['desktop-file'])
433
434 for i in range(10):127 for i in range(10):
435 try:128 current_apps = self.process_manager.get_running_applications()
436 new_windows = []129 new_apps = filter(lambda i: i not in self._app_snapshot, current_apps)
437 [new_windows.extend(a.get_windows()) for a in apps]130 if not new_apps:
438 filter_fn = lambda w: w.x_id not in [c.x_id for c in existing_windows]131 self._app_snapshot = None
439 new_wins = filter(filter_fn, new_windows)132 return
440 if new_wins:133 sleep(1)
441 assert len(new_wins) == 1134 self._app_snapshot = None
442 return new_wins[0]135 raise AssertionError("The following apps were started during the test and not closed: %r", new_apps)
443 except DBusException:
444 pass
445 time.sleep(1)
446 return None
447
448 def get_open_windows_by_application(self, app_name):
449 """Get a list of BamfWindow instances for the given application name.
450
451 :param app_name: The name of one of the well-known applications.
452 :returns: A list of :class:`~autopilot.emulators.bamf.BamfWindow`
453 instances.
454
455 """
456 existing_windows = []
457 [existing_windows.extend(a.get_windows()) for a in self.get_app_instances(app_name)]
458 return existing_windows
459
460 def close_all_app(self, app_name):
461 """Close all instances of the application 'app_name'."""
462 app = self.KNOWN_APPS[app_name]
463 try:
464 pids = check_output(["pidof", app['process-name']]).split()
465 if len(pids):
466 call(["kill"] + pids)
467 except CalledProcessError:
468 logger.warning("Tried to close applicaton '%s' but it wasn't running.", app_name)
469
470 def get_app_instances(self, app_name):
471 """Get BamfApplication instances for app_name."""
472 desktop_file = self.KNOWN_APPS[app_name]['desktop-file']
473 return self.bamf.get_running_applications_by_desktop_file(desktop_file)
474
475 def app_is_running(self, app_name):
476 """Return true if an instance of the application is running."""
477 apps = self.get_app_instances(app_name)
478 return len(apps) > 0
479136
480 def patch_environment(self, key, value):137 def patch_environment(self, key, value):
481 """Patch the process environment, setting *key* with value *value*.138 """Patch the process environment, setting *key* with value *value*.
@@ -513,12 +170,13 @@
513170
514 .. note:: Minimised windows are skipped.171 .. note:: Minimised windows are skipped.
515172
516 :param stack_start: An iterable of BamfWindow instances.173 :param stack_start: An iterable of
174 `~autopilot.process.Window` instances.
517 :raises: **AssertionError** if the top of the window stack does not175 :raises: **AssertionError** if the top of the window stack does not
518 match the contents of the stack_start parameter.176 match the contents of the stack_start parameter.
519177
520 """178 """
521 stack = [win for win in self.bamf.get_open_windows() if not win.is_hidden]179 stack = [win for win in self.process_manager.get_open_windows() if not win.is_hidden]
522 for pos, win in enumerate(stack_start):180 for pos, win in enumerate(stack_start):
523 self.assertThat(stack[pos].x_id, Equals(win.x_id),181 self.assertThat(stack[pos].x_id, Equals(win.x_id),
524 "%r at %d does not equal %r" % (stack[pos], pos, win))182 "%r at %d does not equal %r" % (stack[pos], pos, win))
@@ -531,7 +189,7 @@
531 the autopilot DBus interface).189 the autopilot DBus interface).
532190
533 For example, from within a test, to assert certain properties on a191 For example, from within a test, to assert certain properties on a
534 BamfWindow instance::192 `~autopilot.process.Window` instance::
535193
536 self.assertProperty(my_window, is_maximized=True)194 self.assertProperty(my_window, is_maximized=True)
537195
@@ -561,3 +219,83 @@
561 self.assertThat(lambda: getattr(obj, prop_name), Eventually(Equals(desired_value)))219 self.assertThat(lambda: getattr(obj, prop_name), Eventually(Equals(desired_value)))
562220
563 assertProperties = assertProperty221 assertProperties = assertProperty
222
223 def launch_test_application(self, application, *arguments, **kwargs):
224 """Launch ``application`` and return a proxy object for the application.
225
226 Use this method to launch an application and start testing it. The
227 positional arguments are used as arguments to the application to lanch.
228 Keyword arguments are used to control the manner in which the application
229 is launched.
230
231 This method is designed to be flexible enough to launch all supported
232 types of applications. For example, to launch a traditional Gtk application,
233 a test might start with::
234
235 app_proxy = self.launch_test_application('gedit')
236
237 ... a Qt4 Qml application might be launched like this::
238
239 app_proxy = self.launch_test_application('qmlviewer', 'my_scene.qml')
240
241 ... a Qt5 Qml application is launched in a similar fashion::
242
243 app_proxy = self.launch_test_application('qmlscene', 'my_scene.qml')
244
245 :param application: The application to launch. The application can be
246 specified as:
247
248 * A full, absolute path to an executable file. (``/usr/bin/gedit``)
249 * A relative path to an executable file. (``./build/my_app``)
250 * An app name, which will be searched for in $PATH (``my_app``)
251
252 :keyword launch_dir: If set to a directory that exists the process will be
253 launched from that directory.
254
255 :keyword capture_output: If set to True (the default), the process output
256 will be captured and attached to the test as test detail.
257
258 :raises: **ValueError** if unknown keyword arguments are passed.
259 :return: A proxy object that represents the application. Introspection
260 data is retrievable via this object.
261
262 """
263 # first, we get a launcher. Tests can override this if they need:
264 launcher = self.pick_app_launcher(application)
265 if launcher is None:
266 raise RuntimeError("Autopilot could not determine the correct \
267 introspection type to use. You can specify one by overriding \
268 the AutopilotTestCase.pick_app_launcher method.")
269 process = launch_application(launcher, application, *arguments, **kwargs)
270 self.addCleanup(self._kill_process_and_attach_logs, process)
271 return get_autopilot_proxy_object_for_process(process)
272
273 def pick_app_launcher(self, app_path):
274 """Given an application path, return an object suitable for launching
275 the application.
276
277 This function attempts to guess what kind of application you are
278 launching. If, for some reason the default implementation returns the
279 wrong launcher, test authors may override this method to provide their
280 own implemetnation.
281
282 The default implementation calls
283 :py:func:`autopilot.introspection.get_application_launcher`
284
285 """
286 # default implementation is in autopilot.introspection:
287 return get_application_launcher(app_path)
288
289 def _kill_process_and_attach_logs(self, process):
290 process.kill()
291 logger.info("waiting for process to exit.")
292 for i in range(10):
293 if process.returncode is not None:
294 break
295 if i == 9:
296 logger.info("Terminating process group, since it hasn't exited after 10 seconds.")
297 os.killpg(process.pid, signal.SIGTERM)
298 sleep(1)
299 stdout, stderr = process.communicate()
300 self.addDetail('process-stdout', text_content(stdout))
301 self.addDetail('process-stderr', text_content(stderr))
564302
=== modified file 'autopilot/tests/test_ap_apps.py'
--- autopilot/tests/test_ap_apps.py 2013-02-22 03:59:51 +0000
+++ autopilot/tests/test_ap_apps.py 2013-04-15 21:43:32 +0000
@@ -13,8 +13,7 @@
13from textwrap import dedent13from textwrap import dedent
1414
15from autopilot.testcase import AutopilotTestCase15from autopilot.testcase import AutopilotTestCase
16from autopilot.introspection.gtk import GtkIntrospectionTestMixin16from autopilot.introspection.gtk import GtkApplicationLauncher
17from autopilot.introspection.qt import QtIntrospectionTestMixin
1817
1918
20class ApplicationTests(AutopilotTestCase):19class ApplicationTests(AutopilotTestCase):
@@ -33,15 +32,20 @@
33 return path32 return path
3433
3534
36class QtTests(ApplicationTests, QtIntrospectionTestMixin):35class QtTests(ApplicationTests):
3736
38 def setUp(self):37 def setUp(self):
39 super(QtTests, self).setUp()38 super(QtTests, self).setUp()
4039
41 try:40 try:
42 self.app_path = subprocess.check_output(['which','qmlviewer']).strip()41 self.app_path = subprocess.check_output(['which','qmlscene']).strip()
43 except subprocess.CalledProcessError:42 except subprocess.CalledProcessError:
44 self.skip("qmlviewer not found.")43 self.skip("qmlscene not found.")
44
45 def pick_app_launcher(self, app_path):
46 # force Qt app introspection:
47 from autopilot.introspection.qt import QtApplicationLauncher
48 return QtApplicationLauncher()
4549
46 def test_can_launch_qt_app(self):50 def test_can_launch_qt_app(self):
47 app_proxy = self.launch_test_application(self.app_path)51 app_proxy = self.launch_test_application(self.app_path)
@@ -83,7 +87,7 @@
83 self.assertTrue(app_proxy is not None)87 self.assertTrue(app_proxy is not None)
8488
8589
86class GtkTests(ApplicationTests, GtkIntrospectionTestMixin):90class GtkTests(ApplicationTests):
8791
88 def setUp(self):92 def setUp(self):
89 super(GtkTests, self).setUp()93 super(GtkTests, self).setUp()
9094
=== modified file 'autopilot/tests/test_application_mixin.py'
--- autopilot/tests/test_application_mixin.py 2012-09-13 02:28:04 +0000
+++ autopilot/tests/test_application_mixin.py 2013-04-15 21:43:32 +0000
@@ -8,45 +8,33 @@
88
9from __future__ import absolute_import9from __future__ import absolute_import
1010
11from testtools import TestCase11from autopilot.testcase import AutopilotTestCase
12from testtools.matchers import Equals, Is, Not, raises12from testtools.matchers import Is, Not, raises
13from mock import patch
14
15from autopilot.introspection.qt import QtIntrospectionTestMixin
16
1713
18def dummy_addCleanup(*args, **kwargs):14def dummy_addCleanup(*args, **kwargs):
19 pass15 pass
2016
2117
22class ApplicationSupportTests(TestCase):18class ApplicationSupportTests(AutopilotTestCase):
23
24 def test_can_create(self):
25 mixin = QtIntrospectionTestMixin()
26 self.assertThat(mixin, Not(Is(None)))
2719
28 def test_launch_with_bad_types_raises_typeerror(self):20 def test_launch_with_bad_types_raises_typeerror(self):
29 """Calling launch_test_application with something other than a string must21 """Calling launch_test_application with something other than a string must
30 raise a TypeError"""22 raise a TypeError"""
3123
32 mixin = QtIntrospectionTestMixin()24 self.assertThat(lambda: self.launch_test_application(1), raises(TypeError))
33 mixin.addCleanup = dummy_addCleanup25 self.assertThat(lambda: self.launch_test_application(True), raises(TypeError))
3426 self.assertThat(lambda: self.launch_test_application(1.0), raises(TypeError))
35 self.assertThat(lambda: mixin.launch_test_application(1), raises(TypeError))27 self.assertThat(lambda: self.launch_test_application(object()), raises(TypeError))
36 self.assertThat(lambda: mixin.launch_test_application(True), raises(TypeError))28 self.assertThat(lambda: self.launch_test_application(None), raises(TypeError))
37 self.assertThat(lambda: mixin.launch_test_application(1.0), raises(TypeError))29 self.assertThat(lambda: self.launch_test_application([]), raises(TypeError))
38 self.assertThat(lambda: mixin.launch_test_application(object()), raises(TypeError))30 self.assertThat(lambda: self.launch_test_application((None,)), raises(TypeError))
39 self.assertThat(lambda: mixin.launch_test_application(None), raises(TypeError))
40 self.assertThat(lambda: mixin.launch_test_application([]), raises(TypeError))
41 self.assertThat(lambda: mixin.launch_test_application((None,)), raises(TypeError))
4231
43 def test_launch_raises_ValueError_on_unknown_kwargs(self):32 def test_launch_raises_ValueError_on_unknown_kwargs(self):
44 """launch_test_application must raise ValueError when given unknown33 """launch_test_application must raise ValueError when given unknown
45 keyword arguments.34 keyword arguments.
4635
47 """36 """
48 mixin = QtIntrospectionTestMixin()37 fn = lambda: self.launch_test_application('gedit', arg1=123, arg2='asd')
49 fn = lambda: mixin.launch_test_application('gedit', arg1=123, arg2='asd')
50 self.assertThat(fn, raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'.")))38 self.assertThat(fn, raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'.")))
5139
52 def test_launch_raises_ValueError_on_unknown_kwargs_with_known(self):40 def test_launch_raises_ValueError_on_unknown_kwargs_with_known(self):
@@ -54,6 +42,5 @@
54 keyword arguments.42 keyword arguments.
5543
56 """44 """
57 mixin = QtIntrospectionTestMixin()45 fn = lambda: self.launch_test_application('gedit', arg1=123, arg2='asd', launch_dir='/')
58 fn = lambda: mixin.launch_test_application('gedit', arg1=123, arg2='asd', launch_dir='/')
59 self.assertThat(fn, raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'.")))46 self.assertThat(fn, raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'.")))
6047
=== removed file 'autopilot/tests/test_application_registration.py'
--- autopilot/tests/test_application_registration.py 2012-09-30 22:39:30 +0000
+++ autopilot/tests/test_application_registration.py 1970-01-01 00:00:00 +0000
@@ -1,94 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2012 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9from __future__ import absolute_import
10
11from testtools import TestCase
12from testtools.matchers import Equals, Is, Not, raises, Contains
13from mock import patch
14
15from autopilot.testcase import AutopilotTestCase
16
17
18def safe_unregister_application(test_case_class, app_name):
19 if app_name in test_case_class.KNOWN_APPS:
20 test_case_class.unregister_known_application(app_name)
21
22class ApplicationRegistrationTests(TestCase):
23
24 def test_can_register_new_application(self):
25 AutopilotTestCase.register_known_application(
26 "NewApplicationName",
27 "newapp.desktop",
28 "newapp")
29 self.addCleanup(safe_unregister_application,
30 AutopilotTestCase,
31 "NewApplicationName")
32
33 app_details = AutopilotTestCase.KNOWN_APPS['NewApplicationName']
34
35 self.assertThat(AutopilotTestCase.KNOWN_APPS, Contains("NewApplicationName"))
36 self.assertTrue(type(app_details) is dict)
37 self.assertThat(app_details, Contains('desktop-file'))
38 self.assertThat(app_details, Contains('process-name'))
39 self.assertThat(app_details['desktop-file'], Equals('newapp.desktop'))
40 self.assertThat(app_details['process-name'], Equals('newapp'))
41
42 def test_registering_app_twice_raises_KeyError(self):
43 """Registering an application with the same app name as one that's
44 already in the dictionary must raise KeyError and not change the
45 dictionary.
46
47 """
48 AutopilotTestCase.register_known_application(
49 "NewApplicationName",
50 "newapp.desktop",
51 "newapp")
52 self.addCleanup(safe_unregister_application,
53 AutopilotTestCase,
54 "NewApplicationName")
55
56 app_details = AutopilotTestCase.KNOWN_APPS['NewApplicationName']
57 register_fn = lambda: AutopilotTestCase.register_known_application(
58 "NewApplicationName",
59 "newapp2.desktop",
60 "newapp2")
61
62 self.assertThat(register_fn, raises(
63 KeyError("Application has been registered already")))
64 self.assertThat(AutopilotTestCase.KNOWN_APPS, Contains("NewApplicationName"))
65 self.assertTrue(type(app_details) is dict)
66 self.assertThat(app_details, Contains('desktop-file'))
67 self.assertThat(app_details, Contains('process-name'))
68 self.assertThat(app_details['desktop-file'], Equals('newapp.desktop'))
69 self.assertThat(app_details['process-name'], Equals('newapp'))
70
71 def test_can_unregister_application(self):
72 AutopilotTestCase.register_known_application(
73 "NewApplicationName",
74 "newapp.desktop",
75 "newapp")
76 self.addCleanup(safe_unregister_application,
77 AutopilotTestCase,
78 "NewApplicationName")
79
80 AutopilotTestCase.unregister_known_application("NewApplicationName")
81
82 self.assertThat(AutopilotTestCase.KNOWN_APPS,
83 Not(Contains("NewApplicationName")))
84
85 def test_unregistering_unknown_application_raises_KeyError(self):
86 """Trying to unregister an application that is not already registered
87 must raise a KeyError.
88
89 """
90
91 unregister_fn = lambda: AutopilotTestCase.unregister_known_application("FooBarBaz")
92
93 self.assertThat(unregister_fn, raises(KeyError("Application has not been registered")))
94
950
=== removed file 'autopilot/tests/test_compiz_key_translate.py'
--- autopilot/tests/test_compiz_key_translate.py 2012-05-08 16:14:53 +0000
+++ autopilot/tests/test_compiz_key_translate.py 1970-01-01 00:00:00 +0000
@@ -1,69 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2012 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9from __future__ import absolute_import
10
11from testscenarios import TestWithScenarios
12from testtools import TestCase
13from testtools.matchers import raises, Equals
14
15from autopilot.keybindings import _translate_compiz_keystroke_string as translate_func
16
17class KeyTranslateArgumentTests(TestWithScenarios, TestCase):
18 """Tests that the compizconfig keycode translation routes work as advertised."""
19
20 scenarios = [
21 ('bool', {'input': True}),
22 ('int', {'input': 42}),
23 ('float', {'input': 0.321}),
24 ('none', {'input': None}),
25 ]
26
27 def test_requires_string_instance(self):
28 """Function must raise TypeError unless given an instance of basestring."""
29 self.assertThat(lambda: translate_func(self.input), raises(TypeError))
30
31
32class TranslationTests(TestWithScenarios, TestCase):
33 """Test that we get the result we expect, with the given input."""
34
35 scenarios = [
36 ('empty string', dict(input='', expected='')),
37 ('single simpe letter', dict(input='a', expected='a')),
38 ('trailing space', dict(input='d ', expected='d')),
39 ('only whitespace', dict(input='\t\n ', expected='')),
40 ('special key: Ctrl', dict(input='<Control>', expected='Ctrl')),
41 ('special key: Primary', dict(input='<Primary>', expected='Ctrl')),
42 ('special key: Alt', dict(input='<Alt>', expected='Alt')),
43 ('special key: Shift', dict(input='<Shift>', expected='Shift')),
44 ('direction key up', dict(input='Up', expected='Up')),
45 ('direction key down', dict(input='Down', expected='Down')),
46 ('direction key left', dict(input='Left', expected='Left')),
47 ('direction key right', dict(input='Right', expected='Right')),
48 ('Ctrl+a', dict(input='<Control>a', expected='Ctrl+a')),
49 ('Primary+a', dict(input='<Control>a', expected='Ctrl+a')),
50 ('Shift+s', dict(input='<Shift>s', expected='Shift+s')),
51 ('Alt+d', dict(input='<Alt>d', expected='Alt+d')),
52 ('Super+w', dict(input='<Super>w', expected='Super+w')),
53 ('Ctrl+Up', dict(input='<Control>Up', expected='Ctrl+Up')),
54 ('Primary+Down', dict(input='<Control>Down', expected='Ctrl+Down')),
55 ('Alt+Left', dict(input='<Alt>Left', expected='Alt+Left')),
56 ('Shift+F3', dict(input='<Shift>F3', expected='Shift+F3')),
57 ('duplicate keys Ctrl+Ctrl', dict(input='<Control><Control>', expected='Ctrl')),
58 ('duplicate keys Ctrl+Primary', dict(input='<Control><Primary>', expected='Ctrl')),
59 ('duplicate keys Ctrl+Primary', dict(input='<Primary><Control>', expected='Ctrl')),
60 ('duplicate keys Alt+Alt', dict(input='<Alt><Alt>', expected='Alt')),
61 ('duplicate keys Ctrl+Primary+left', dict(input='<Control><Primary>Left', expected='Ctrl+Left')),
62 ('first key wins', dict(input='<Control><Alt>Down<Alt>', expected='Ctrl+Alt+Down')),
63 ('Getting silly now', dict(input='<Control><Primary><Shift><Shift><Alt>Left', expected='Ctrl+Shift+Alt+Left')),
64 ]
65
66 def test_translation(self):
67 self.assertThat(translate_func(self.input), Equals(self.expected))
68
69
700
=== removed file 'autopilot/tests/test_compiz_option_support.py'
--- autopilot/tests/test_compiz_option_support.py 2012-08-20 03:59:45 +0000
+++ autopilot/tests/test_compiz_option_support.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2012 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9from __future__ import absolute_import
10
11from testtools.matchers import Equals, raises, Not
12
13from autopilot.testcase import AutopilotTestCase
14import logging
15logger = logging.getLogger(__name__)
16
17
18
19class CompizConfigOptionTests(AutopilotTestCase):
20
21 def test_set_option_raises_KeyError_on_bad_plugin_name(self):
22 """set_compiz_option must raise KeyError when given a bad plugin name."""
23 fn = lambda: self.set_compiz_option('rubbishpluginname', 'rubbishsettingname', 'settingvalue')
24 self.assertThat(fn, raises(KeyError("Compiz plugin 'rubbishpluginname' does not exist.")))
25
26 def test_set_option_raises_KeyError_on_bad_setting_name(self):
27 """set_compiz_option must raise KeyError when called with a bad setting name."""
28 fn = lambda: self.set_compiz_option('core', 'rubbishsettingname', 'settingvalue')
29 self.assertThat(fn, raises(KeyError("Compiz setting 'rubbishsettingname' does not exist in plugin 'core'.")))
30
31
320
=== modified file 'autopilot/tests/test_keyboard.py'
--- autopilot/tests/test_keyboard.py 2012-12-04 01:37:07 +0000
+++ autopilot/tests/test_keyboard.py 2013-04-15 21:43:32 +0000
@@ -30,7 +30,7 @@
3030
31 def test_keyboard_types_correct_characters(self):31 def test_keyboard_types_correct_characters(self):
32 """Verify that the keyboard.type method types what we expect."""32 """Verify that the keyboard.type method types what we expect."""
33 self.start_app_window('Terminal')33 self.process_manager.start_app_window('Terminal')
34 filename = mktemp()34 filename = mktemp()
35 self.keyboard.type('''python -c "open('%s','w').write(raw_input())"''' % filename)35 self.keyboard.type('''python -c "open('%s','w').write(raw_input())"''' % filename)
36 self.keyboard.press_and_release('Enter')36 self.keyboard.press_and_release('Enter')
@@ -46,7 +46,7 @@
46 expect.46 expect.
4747
48 """48 """
49 self.start_app_window('Terminal')49 self.process_manager.start_app_window('Terminal')
50 filename = mktemp()50 filename = mktemp()
51 self.keyboard.type('''python -c "open('%s','w').write(raw_input())"''' % filename)51 self.keyboard.type('''python -c "open('%s','w').write(raw_input())"''' % filename)
52 self.keyboard.press_and_release('Enter')52 self.keyboard.press_and_release('Enter')
5353
=== modified file 'autopilot/tests/test_mouse_emulator.py'
--- autopilot/tests/test_mouse_emulator.py 2013-02-28 04:16:14 +0000
+++ autopilot/tests/test_mouse_emulator.py 2013-04-15 21:43:32 +0000
@@ -10,7 +10,7 @@
10from testtools.matchers import Equals, raises10from testtools.matchers import Equals, raises
11from mock import patch11from mock import patch
1212
13from autopilot.emulators.input import get_mouse13from autopilot.input import Mouse
1414
15class Empty(object):15class Empty(object):
16 pass16 pass
@@ -31,7 +31,7 @@
3131
32 def setUp(self):32 def setUp(self):
33 super(MouseEmulatorTests, self).setUp()33 super(MouseEmulatorTests, self).setUp()
34 self.mouse = get_mouse()34 self.mouse = Mouse.create()
3535
36 def tearDown(self):36 def tearDown(self):
37 super(MouseEmulatorTests, self).tearDown()37 super(MouseEmulatorTests, self).tearDown()
3838
=== modified file 'autopilot/tests/test_open_window.py'
--- autopilot/tests/test_open_window.py 2012-07-08 23:29:30 +0000
+++ autopilot/tests/test_open_window.py 2013-04-15 21:43:32 +0000
@@ -11,23 +11,24 @@
11from testtools.matchers import Equals11from testtools.matchers import Equals
1212
13from autopilot.testcase import AutopilotTestCase13from autopilot.testcase import AutopilotTestCase
14from autopilot.process import ProcessManager
14import logging15import logging
15logger = logging.getLogger(__name__)16logger = logging.getLogger(__name__)
1617
1718
18class OpenWindowTests(AutopilotTestCase):19class OpenWindowTests(AutopilotTestCase):
1920
20 scenarios = [(k, {'app_name': k}) for k in AutopilotTestCase.KNOWN_APPS.iterkeys()]21 scenarios = [(k, {'app_name': k}) for k in ProcessManager.KNOWN_APPS.iterkeys()]
2122
22 def test_open_window(self):23 def test_open_window(self):
23 """self.start_app_window must open a new window of the given app."""24 """self.start_app_window must open a new window of the given app."""
24 existing_apps = self.get_app_instances(self.app_name)25 existing_apps = self.process_manager.get_app_instances(self.app_name)
25 old_wins = []26 old_wins = []
26 for app in existing_apps:27 for app in existing_apps:
27 old_wins.extend(app.get_windows())28 old_wins.extend(app.get_windows())
28 logger.debug("Old windows: %r", old_wins)29 logger.debug("Old windows: %r", old_wins)
2930
30 win = self.start_app_window(self.app_name)31 win = self.process_manager.start_app_window(self.app_name)
31 logger.debug("New window: %r", win)32 logger.debug("New window: %r", win)
32 is_new = win.x_id not in [w.x_id for w in old_wins]33 is_new = win.x_id not in [w.x_id for w in old_wins]
33 self.assertThat(is_new, Equals(True))34 self.assertThat(is_new, Equals(True))
3435
=== added file 'autopilot/tests/test_out_of_test_addcleanup.py'
--- autopilot/tests/test_out_of_test_addcleanup.py 1970-01-01 00:00:00 +0000
+++ autopilot/tests/test_out_of_test_addcleanup.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,34 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9from testtools import TestCase
10from testtools.matchers import Equals
11
12from autopilot.testcase import AutopilotTestCase
13from autopilot.utilities import addCleanup
14
15log = ''
16
17class AddCleanupTests(TestCase):
18
19 def test_addCleanup_called_with_args_and_kwargs(self):
20 """Test that out-of-test addClenaup works as expected, and is passed both
21 args and kwargs.
22
23 """
24 class InnerTest(AutopilotTestCase):
25 def write_to_log(self, *args, **kwargs):
26 global log
27 log = "Hello %r %r" % (args, kwargs)
28
29 def test_foo(self):
30 addCleanup(self.write_to_log, "arg1", 2, foo='bar')
31
32 InnerTest('test_foo').run()
33 self.assertThat(log, Equals("Hello ('arg1', 2) {'foo': 'bar'}"))
34
035
=== added file 'autopilot/tests/test_platform.py'
--- autopilot/tests/test_platform.py 1970-01-01 00:00:00 +0000
+++ autopilot/tests/test_platform.py 2013-04-15 21:43:32 +0000
@@ -0,0 +1,134 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2013 Canonical
3# Author: Thomi Richards
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8
9"Tests for the autopilot platform code."
10
11import autopilot.platform as platform
12
13from StringIO import StringIO
14from testtools import TestCase
15from testtools.matchers import Equals
16
17from mock import patch
18
19class PlatformDetectorTests(TestCase):
20
21 def tearDown(self):
22 super(PlatformDetectorTests, self).tearDown()
23 # platform detector is cached, so destroy the cache at the end of each
24 # test:
25 platform._PlatformDetector._cached_detector = None
26
27 def test_platform_detector_is_cached(self):
28 """Test that the platform detector is only created once."""
29 detector1 = platform._PlatformDetector.create()
30 detector2 = platform._PlatformDetector.create()
31 self.assertThat(id(detector1), Equals(id(detector2)))
32
33 @patch('autopilot.platform._get_property_file')
34 def test_default_model(self, mock_get_property_file):
35 """The default model name must be 'Desktop'."""
36 mock_get_property_file.return_value = None
37
38 detector = platform._PlatformDetector.create()
39 self.assertThat(detector.model, Equals('Desktop'))
40
41 @patch('autopilot.platform._get_property_file')
42 def test_default_image_codename(self, mock_get_property_file):
43 """The default image codename must be 'Desktop'."""
44 mock_get_property_file.return_value = None
45
46 detector = platform._PlatformDetector.create()
47 self.assertThat(detector.image_codename, Equals('Desktop'))
48
49 @patch('autopilot.platform._get_property_file')
50 def test_model_is_set_from_property_file(self, mock_get_property_file):
51 """Detector must read product model from android properties file."""
52 mock_get_property_file.return_value = StringIO("ro.product.model=test123")
53
54 detector = platform._PlatformDetector.create()
55 self.assertThat(detector.model, Equals('test123'))
56
57 @patch('autopilot.platform._get_property_file', new=lambda: StringIO(""))
58 def test_model_has_default_when_not_in_property_file(self):
59 """Detector must use 'Desktop' as a default value for the model name
60 when the property file exists, but does not contain a model description.
61
62 """
63 detector = platform._PlatformDetector.create()
64 self.assertThat(detector.model, Equals('Desktop'))
65
66 @patch('autopilot.platform._get_property_file')
67 def test_product_codename_is_set_from_property_file(self, mock_get_property_file):
68 """Detector must read product model from android properties file."""
69 mock_get_property_file.return_value = StringIO("ro.product.name=test123")
70
71 detector = platform._PlatformDetector.create()
72 self.assertThat(detector.image_codename, Equals('test123'))
73
74 @patch('autopilot.platform._get_property_file', new=lambda: StringIO(""))
75 def test_product_codename_has_default_when_not_in_property_file(self):
76 """Detector must use 'Desktop' as a default value for the product codename
77 when the property file exists, but does not contain a model description.
78
79 """
80 detector = platform._PlatformDetector.create()
81 self.assertThat(detector.image_codename, Equals('Desktop'))
82
83
84class BuildPropertyParserTests(TestCase):
85
86 """Tests for the android build properties file parser."""
87
88 def test_empty_file_returns_empty_dictionary(self):
89 """An empty file must result in an empty dictionary."""
90 prop_file = StringIO("")
91 properties = platform._parse_build_properties_file(prop_file)
92 self.assertThat(len(properties), Equals(0))
93
94 def test_whitespace_is_ignored(self):
95 """Whitespace in build file must be ignored."""
96 prop_file = StringIO("\n\n\n\n\n")
97 properties = platform._parse_build_properties_file(prop_file)
98 self.assertThat(len(properties), Equals(0))
99
100 def test_comments_are_ignored(self):
101 """Comments in build file must be ignored."""
102 prop_file = StringIO("# Hello World\n #Hello Again\n#####")
103 properties = platform._parse_build_properties_file(prop_file)
104 self.assertThat(len(properties), Equals(0))
105
106 def test_invalid_lines_are_ignored(self):
107 """lines without ana ssignment must be ignored."""
108 prop_file = StringIO("Hello")
109 properties = platform._parse_build_properties_file(prop_file)
110 self.assertThat(len(properties), Equals(0))
111
112 def test_simple_value(self):
113 """Test a simple a=b expression."""
114 prop_file = StringIO("a=b")
115 properties = platform._parse_build_properties_file(prop_file)
116 self.assertThat(properties, Equals(dict(a='b')))
117
118 def test_multiple_values(self):
119 """Test several expressions over multiple lines."""
120 prop_file = StringIO("a=b\nb=23")
121 properties = platform._parse_build_properties_file(prop_file)
122 self.assertThat(properties, Equals(dict(a='b',b='23')))
123
124 def test_values_with_equals_in_them(self):
125 """Test that we can parse values with a '=' in them."""
126 prop_file = StringIO("a=b=c")
127 properties = platform._parse_build_properties_file(prop_file)
128 self.assertThat(properties, Equals(dict(a='b=c')))
129
130 def test_dotted_values_work(self):
131 """Test that we can use dotted values as the keys."""
132 prop_file = StringIO("ro.product.model=maguro")
133 properties = platform._parse_build_properties_file(prop_file)
134 self.assertThat(properties, Equals({'ro.product.model':'maguro'}))
0135
=== renamed file 'autopilot/tests/test_bamf_emulator.py' => 'autopilot/tests/test_process_emulator.py'
--- autopilot/tests/test_bamf_emulator.py 2013-01-06 21:39:32 +0000
+++ autopilot/tests/test_process_emulator.py 2013-04-15 21:43:32 +0000
@@ -15,11 +15,11 @@
15from time import sleep, time15from time import sleep, time
1616
1717
18class BamfEmulatorTests(AutopilotTestCase):18class ProcessEmulatorTests(AutopilotTestCase):
1919
20 def ensure_gedit_not_running(self):20 def ensure_gedit_not_running(self):
21 """Close any open gedit applications."""21 """Close any open gedit applications."""
22 apps = self.bamf.get_running_applications_by_desktop_file('gedit.desktop')22 apps = self.process_manager.get_running_applications_by_desktop_file('gedit.desktop')
23 if apps:23 if apps:
24 # this is a bit brutal, but easier in this context than the alternative.24 # this is a bit brutal, but easier in this context than the alternative.
25 call(['killall', 'gedit'])25 call(['killall', 'gedit'])
@@ -34,7 +34,7 @@
34 start = time()34 start = time()
35 t = Thread(target=start_gedit())35 t = Thread(target=start_gedit())
36 t.start()36 t.start()
37 ret = self.bamf.wait_until_application_is_running('gedit.desktop', 10)37 ret = self.process_manager.wait_until_application_is_running('gedit.desktop', 10)
38 end = time()38 end = time()
39 t.join()39 t.join()
4040
@@ -46,7 +46,7 @@
46 self.ensure_gedit_not_running()46 self.ensure_gedit_not_running()
4747
48 start = time()48 start = time()
49 ret = self.bamf.wait_until_application_is_running('gedit.desktop', 5)49 ret = self.process_manager.wait_until_application_is_running('gedit.desktop', 5)
50 end = time()50 end = time()
5151
52 self.assertThat(abs(end - start - 5.0), LessThan(1))52 self.assertThat(abs(end - start - 5.0), LessThan(1))
5353
=== modified file 'autopilot/utilities.py'
--- autopilot/utilities.py 2013-03-03 21:21:08 +0000
+++ autopilot/utilities.py 2013-04-15 21:43:32 +0000
@@ -14,103 +14,26 @@
1414
15from __future__ import absolute_import15from __future__ import absolute_import
1616
17import inspect
17import logging18import logging
18import os19import os
19import sys
20import time20import time
21from functools import wraps21from functools import wraps
22from Xlib import X, display, protocol22
2323
24_display = None24def _pick_variant(variants, preferred_variant):
2525 possible_backends = variants.keys()
26def get_display():26 get_debug_logger().debug("Possible variants: %s", ','.join(possible_backends))
27 """Get a Xlib display object. Creating the display prints garbage to stdout."""27 if preferred_variant in possible_backends:
28 global _display28 possible_backends.sort(lambda a,b: -1 if a == preferred_variant else 0)
29 if _display is None:29 failure_reasons = []
30 with Silence():30 for be in possible_backends:
31 _display = display.Display()31 try:
32 return _display32 return variants[be]()
3333 except Exception as e:
3434 get_debug_logger().warning("Can't create variant %s: %r", be, e)
3535 failure_reasons.append('%s: %r' % (be, e))
36def make_window_skip_taskbar(window, set_flag=True):36 raise RuntimeError("Unable to instantiate any backends\n%s" % '\n'.join(failure_reasons))
37 """Set the skip-taskbar kint on an X11 window.
38
39 'window' should be an Xlib window object.
40 set_flag should be 'True' to set the flag, 'False' to clear it.
41
42 """
43 state = get_display().get_atom('_NET_WM_STATE_SKIP_TASKBAR', 1)
44 action = int(set_flag)
45 if action == 0:
46 print "Clearing flag"
47 elif action == 1:
48 print "Setting flag"
49 _setProperty('_NET_WM_STATE', [action, state, 0, 1], window)
50 get_display().sync()
51
52
53def get_desktop_viewport():
54 """Get the x,y coordinates for the current desktop viewport top-left corner."""
55 return _getProperty('_NET_DESKTOP_VIEWPORT')
56
57
58def get_desktop_geometry():
59 """Get the full width and height of the desktop, including all the viewports."""
60 return _getProperty('_NET_DESKTOP_GEOMETRY')
61
62
63def _setProperty(_type, data, win=None, mask=None):
64 """ Send a ClientMessage event to a window"""
65 if not win:
66 win = get_display().screen().root
67 if type(data) is str:
68 dataSize = 8
69 else:
70 # data length must be 5 - pad with 0's if it's short, truncate otherwise.
71 data = (data + [0] * (5 - len(data)))[:5]
72 dataSize = 32
73
74 ev = protocol.event.ClientMessage(window=win,
75 client_type=get_display().get_atom(_type),
76 data=(dataSize, data))
77
78 if not mask:
79 mask = (X.SubstructureRedirectMask | X.SubstructureNotifyMask)
80 get_display().screen().root.send_event(ev, event_mask=mask)
81
82
83def _getProperty(_type, win=None):
84 if not win:
85 win = get_display().screen().root
86 atom = win.get_full_property(get_display().get_atom(_type), X.AnyPropertyType)
87 if atom: return atom.value
88
89
90def get_compiz_setting(plugin_name, setting_name):
91 """Get a compiz setting object.
92
93 'plugin_name' is the name of the plugin (e.g. 'core' or 'unityshell')
94 'setting_name' is the name of the setting (e.g. 'alt_tab_timeout')
95
96 This function will raise KeyError if the plugin or setting named does not
97 exist.
98
99 """
100 # circular dependancy:
101 from autopilot.compizconfig import get_setting
102 return get_setting(plugin_name, setting_name)
103
104
105def get_compiz_option(plugin_name, setting_name):
106 """Get a compiz setting value.
107
108 This is the same as calling:
109
110 >>> get_compiz_setting(plugin_name, setting_name).Value
111
112 """
113 return get_compiz_setting(plugin_name, setting_name).Value
11437
11538
116# Taken from http://code.activestate.com/recipes/577564-context-manager-for-low-level-redirection-of-stdou/39# Taken from http://code.activestate.com/recipes/577564-context-manager-for-low-level-redirection-of-stdou/
@@ -226,7 +149,33 @@
226 def fdec(fn):149 def fdec(fn):
227 @wraps(fn)150 @wraps(fn)
228 def wrapped(*args, **kwargs):151 def wrapped(*args, **kwargs):
229 sys.stderr.write("WARNING: This function is deprecated. Please use '%s' instead.\n" % alternative)152 import sys
153 outerframe_details = inspect.getouterframes(inspect.currentframe())[1]
154 filename, line_number, function_name = outerframe_details[1:4]
155 sys.stderr.write("WARNING: in file \"{0}\", line {1} in {2}\n".format(filename, line_number, function_name))
156 sys.stderr.write("This function is deprecated. Please use '%s' instead.\n" % alternative)
230 return fn(*args, **kwargs)157 return fn(*args, **kwargs)
231 return wrapped158 return wrapped
232 return fdec159 return fdec
160
161
162class _CleanupWrapper(object):
163 """Support for calling 'addCleanup' outside the test case."""
164
165 def __init__(self):
166 self._test_instance = None
167
168 def __call__(self, callable, *args, **kwargs):
169 if self._test_instance is None:
170 raise RuntimeError("Out-of-test addCleanup can only be called while an autopilot test case is running!")
171 self._test_instance.addCleanup(callable, *args, **kwargs)
172
173 def set_test_instance(self, test_instance):
174 self._test_instance = test_instance
175 test_instance.addCleanup(self._on_test_ended)
176
177 def _on_test_ended(self):
178 self._test_instance = None
179
180
181addCleanup = _CleanupWrapper()
233182
=== modified file 'bin/autopilot'
--- bin/autopilot 2013-02-21 02:37:04 +0000
+++ bin/autopilot 2013-04-15 21:43:32 +0000
@@ -27,13 +27,13 @@
27from argparse import ArgumentParser27from argparse import ArgumentParser
28from unittest.loader import TestLoader28from unittest.loader import TestLoader
29from unittest import TestSuite29from unittest import TestSuite
30from autopilot.introspection.gtk import GtkIntrospectionTestMixin30from autopilot.introspection import launch_application
31from autopilot.introspection.qt import QtIntrospectionTestMixin31from autopilot.introspection.gtk import GtkApplicationLauncher
32from autopilot.introspection.qt import QtApplicationLauncher
3233
33# list autopilot depends here, with the form:34# list autopilot depends here, with the form:
34# ('python module name', 'ubuntu package name'),35# ('python module name', 'ubuntu package name'),
35DEPENDS = [36DEPENDS = [
36 ('compizconfig', 'python-compizconfig'),
37 ('dbus', 'python-dbus'),37 ('dbus', 'python-dbus'),
38 ('gi.repository.GConf', 'gir1.2-gconf-2.0'),38 ('gi.repository.GConf', 'gir1.2-gconf-2.0'),
39 ('gi.repository.IBus', 'gir1.2-ibus-1.0'),39 ('gi.repository.IBus', 'gir1.2-ibus-1.0'),
@@ -345,27 +345,19 @@
345 exit(1)345 exit(1)
346346
347 # We now have a full path to the application.347 # We now have a full path to the application.
348 IntrospectionBase = None348 launcher = None
349 if args.interface == 'Auto':349 if args.interface == 'Auto':
350 IntrospectionBase = get_application_introspection_base(app_name)350 launcher = get_application_introspection_base(app_name)
351 elif args.interface == 'Gtk':351 elif args.interface == 'Gtk':
352 IntrospectionBase = GtkIntrospectionTestMixin352 launcher = GtkApplicationLauncher()
353 elif args.interface == 'Qt':353 elif args.interface == 'Qt':
354 IntrospectionBase = QtIntrospectionTestMixin354 launcher = QtApplicationLauncher()
355 if IntrospectionBase is None:355 if launcher is None:
356 print "Error: Could not determine introspection type to use for application '%s'." % app_name356 print "Error: Could not determine introspection type to use for application '%s'." % app_name
357 exit(1)357 exit(1)
358358
359 # IntrospectionBase is supposed to be a mixin class with a TestCase, and makes
360 # use of addCleanup to shut down the app after the test has completed. We
361 # patch that function here so we don't error...
362 def fake_cleanup(self, *args, **kwargs):
363 pass
364 IntrospectionBase.addCleanup = fake_cleanup
365
366 b = IntrospectionBase()
367 try:359 try:
368 b.launch_test_application(app_name, capture_output=False)360 launch_application(launcher, app_name, capture_output=False)
369 except RuntimeError as e:361 except RuntimeError as e:
370 print "Error: " + e.message362 print "Error: " + e.message
371 exit(1)363 exit(1)
@@ -387,9 +379,9 @@
387 print "Use the '-i' argument to specify an interface."379 print "Use the '-i' argument to specify an interface."
388 exit(1)380 exit(1)
389 if 'libqtcore' in ldd_output:381 if 'libqtcore' in ldd_output:
390 return QtIntrospectionTestMixin382 return QtApplicationLauncher
391 elif 'libgtk' in ldd_output:383 elif 'libgtk' in ldd_output:
392 return GtkIntrospectionTestMixin384 return GtkApplicationLauncher
393 return None385 return None
394386
395387
396388
=== modified file 'debian/changelog'
--- debian/changelog 2013-03-26 00:02:00 +0000
+++ debian/changelog 2013-04-15 21:43:32 +0000
@@ -1,3 +1,9 @@
1autopilot (1.3) UNRELEASED; urgency=low
2
3 * Create version 1.3 (LP: #1168971)
4
5 -- Thomi Richards <thomi.richards@canonical.com> Mon, 15 Apr 2013 09:33:21 +1200
6
1autopilot (1.2daily13.03.26-0ubuntu1) raring; urgency=low7autopilot (1.2daily13.03.26-0ubuntu1) raring; urgency=low
28
3 * Automatic snapshot from revision 1559 * Automatic snapshot from revision 155
410
=== modified file 'debian/control'
--- debian/control 2012-12-04 11:43:20 +0000
+++ debian/control 2013-04-15 21:43:32 +0000
@@ -9,6 +9,7 @@
9 gir1.2-gtk-2.0,9 gir1.2-gtk-2.0,
10 python (>= 2.6),10 python (>= 2.6),
11 python-dbus,11 python-dbus,
12 python-debian,
12 python-setuptools,13 python-setuptools,
13 python-sphinx,14 python-sphinx,
14 python-testtools,15 python-testtools,
@@ -28,7 +29,6 @@
28 gir1.2-glib-2.0,29 gir1.2-glib-2.0,
29 gir1.2-gtk-2.0,30 gir1.2-gtk-2.0,
30 gir1.2-ibus-1.0,31 gir1.2-ibus-1.0,
31 python-compizconfig,
32 python-dbus,32 python-dbus,
33 python-junitxml,33 python-junitxml,
34 python-qt4,34 python-qt4,
3535
=== added file 'docs/_templates/indexcontent.html'
--- docs/_templates/indexcontent.html 1970-01-01 00:00:00 +0000
+++ docs/_templates/indexcontent.html 2013-04-15 21:43:32 +0000
@@ -0,0 +1,45 @@
1{% extends "defindex.html" %}
2{% block tables %}
3<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>
4 <p><strong>Parts of the documentation:</strong></p>
5 <table class="contentstable" align="center">
6 <tr>
7 <td width="50%">
8 <p class="biglink">
9 <a class="biglink" href="{{ pathto("tutorial/tutorial") }}">Getting started with Autopilot</a><br/>
10 <span class="linkdescr">How to write your first Autopilot test.</span>
11 </p>
12 </td>
13 <td width="50%">
14 <p class="biglink">
15 <a class="biglink" href="{{ pathto("api/autopilot") }}">API Reference</a><br/>
16 <span class="linkdescr">API reference documentation for Autopilot.</span>
17 </p>
18 </td>
19 </tr>
20 <tr>
21 <td>
22 <p class="biglink">
23 <a class="biglink" href="{{ pathto("faq/faq") }}">Frequently Asked Questions</a><br/>
24 <span class="linkdescr">...with answers!</span>
25 </p>
26 </td>
27 <td>
28 <p class="biglink">
29 <a class="biglink" href="{{ pathto("porting/porting") }}">Porting Autopilot Tests</a><br/>
30 <span class="linkdescr">How to port your tests from earlier versions of Autopilot.</span>
31 </p>
32 </td>
33 </tr>
34 </table>
35
36 <p><strong>Indices and tables:</strong></p>
37 <table class="contentstable" align="center"><tr>
38 <td width="50%">
39 <p class="biglink"><a class="biglink" href="{{ pathto("py-modindex") }}">Module Index</a><br/>
40 <span class="linkdescr">quick access to all modules</span></p>
41{# <p class="biglink"><a class="biglink" href="{{ pathto("contents") }}">Complete Table of Contents</a><br/>
42 <span class="linkdescr">lists all sections and subsections</span></p> #}
43 </td></tr>
44 </table>
45{% endblock %}
046
=== modified file 'docs/api/autopilot.rst'
--- docs/api/autopilot.rst 2013-02-28 01:01:11 +0000
+++ docs/api/autopilot.rst 2013-04-15 21:43:32 +0000
@@ -1,103 +1,10 @@
1:orphan:
2
1Autopilot API Documentation3Autopilot API Documentation
2===========================4===========================
35
4Autopilot Utility Modules6.. toctree::
5+++++++++++++++++++++++++7 :maxdepth: 1
68 :glob:
7:mod:`testcase` Module9
8----------------------10 *
9
10.. autoclass:: autopilot.testcase.AutopilotTestCase
11 :members:
12
13.. autofunction:: autopilot.testcase.multiply_scenarios
14
15:mod:`keybindings` Module
16-------------------------
17
18.. automodule:: autopilot.keybindings
19 :members:
20 :undoc-members:
21 :show-inheritance:
22
23Emulators Package
24+++++++++++++++++
25
26:mod:`input` Module
27-------------------
28
29.. automodule:: autopilot.emulators.input
30 :members:
31 :undoc-members:
32 :show-inheritance:
33
34:mod:`bamf` Module
35------------------
36
37.. automodule:: autopilot.emulators.bamf
38 :members:
39 :undoc-members:
40 :show-inheritance:
41
42:mod:`ibus` Module
43------------------
44
45.. automodule:: autopilot.emulators.ibus
46 :members:
47 :undoc-members:
48 :show-inheritance:
49
50:mod:`zeitgeist` Module
51-----------------------
52
53.. automodule:: autopilot.emulators.zeitgeist
54 :members:
55 :undoc-members:
56 :show-inheritance:
57
58Introspection Package
59+++++++++++++++++++++
60
61:mod:`introspection` Package
62----------------------------
63
64.. automodule:: autopilot.introspection
65 :members:
66 :undoc-members:
67 :show-inheritance:
68
69:mod:`dbus` Module
70-------------------
71
72.. automodule:: autopilot.introspection.dbus
73 :members:
74 :undoc-members:
75 :show-inheritance:
76
77:mod:`qt` Module
78-------------------
79
80.. automodule:: autopilot.introspection.qt
81 :members:
82 :undoc-members:
83 :show-inheritance:
84
85:mod:`gtk` Module
86-------------------
87
88.. automodule:: autopilot.introspection.gtk
89 :members:
90 :undoc-members:
91 :show-inheritance:
92
93Matchers Package
94++++++++++++++++
95
96:mod:`matchers` Package
97-----------------------
98
99.. automodule:: autopilot.matchers
100 :members:
101 :undoc-members:
102 :show-inheritance:
103
10411
=== added file 'docs/api/display.rst'
--- docs/api/display.rst 1970-01-01 00:00:00 +0000
+++ docs/api/display.rst 2013-04-15 21:43:32 +0000
@@ -0,0 +1,7 @@
1``display`` - Get information about the current display(s)
2++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3
4
5.. automodule:: autopilot.display
6 :members:
7 :undoc-members:
08
=== added file 'docs/api/emulators.rst'
--- docs/api/emulators.rst 1970-01-01 00:00:00 +0000
+++ docs/api/emulators.rst 2013-04-15 21:43:32 +0000
@@ -0,0 +1,20 @@
1``emulators`` - Backwards compatibility for autopilot v1.2
2++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3
4
5.. module autopilot.emulators
6 :synopsis: Backwards compatibility module to provide the 'emulators' namespace.
7
8
9The emulators module exists for backwards compatibility only.
10
11This module exists to make it easier to upgrade from autopilot v1.2 to v1.3 by
12providing the old 'emulators' namespace. However, it's a bad idea to rely on this
13module continuing to exist. It contains several sub-modules:
14
15 * :mod:`autopilot.display`
16 * autopilot.clipboard (deprecated)
17 * autopilot.dbus_handler (for internal use only)
18 * autopilot.ibus (deprecated)
19 * :mod:`autopilot.input`
20
021
=== added file 'docs/api/gestures.rst'
--- docs/api/gestures.rst 1970-01-01 00:00:00 +0000
+++ docs/api/gestures.rst 2013-04-15 21:43:32 +0000
@@ -0,0 +1,6 @@
1``gestures`` - Gestural and multi-touch support
2+++++++++++++++++++++++++++++++++++++++++++++++
3
4
5.. automodule:: autopilot.gestures
6 :members:
07
=== added file 'docs/api/input.rst'
--- docs/api/input.rst 1970-01-01 00:00:00 +0000
+++ docs/api/input.rst 2013-04-15 21:43:32 +0000
@@ -0,0 +1,7 @@
1``input`` - Generate keyboard, mouse, and touch input events
2++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3
4
5.. automodule:: autopilot.input
6 :members:
7 :undoc-members:
08
=== added file 'docs/api/introspection.rst'
--- docs/api/introspection.rst 1970-01-01 00:00:00 +0000
+++ docs/api/introspection.rst 2013-04-15 21:43:32 +0000
@@ -0,0 +1,6 @@
1``introspection`` - Autopilot introspection internals
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches