Merge lp:~thomir-deactivatedaccount/autopilot/1.3-fix-scan-speed into lp:autopilot/1.3
- 1.3-fix-scan-speed
- Merge into 1.3
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Thomi Richards | ||||
Approved revision: | 336 | ||||
Merged at revision: | 333 | ||||
Proposed branch: | lp:~thomir-deactivatedaccount/autopilot/1.3-fix-scan-speed | ||||
Merge into: | lp:autopilot/1.3 | ||||
Diff against target: |
540 lines (+287/-161) 5 files modified
autopilot/introspection/__init__.py (+29/-19) autopilot/testcase.py (+35/-11) autopilot/tests/functional/__init__.py (+137/-0) autopilot/tests/functional/test_autopilot_functional.py (+5/-131) autopilot/tests/functional/test_autopilot_performance.py (+81/-0) |
||||
To merge this branch: | bzr merge lp:~thomir-deactivatedaccount/autopilot/1.3-fix-scan-speed | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Martin Pitt (community) | Approve | ||
Review via email:
|
Commit message
Performance improvments for launching and closing applications under test with autopilot.
Description of the change
Merge performance improvments for autopilot.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
- 336. By Thomi Richards
-
Fix PEP8 issues.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Martin Pitt (pitti) wrote : | # |
I'm a bit sceptical that this is going to be long enough for ARM:
+ with maximum_
Also, I don't believe the rather complicated retry loop in _kill_process() isn't necessary, as in both py2 and py3 communicate() is documented to actually wait for the process to terminate.
Otherwise, LGTM. Still waiting for CI tests.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:336
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'autopilot/introspection/__init__.py' |
2 | --- autopilot/introspection/__init__.py 2013-09-11 16:16:00 +0000 |
3 | +++ autopilot/introspection/__init__.py 2013-09-17 18:50:22 +0000 |
4 | @@ -33,6 +33,7 @@ |
5 | from time import sleep |
6 | from functools import partial |
7 | import os |
8 | +import psutil |
9 | |
10 | from autopilot.introspection.backends import DBusAddress |
11 | from autopilot.introspection.constants import ( |
12 | @@ -239,8 +240,10 @@ |
13 | :param emulator_base: The custom emulator to create the resulting proxy |
14 | object with. |
15 | |
16 | - :raises: RuntimeError if no search criteria match. |
17 | - :raises: RuntimeError if the search criteria results in many matches. |
18 | + :raises ProcessSearchError: if no search criteria match. |
19 | + :raises RuntimeError: if the search criteria results in many matches. |
20 | + :raises RuntimeError: if both ``process`` and ``pid`` are supplied, but |
21 | + ``process.pid != pid``. |
22 | |
23 | """ |
24 | if process is not None: |
25 | @@ -282,6 +285,7 @@ |
26 | _reset_known_connection_list() |
27 | |
28 | for i in range(10): |
29 | + _get_child_pids.reset_cache() |
30 | if process is not None and not _process_is_running(process): |
31 | return_code = process.poll() |
32 | raise ProcessSearchError( |
33 | @@ -430,26 +434,32 @@ |
34 | obj_iface.GetVersion() |
35 | |
36 | |
37 | -def _get_child_pids(pid): |
38 | +class _cached_get_child_pids(object): |
39 | """Get a list of all child process Ids, for the given parent. |
40 | |
41 | + Since we call this often, and it's a very expensive call, we optimise this |
42 | + such that the return value will be cached for each scan through the dbus |
43 | + bus. |
44 | + |
45 | + Calling reset_cache() at the end of each dbus scan will ensure that you get |
46 | + fresh values on the next call. |
47 | """ |
48 | - def get_children(pid): |
49 | - command = ['ps', '-o', 'pid', '--ppid', str(pid), '--noheaders'] |
50 | - try: |
51 | - raw_output = subprocess.check_output(command) |
52 | - except subprocess.CalledProcessError: |
53 | - return [] |
54 | - return [int(p) for p in raw_output.split()] |
55 | - |
56 | - result = [] |
57 | - data = get_children(pid) |
58 | - while data: |
59 | - pid = data.pop(0) |
60 | - result.append(pid) |
61 | - data.extend(get_children(pid)) |
62 | - |
63 | - return result |
64 | + |
65 | + def __init__(self): |
66 | + self._cached_result = None |
67 | + |
68 | + def __call__(self, pid): |
69 | + if self._cached_result is None: |
70 | + self._cached_result = [ |
71 | + p.pid for p in psutil.Process(pid).get_children(recursive=True) |
72 | + ] |
73 | + return self._cached_result |
74 | + |
75 | + def reset_cache(self): |
76 | + self._cached_result = None |
77 | + |
78 | + |
79 | +_get_child_pids = _cached_get_child_pids() |
80 | |
81 | |
82 | def _make_proxy_object(data_source, emulator_base): |
83 | |
84 | === modified file 'autopilot/testcase.py' |
85 | --- autopilot/testcase.py 2013-09-13 18:52:37 +0000 |
86 | +++ autopilot/testcase.py 2013-09-17 18:50:22 +0000 |
87 | @@ -345,7 +345,7 @@ |
88 | if app_id in line and "start/running" in line: |
89 | target_pid = int(line.split()[-1]) |
90 | |
91 | - self.addCleanup(self._kill_process, target_pid) |
92 | + self.addCleanup(self._kill_pid, target_pid) |
93 | logger.info( |
94 | "Click package %s has been launched with PID %d", |
95 | app_id, |
96 | @@ -525,16 +525,11 @@ |
97 | # default implementation is in autopilot.introspection: |
98 | return get_application_launcher(app_path) |
99 | |
100 | - def _attach_process_logs(self, process): |
101 | - stdout, stderr = process.communicate() |
102 | - return_code = process.returncode |
103 | - self.addDetail('process-return-code', text_content(str(return_code))) |
104 | - self.addDetail('process-stdout', text_content(stdout)) |
105 | - self.addDetail('process-stderr', text_content(stderr)) |
106 | - |
107 | - def _kill_process(self, pid): |
108 | + def _kill_pid(self, pid): |
109 | + """Kill the process with the specified pid.""" |
110 | logger.info("waiting for process to exit.") |
111 | try: |
112 | + logger.info("Killing process %d", pid) |
113 | os.killpg(pid, signal.SIGTERM) |
114 | except OSError: |
115 | logger.info("Appears process has already exited.") |
116 | @@ -548,10 +543,39 @@ |
117 | ) |
118 | os.killpg(pid, signal.SIGKILL) |
119 | sleep(1) |
120 | + return stdout, stderr, process.returncode |
121 | + |
122 | + def _kill_process(self, process): |
123 | + """Kill the process, and return the stdout, stderr and return code.""" |
124 | + stdout = "" |
125 | + stderr = "" |
126 | + return_code = -1 |
127 | + logger.info("waiting for process to exit.") |
128 | + try: |
129 | + logger.info("Killing process %d", process.pid) |
130 | + os.killpg(process.pid, signal.SIGTERM) |
131 | + except OSError: |
132 | + logger.info("Appears process has already exited.") |
133 | + for i in range(10): |
134 | + tmp_out, tmp_err = process.communicate() |
135 | + stdout += tmp_out |
136 | + stderr += tmp_err |
137 | + if not _is_process_running(process.pid): |
138 | + break |
139 | + if i == 9: |
140 | + logger.info( |
141 | + "Killing process group, since it hasn't exited after " |
142 | + "10 seconds." |
143 | + ) |
144 | + os.killpg(process.pid, signal.SIGKILL) |
145 | + sleep(1) |
146 | + return stdout, stderr, process.returncode |
147 | |
148 | def _kill_process_and_attach_logs(self, process): |
149 | - self._kill_process(process.pid) |
150 | - self._attach_process_logs(process) |
151 | + stdout, stderr, return_code = self._kill_process(process) |
152 | + self.addDetail('process-return-code', text_content(str(return_code))) |
153 | + self.addDetail('process-stdout', text_content(stdout)) |
154 | + self.addDetail('process-stderr', text_content(stderr)) |
155 | |
156 | |
157 | def _is_process_running(pid): |
158 | |
159 | === modified file 'autopilot/tests/functional/__init__.py' |
160 | --- autopilot/tests/functional/__init__.py 2013-05-05 02:39:14 +0000 |
161 | +++ autopilot/tests/functional/__init__.py 2013-09-17 18:50:22 +0000 |
162 | @@ -16,3 +16,140 @@ |
163 | # You should have received a copy of the GNU General Public License |
164 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
165 | # |
166 | + |
167 | + |
168 | +from __future__ import absolute_import |
169 | + |
170 | +from codecs import open |
171 | +import os |
172 | +import os.path |
173 | +import logging |
174 | +from shutil import rmtree |
175 | +import subprocess |
176 | +from tempfile import mkdtemp |
177 | +from testtools.content import Content, text_content |
178 | +from testtools.content_type import ContentType |
179 | + |
180 | +from autopilot.testcase import AutopilotTestCase |
181 | + |
182 | + |
183 | +def remove_if_exists(path): |
184 | + if os.path.exists(path): |
185 | + if os.path.isdir(path): |
186 | + rmtree(path) |
187 | + else: |
188 | + os.remove(path) |
189 | + |
190 | + |
191 | +logger = logging.getLogger(__name__) |
192 | + |
193 | + |
194 | +class AutopilotRunTestBase(AutopilotTestCase): |
195 | + |
196 | + """The base class for the autopilot functional tests.""" |
197 | + |
198 | + def setUp(self): |
199 | + super(AutopilotRunTestBase, self).setUp() |
200 | + self.base_path = self.create_empty_test_module() |
201 | + |
202 | + def create_empty_test_module(self): |
203 | + """Create an empty temp directory, with an empty test directory inside |
204 | + it. |
205 | + |
206 | + This method handles cleaning up the directory once the test completes. |
207 | + |
208 | + Returns the full path to the temp directory. |
209 | + |
210 | + """ |
211 | + |
212 | + # create the base directory: |
213 | + base_path = mkdtemp() |
214 | + self.addDetail('base path', text_content(base_path)) |
215 | + self.addCleanup(rmtree, base_path) |
216 | + |
217 | + # create the tests directory: |
218 | + os.mkdir( |
219 | + os.path.join(base_path, 'tests') |
220 | + ) |
221 | + |
222 | + # make tests importable: |
223 | + open( |
224 | + os.path.join( |
225 | + base_path, |
226 | + 'tests', |
227 | + '__init__.py'), |
228 | + 'w').write('# Auto-generated file.') |
229 | + return base_path |
230 | + |
231 | + def run_autopilot(self, arguments): |
232 | + ap_base_path = os.path.abspath( |
233 | + os.path.join( |
234 | + os.path.dirname(__file__), |
235 | + '..', |
236 | + '..', |
237 | + '..' |
238 | + ) |
239 | + ) |
240 | + |
241 | + environment_patch = dict(DISPLAY=':0') |
242 | + if not os.getcwd().startswith('/usr/'): |
243 | + environment_patch['PYTHONPATH'] = ap_base_path |
244 | + bin_path = os.path.join(ap_base_path, 'bin', 'autopilot') |
245 | + if not os.path.exists(bin_path): |
246 | + bin_path = subprocess.check_output(['which', 'autopilot']).strip() |
247 | + logger.info( |
248 | + "Not running from source, setting bin_path to %s", bin_path) |
249 | + |
250 | + environ = os.environ |
251 | + environ.update(environment_patch) |
252 | + |
253 | + logger.info("Starting autopilot command with:") |
254 | + logger.info("Autopilot command = %s", bin_path) |
255 | + logger.info("Arguments = %s", arguments) |
256 | + logger.info("CWD = %r", self.base_path) |
257 | + |
258 | + arg = [bin_path] |
259 | + arg.extend(arguments) |
260 | + process = subprocess.Popen( |
261 | + arg, |
262 | + cwd=self.base_path, |
263 | + env=environ, |
264 | + stdout=subprocess.PIPE, |
265 | + stderr=subprocess.PIPE, |
266 | + ) |
267 | + |
268 | + stdout, stderr = process.communicate() |
269 | + retcode = process.poll() |
270 | + |
271 | + self.addDetail('retcode', text_content(str(retcode))) |
272 | + self.addDetail( |
273 | + 'stdout', |
274 | + Content( |
275 | + ContentType('text', 'plain', {'charset': 'iso-8859-1'}), |
276 | + lambda: [stdout] |
277 | + ) |
278 | + ) |
279 | + self.addDetail( |
280 | + 'stderr', |
281 | + Content( |
282 | + ContentType('text', 'plain', {'charset': 'iso-8859-1'}), |
283 | + lambda: [stderr] |
284 | + ) |
285 | + ) |
286 | + |
287 | + return (retcode, stdout, stderr) |
288 | + |
289 | + def create_test_file(self, name, contents): |
290 | + """Create a test file with the given name and contents. |
291 | + |
292 | + 'name' must end in '.py' if it is to be importable. |
293 | + 'contents' must be valid python code. |
294 | + |
295 | + """ |
296 | + open( |
297 | + os.path.join( |
298 | + self.base_path, |
299 | + 'tests', |
300 | + name), |
301 | + 'w', |
302 | + encoding='utf8').write(contents) |
303 | |
304 | === modified file 'autopilot/tests/functional/test_autopilot_functional.py' |
305 | --- autopilot/tests/functional/test_autopilot_functional.py 2013-09-13 15:00:35 +0000 |
306 | +++ autopilot/tests/functional/test_autopilot_functional.py 2013-09-17 18:50:22 +0000 |
307 | @@ -23,142 +23,16 @@ |
308 | from codecs import open |
309 | import os |
310 | import os.path |
311 | -import logging |
312 | -from shutil import rmtree |
313 | -import subprocess |
314 | -from tempfile import mktemp, mkdtemp |
315 | -from testtools.content import Content, text_content |
316 | -from testtools.content_type import ContentType |
317 | +from tempfile import mktemp |
318 | from testtools.matchers import Contains, Equals, MatchesRegex, Not |
319 | from textwrap import dedent |
320 | import re |
321 | |
322 | from autopilot.testcase import AutopilotTestCase |
323 | - |
324 | - |
325 | -def remove_if_exists(path): |
326 | - if os.path.exists(path): |
327 | - if os.path.isdir(path): |
328 | - rmtree(path) |
329 | - else: |
330 | - os.remove(path) |
331 | - |
332 | - |
333 | -logger = logging.getLogger(__name__) |
334 | - |
335 | - |
336 | -class AutopilotFunctionalTestsBase(AutopilotTestCase): |
337 | - |
338 | - """The base class for the autopilot functional tests.""" |
339 | - |
340 | - def setUp(self): |
341 | - super(AutopilotFunctionalTestsBase, self).setUp() |
342 | - self.base_path = self.create_empty_test_module() |
343 | - |
344 | - def create_empty_test_module(self): |
345 | - """Create an empty temp directory, with an empty test directory inside |
346 | - it. |
347 | - |
348 | - This method handles cleaning up the directory once the test completes. |
349 | - |
350 | - Returns the full path to the temp directory. |
351 | - |
352 | - """ |
353 | - |
354 | - # create the base directory: |
355 | - base_path = mkdtemp() |
356 | - self.addDetail('base path', text_content(base_path)) |
357 | - self.addCleanup(rmtree, base_path) |
358 | - |
359 | - # create the tests directory: |
360 | - os.mkdir( |
361 | - os.path.join(base_path, 'tests') |
362 | - ) |
363 | - |
364 | - # make tests importable: |
365 | - open( |
366 | - os.path.join( |
367 | - base_path, |
368 | - 'tests', |
369 | - '__init__.py'), |
370 | - 'w').write('# Auto-generated file.') |
371 | - return base_path |
372 | - |
373 | - def run_autopilot(self, arguments): |
374 | - ap_base_path = os.path.abspath( |
375 | - os.path.join( |
376 | - os.path.dirname(__file__), |
377 | - '..', |
378 | - '..', |
379 | - '..' |
380 | - ) |
381 | - ) |
382 | - |
383 | - environment_patch = dict(DISPLAY=':0') |
384 | - if not os.getcwd().startswith('/usr/'): |
385 | - environment_patch['PYTHONPATH'] = ap_base_path |
386 | - bin_path = os.path.join(ap_base_path, 'bin', 'autopilot') |
387 | - if not os.path.exists(bin_path): |
388 | - bin_path = subprocess.check_output(['which', 'autopilot']).strip() |
389 | - logger.info( |
390 | - "Not running from source, setting bin_path to %s", bin_path) |
391 | - |
392 | - environ = os.environ |
393 | - environ.update(environment_patch) |
394 | - |
395 | - logger.info("Starting autopilot command with:") |
396 | - logger.info("Autopilot command = %s", bin_path) |
397 | - logger.info("Arguments = %s", arguments) |
398 | - logger.info("CWD = %r", self.base_path) |
399 | - |
400 | - arg = [bin_path] |
401 | - arg.extend(arguments) |
402 | - process = subprocess.Popen( |
403 | - arg, |
404 | - cwd=self.base_path, |
405 | - env=environ, |
406 | - stdout=subprocess.PIPE, |
407 | - stderr=subprocess.PIPE, |
408 | - ) |
409 | - |
410 | - stdout, stderr = process.communicate() |
411 | - retcode = process.poll() |
412 | - |
413 | - self.addDetail('retcode', text_content(str(retcode))) |
414 | - self.addDetail( |
415 | - 'stdout', |
416 | - Content( |
417 | - ContentType('text', 'plain', {'charset': 'iso-8859-1'}), |
418 | - lambda: [stdout] |
419 | - ) |
420 | - ) |
421 | - self.addDetail( |
422 | - 'stderr', |
423 | - Content( |
424 | - ContentType('text', 'plain', {'charset': 'iso-8859-1'}), |
425 | - lambda: [stderr] |
426 | - ) |
427 | - ) |
428 | - |
429 | - return (retcode, stdout, stderr) |
430 | - |
431 | - def create_test_file(self, name, contents): |
432 | - """Create a test file with the given name and contents. |
433 | - |
434 | - 'name' must end in '.py' if it is to be importable. |
435 | - 'contents' must be valid python code. |
436 | - |
437 | - """ |
438 | - open( |
439 | - os.path.join( |
440 | - self.base_path, |
441 | - 'tests', |
442 | - name), |
443 | - 'w', |
444 | - encoding='utf8').write(contents) |
445 | - |
446 | - |
447 | -class AutopilotFunctionalTests(AutopilotFunctionalTestsBase): |
448 | +from autopilot.tests.functional import AutopilotRunTestBase, remove_if_exists |
449 | + |
450 | + |
451 | +class AutopilotFunctionalTestsBase(AutopilotRunTestBase): |
452 | |
453 | """A collection of functional tests for autopilot.""" |
454 | |
455 | |
456 | === added file 'autopilot/tests/functional/test_autopilot_performance.py' |
457 | --- autopilot/tests/functional/test_autopilot_performance.py 1970-01-01 00:00:00 +0000 |
458 | +++ autopilot/tests/functional/test_autopilot_performance.py 2013-09-17 18:50:22 +0000 |
459 | @@ -0,0 +1,81 @@ |
460 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
461 | +# |
462 | +# Autopilot Functional Test Tool |
463 | +# Copyright (C) 2012-2013 Canonical |
464 | +# |
465 | +# This program is free software: you can redistribute it and/or modify |
466 | +# it under the terms of the GNU General Public License as published by |
467 | +# the Free Software Foundation, either version 3 of the License, or |
468 | +# (at your option) any later version. |
469 | +# |
470 | +# This program is distributed in the hope that it will be useful, |
471 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
472 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
473 | +# GNU General Public License for more details. |
474 | +# |
475 | +# You should have received a copy of the GNU General Public License |
476 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
477 | +# |
478 | + |
479 | +from __future__ import absolute_import |
480 | + |
481 | + |
482 | +from contextlib import contextmanager |
483 | +from time import time |
484 | +from textwrap import dedent |
485 | +from testtools.matchers import Equals |
486 | + |
487 | +import logging |
488 | +from autopilot.tests.functional import AutopilotRunTestBase |
489 | + |
490 | + |
491 | +logger = logging.getLogger(__name__) |
492 | + |
493 | + |
494 | +@contextmanager |
495 | +def maximum_runtime(max_time): |
496 | + start_time = time() |
497 | + yield |
498 | + total_time = abs(time() - start_time) |
499 | + if total_time >= max_time: |
500 | + raise AssertionError( |
501 | + "Runtime of %f was not within defined " |
502 | + "limit of %f" % (total_time, max_time) |
503 | + ) |
504 | + else: |
505 | + logger.info( |
506 | + "Test completed in %f seconds, which is below the " |
507 | + " threshold of %f.", |
508 | + total_time, |
509 | + max_time |
510 | + ) |
511 | + |
512 | + |
513 | +class AutopilotPerformanceTests(AutopilotRunTestBase): |
514 | + |
515 | + """A suite of functional tests that will fail if autopilot performance |
516 | + regresses below certain strictly defined limits. |
517 | + |
518 | + Each test must be named after the feature we are benchmarking, and should |
519 | + use the maximum_runtime contextmanager defined above. |
520 | + |
521 | + """ |
522 | + |
523 | + def test_autopilot_launch_test_app(self): |
524 | + self.create_test_file( |
525 | + 'test_something.py', |
526 | + dedent(""" |
527 | + from autopilot.testcase import AutopilotTestCase |
528 | + |
529 | + class LaunchTestAppTests(AutopilotTestCase): |
530 | + |
531 | + def test_launch_test_app(self): |
532 | + app_proxy = self.launch_test_application( |
533 | + 'window-mocker', |
534 | + app_type='qt' |
535 | + ) |
536 | + """) |
537 | + ) |
538 | + with maximum_runtime(5.0): |
539 | + rc, out, err = self.run_autopilot(['run', 'tests']) |
540 | + self.assertThat(rc, Equals(0)) |
FAILED: Continuous integration, rev:335 jenkins. qa.ubuntu. com/job/ autopilot- 1.3-ci/ 18/ jenkins. qa.ubuntu. com/job/ autopilot- 1.3-saucy- amd64-ci/ 18/console jenkins. qa.ubuntu. com/job/ autopilot- 1.3-saucy- armhf-ci/ 18/console jenkins. qa.ubuntu. com/job/ autopilot- 1.3-saucy- i386-ci/ 2/console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins: 8080/job/ autopilot- 1.3-ci/ 18/rebuild
http://