Merge lp:~thomir-deactivatedaccount/autopilot/add-click-package-support into lp:autopilot/1.3

Proposed by Thomi Richards
Status: Merged
Approved by: Thomi Richards
Approved revision: 333
Merged at revision: 325
Proposed branch: lp:~thomir-deactivatedaccount/autopilot/add-click-package-support
Merge into: lp:autopilot/1.3
Diff against target: 340 lines (+189/-16)
6 files modified
autopilot/introspection/qt.py (+1/-0)
autopilot/introspection/utilities.py (+29/-0)
autopilot/testcase.py (+102/-6)
autopilot/tests/functional/test_application_mixin.py (+39/-0)
autopilot/tests/functional/test_autopilot_functional.py (+10/-4)
debian/control (+8/-6)
To merge this branch: bzr merge lp:~thomir-deactivatedaccount/autopilot/add-click-package-support
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Thomi Richards (community) Approve
Leo Arias (community) code review Approve
Review via email: mp+185150@code.launchpad.net

Commit message

Add support for click packages.

Description of the change

This branch adds support for launching applications contained within click packages. It is required in order to test apps packaged inside click packages.

There are new unit and integration tests for the new functionality. There are no functional tests, since our functional test suite only runs on the desktop, and click package support has not landed there yet.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:320
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/add-click-package-support/+merge/185150/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/autopilot-1.3-ci/1/
Executed test runs:
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-1.3-saucy-amd64-ci/1/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/autopilot-1.3-saucy-armhf-ci/1/console

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

review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Leo Arias (elopio) wrote :

93 + app_proxy = self.launch_click_application(

It should be launch_click_package.

115 + # set the qt testability env:
121 + # launch the application:

Instead of those comments, I would extract the statements to a method with a nice name, like set_qt_testability_environment and start_click_application

129 + for i in range(10):

Why is ten a good number? I would move it to a constant with a docstring saying why you chose it.

137 + except subprocess.CalledProcessError:
138 + pass
It would be nice to document here what can cause a called process error and why we are ignoring it.

140 + for line in list_output.split('\n'):
...
151 + break

Is it possible for this target_pid = int(line.split()[-1]) to be -1?
I think not, but as there are no tests I can't be sure. If it can never be -1, I think it will make the code easier to read:

for line in list_output.split('\n'):
    if app_id in line and "start/running" in line:
        target_pid = int(line.split()[-1])
 self.addCleanup(self._kill_process, target_pid)
        logger.info(
     "Click package %s has been launched with PID %d", app_id,
            target_pid
        )
        break

154 + if target_pid == -1:

Instead of that, you can add an else to the for. And then maybe you will not need to initialize target_pid to -1 at all.

165 + # reset the upstart env, and hope no one else launched...

What would happen if another app launched?

Only the first comment needs to be fixed. The rest are suggestions. It would be nice if you file a bug per every missing test on this branch, so you won't forget about adding them.

review: Approve (code review)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) :
review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/introspection/qt.py'
2--- autopilot/introspection/qt.py 2013-07-14 08:38:36 +0000
3+++ autopilot/introspection/qt.py 2013-09-13 18:52:52 +0000
4@@ -26,6 +26,7 @@
5 import functools
6
7 import logging
8+import subprocess
9
10 from autopilot.introspection import ApplicationLauncher
11
12
13=== modified file 'autopilot/introspection/utilities.py'
14--- autopilot/introspection/utilities.py 2013-07-30 15:08:30 +0000
15+++ autopilot/introspection/utilities.py 2013-09-13 18:52:52 +0000
16@@ -20,6 +20,8 @@
17 from __future__ import absolute_import
18 from dbus import Interface
19 import os.path
20+import subprocess
21+import json
22
23
24 def _pid_is_running(pid):
25@@ -39,3 +41,30 @@
26 bus_obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
27 bus_iface = Interface(bus_obj, 'org.freedesktop.DBus')
28 return bus_iface.GetConnectionUnixProcessID(connection_name)
29+
30+
31+def _get_click_manifest():
32+ """Return the click package manifest as a python list."""
33+ # get the whole click package manifest every time - it seems fast enough
34+ # but this is a potential optimisation point for the future:
35+ click_manifest_str = subprocess.check_output(
36+ ["click", "list", "--manifest"]
37+ )
38+ return json.loads(click_manifest_str)
39+
40+
41+def _get_click_app_id(package_id, app_name=None):
42+ for pkg in _get_click_manifest():
43+ if pkg['name'] == package_id:
44+ if app_name is None:
45+ app_name = pkg['hooks'].keys()[0]
46+ elif app_name not in pkg['hooks']:
47+ raise RuntimeError(
48+ "Application '{}' is not present within the click "
49+ "package '{}'.".format(app_name, package_id))
50+
51+ return "{0}_{1}_{2}".format(package_id, app_name, pkg['version'])
52+ raise RuntimeError(
53+ "Unable to find package '{}' in the click manifest."
54+ .format(package_id)
55+ )
56
57=== modified file 'autopilot/testcase.py'
58--- autopilot/testcase.py 2013-07-29 10:29:37 +0000
59+++ autopilot/testcase.py 2013-09-13 18:52:52 +0000
60@@ -50,6 +50,7 @@
61
62 import logging
63 import os
64+import psutil
65 import signal
66 import subprocess
67
68@@ -65,8 +66,10 @@
69 get_application_launcher,
70 get_application_launcher_from_string_hint,
71 get_autopilot_proxy_object_for_process,
72+ get_proxy_object_for_existing_process,
73 launch_application,
74 )
75+from autopilot.introspection.utilities import _get_click_app_id
76 from autopilot.display import Display
77 from autopilot.utilities import on_test_started
78 from autopilot.keybindings import KeybindingsHelper
79@@ -281,6 +284,96 @@
80 dbus_bus
81 )
82
83+ def launch_click_package(self, package_id, app_name=None, **kwargs):
84+ """Launch a click package application with introspection enabled.
85+
86+ This method takes care of launching a click package with introspection
87+ exabled. You probably want to use this method if your application is
88+ packaged in a click application, or is started via upstart.
89+
90+ Usage is similar to the
91+ :py:meth:`AutopilotTestCase.launch_test_application`::
92+
93+ app_proxy = self.launch_click_package(
94+ "com.ubuntu.dropping-letters"
95+ )
96+
97+ :param package_id: The Click package name you want to launch. For
98+ example: ``com.ubuntu.dropping-letters``
99+ :param app_name: Currently, only one application can be packaged in a
100+ click package, and this parameter can be left at None. If
101+ specified, it should be the application name you wish to launch.
102+
103+ :keyword emulator_base: If set, specifies the base class to be used for
104+ all emulators for this loaded application.
105+
106+ :raises RuntimeError: If the specified package_id cannot be found in
107+ the click package manifest.
108+ :raises RuntimeError: If the specified app_name cannot be found within
109+ the specified click package.
110+
111+ """
112+ app_id = _get_click_app_id(package_id, app_name)
113+ # sadly, we cannot re-use the existing launch_test_application
114+ # since upstart is a little odd.
115+ # set the qt testability env:
116+ subprocess.call([
117+ "/sbin/initctl",
118+ "set-env",
119+ "QT_LOAD_TESTABILITY=1",
120+ ])
121+ # launch the application:
122+ subprocess.check_output([
123+ "/sbin/start",
124+ "application",
125+ "APP_ID={}".format(app_id),
126+ ])
127+ # perhaps we should do this with a regular expression instead?
128+ for i in range(10):
129+ try:
130+ list_output = subprocess.check_output([
131+ "/sbin/initctl",
132+ "status",
133+ "application-click",
134+ "APP_ID={}".format(app_id)
135+ ])
136+ except subprocess.CalledProcessError:
137+ # application not started yet.
138+ pass
139+ else:
140+ for line in list_output.split('\n'):
141+ if app_id in line and "start/running" in line:
142+ target_pid = int(line.split()[-1])
143+
144+ self.addCleanup(self._kill_process, target_pid)
145+ logger.info(
146+ "Click package %s has been launched with PID %d",
147+ app_id,
148+ target_pid
149+ )
150+
151+ emulator_base = kwargs.pop('emulator_base', None)
152+ proxy = get_proxy_object_for_existing_process(
153+ pid=target_pid,
154+ emulator_base=emulator_base
155+ )
156+ # reset the upstart env, and hope no one else launched,
157+ # or they'll have introspection enabled as well,
158+ # although this isn't the worth thing in the world.
159+ subprocess.call([
160+ "/sbin/initctl",
161+ "unset-env",
162+ "QT_LOAD_TESTABILITY",
163+ ])
164+ return proxy
165+ # give the app time to launch - maybe this is not needed?:
166+ sleep(1)
167+ else:
168+ raise RuntimeError(
169+ "Could not find autopilot interface for click package"
170+ " '{}' after 10 seconds.".format(app_id)
171+ )
172+
173 def _compare_system_with_app_snapshot(self):
174 """Compare the currently running application with the last snapshot.
175
176@@ -439,24 +532,27 @@
177 self.addDetail('process-stdout', text_content(stdout))
178 self.addDetail('process-stderr', text_content(stderr))
179
180- def _kill_process(self, process):
181+ def _kill_process(self, pid):
182 logger.info("waiting for process to exit.")
183 try:
184- os.killpg(process.pid, signal.SIGTERM)
185+ os.killpg(pid, signal.SIGTERM)
186 except OSError:
187 logger.info("Appears process has already exited.")
188 for i in range(10):
189- process.poll()
190- if process.returncode is not None:
191+ if not _is_process_running(pid):
192 break
193 if i == 9:
194 logger.info(
195 "Killing process group, since it hasn't exited after "
196 "10 seconds."
197 )
198- os.killpg(process.pid, signal.SIGKILL)
199+ os.killpg(pid, signal.SIGKILL)
200 sleep(1)
201
202 def _kill_process_and_attach_logs(self, process):
203- self._kill_process(process)
204+ self._kill_process(process.pid)
205 self._attach_process_logs(process)
206+
207+
208+def _is_process_running(pid):
209+ return pid in psutil.get_pid_list()
210
211=== modified file 'autopilot/tests/functional/test_application_mixin.py'
212--- autopilot/tests/functional/test_application_mixin.py 2013-07-14 10:03:10 +0000
213+++ autopilot/tests/functional/test_application_mixin.py 2013-09-13 18:52:52 +0000
214@@ -22,6 +22,7 @@
215
216 from autopilot.testcase import AutopilotTestCase
217 from testtools.matchers import raises
218+from mock import patch
219
220
221 class ApplicationSupportTests(AutopilotTestCase):
222@@ -66,3 +67,41 @@
223 self.assertThat(
224 fn,
225 raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'.")))
226+
227+ @patch(
228+ 'autopilot.introspection.utilities._get_click_manifest',
229+ new=lambda: []
230+ )
231+ def test_launch_click_package_raises_runtimeerror_on_missing_package(self):
232+ """launch_click_package must raise a RuntimeError if the requested
233+ package id is not found in the click manifest.
234+
235+ """
236+ fn = lambda: self.launch_click_package("com.ubuntu.something")
237+ self.assertThat(
238+ fn,
239+ raises(
240+ RuntimeError(
241+ "Unable to find package 'com.ubuntu.something' in the "
242+ "click manifest."
243+ )
244+ )
245+ )
246+
247+ @patch('autopilot.introspection.utilities._get_click_manifest')
248+ def test_launch_click_package_raises_runtimeerror_on_wrong_app(self, cm):
249+ """launch_click_package must raise a RuntimeError if the requested
250+ package id is not found in the click manifest.
251+
252+ """
253+ cm.return_value = [{'name': 'foo', 'hooks': {}}]
254+ fn = lambda: self.launch_click_package("foo", "bar")
255+ self.assertThat(
256+ fn,
257+ raises(
258+ RuntimeError(
259+ "Application 'bar' is not present within the click package"
260+ " 'foo'."
261+ )
262+ )
263+ )
264
265=== modified file 'autopilot/tests/functional/test_autopilot_functional.py'
266--- autopilot/tests/functional/test_autopilot_functional.py 2013-08-29 06:46:41 +0000
267+++ autopilot/tests/functional/test_autopilot_functional.py 2013-09-13 18:52:52 +0000
268@@ -126,13 +126,19 @@
269
270 self.addDetail('retcode', text_content(str(retcode)))
271 self.addDetail(
272- 'stdout', Content(
273+ 'stdout',
274+ Content(
275 ContentType('text', 'plain', {'charset': 'iso-8859-1'}),
276- lambda: [stdout]))
277+ lambda: [stdout]
278+ )
279+ )
280 self.addDetail(
281- 'stderr', Content(
282+ 'stderr',
283+ Content(
284 ContentType('text', 'plain', {'charset': 'iso-8859-1'}),
285- lambda: [stderr]))
286+ lambda: [stderr]
287+ )
288+ )
289
290 return (retcode, stdout, stderr)
291
292
293=== modified file 'debian/control'
294--- debian/control 2013-08-30 15:35:45 +0000
295+++ debian/control 2013-09-13 18:52:52 +0000
296@@ -4,21 +4,22 @@
297 Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
298 XSBC-Original-Maintainer: Thomi Richards <thomi.richards@canonical.com>
299 Build-Depends: debhelper (>= 9.0.0),
300- gir1.2-ibus-1.0,
301 gir1.2-gconf-2.0,
302 gir1.2-gtk-3.0,
303+ gir1.2-ibus-1.0,
304+ liblttng-ust-dev,
305 python (>= 2.6),
306 python-dbus,
307+ python-debian,
308 python-dev,
309- python-debian,
310+ python-gi,
311 python-mock,
312+ python-psutil,
313 python-setuptools,
314 python-sphinx,
315+ python-testscenarios,
316 python-testtools,
317- python-testscenarios,
318 python-xlib,
319- python-gi,
320- liblttng-ust-dev,
321 Standards-Version: 3.9.4
322 Homepage: https://launchpad.net/autopilot
323 Vcs-bzr: https://code.launchpad.net/+branch/ubuntu/autopilot
324@@ -29,6 +30,7 @@
325 ${python:Depends},
326 python-dbus,
327 python-junitxml,
328+ python-psutil,
329 python-testscenarios,
330 python-testtools,
331 udev,
332@@ -58,7 +60,7 @@
333 Architecture: any
334 Section: metapackages
335 Depends: ${misc:Depends},
336- libautopilot-qt (>= 1.3),
337+ libautopilot-qt (>= 1.3) [!powerpc],
338 python-autopilot,
339 python-evdev,
340 python-ubuntu-platform-api [armhf],

Subscribers

People subscribed via source and target branches

to all changes: