Merge lp:~thomir-deactivatedaccount/autopilot/trunk-add-new-formats into lp:autopilot
- trunk-add-new-formats
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Christopher Lee | ||||
Approved revision: | 383 | ||||
Merged at revision: | 378 | ||||
Proposed branch: | lp:~thomir-deactivatedaccount/autopilot/trunk-add-new-formats | ||||
Merge into: | lp:autopilot | ||||
Diff against target: |
406 lines (+173/-97) 5 files modified
autopilot/__init__.py (+6/-13) autopilot/run.py (+14/-52) autopilot/testresult.py (+57/-30) autopilot/tests/unit/test_testresults.py (+92/-0) debian/control (+4/-2) |
||||
To merge this branch: | bzr merge lp:~thomir-deactivatedaccount/autopilot/trunk-add-new-formats | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher Lee (community) | Approve | ||
PS Jenkins bot | continuous-integration | Approve | |
Review via email: mp+197306@code.launchpad.net |
Commit message
Make autopilot support subunit bytestream output.
Description of the change
Make autopilot support subunit bytestream output.
- 380. By Thomi Richards
-
Merged trunk.
- 381. By Thomi Richards
-
Fix commented out code and incorrect copyright header date.
PS Jenkins bot (ps-jenkins) wrote : | # |
- 382. By Thomi Richards
-
Add junitxml as a build-dep.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:381
http://
Executed test runs:
None: http://
None: http://
None: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:380
http://
Executed test runs:
None: http://
None: http://
None: http://
Click here to trigger a rebuild:
http://
- 383. By Thomi Richards
-
Fix PEP8.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:382
http://
Executed test runs:
FAILURE: http://
None: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:383
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'autopilot/__init__.py' |
2 | --- autopilot/__init__.py 2013-11-26 22:54:36 +0000 |
3 | +++ autopilot/__init__.py 2013-12-02 02:12:42 +0000 |
4 | @@ -20,20 +20,12 @@ |
5 | from argparse import ArgumentParser, REMAINDER, Action |
6 | import subprocess |
7 | |
8 | +from autopilot.testresult import get_output_formats, get_default_format |
9 | +from autopilot.exceptions import BackendException |
10 | + |
11 | version = '1.4.0' |
12 | |
13 | |
14 | -class BackendException(RuntimeError): |
15 | - |
16 | - """An error occured while trying to initialise an autopilot backend.""" |
17 | - |
18 | - def __init__(self, original_exception): |
19 | - super(BackendException, self).__init__( |
20 | - "Error while initialising backend. Original exception was: " + |
21 | - str(original_exception)) |
22 | - self.original_exception = original_exception |
23 | - |
24 | - |
25 | def parse_arguments(argv=None): |
26 | """Parse command-line arguments, and return an argparse arguments |
27 | object. |
28 | @@ -57,8 +49,9 @@ |
29 | If given a directory instead of a file will \ |
30 | write to a file in that directory named: \ |
31 | <hostname>_<dd.mm.yyy_HHMMSS>.log') |
32 | - parser_run.add_argument('-f', "--format", choices=['text', 'xml'], |
33 | - default='text', |
34 | + available_formats = get_output_formats().keys() |
35 | + parser_run.add_argument('-f', "--format", choices=available_formats, |
36 | + default=get_default_format(), |
37 | required=False, |
38 | help='Specify desired output format. \ |
39 | Default is "text".') |
40 | |
41 | === modified file 'autopilot/run.py' |
42 | --- autopilot/run.py 2013-11-28 04:03:49 +0000 |
43 | +++ autopilot/run.py 2013-12-02 02:12:42 +0000 |
44 | @@ -41,11 +41,10 @@ |
45 | from unittest import TestLoader, TestSuite |
46 | |
47 | from testtools import iterate_tests |
48 | -from testtools import TextTestResult |
49 | |
50 | from autopilot import get_version_string, parse_arguments |
51 | import autopilot.globals |
52 | -from autopilot.testresult import AutopilotVerboseResult |
53 | +from autopilot.testresult import get_output_formats |
54 | from autopilot.utilities import DebugLogFilter, LogFormatter |
55 | |
56 | |
57 | @@ -69,29 +68,13 @@ |
58 | root_logger.info(get_version_string()) |
59 | |
60 | |
61 | -def construct_test_runner(args): |
62 | - kwargs = dict( |
63 | - stdout=get_output_stream(args), |
64 | - output_format=get_output_format(args.format), |
65 | +def construct_test_result(args): |
66 | + formats = get_output_formats() |
67 | + return formats[args.format]( |
68 | + stream=get_output_stream(args), |
69 | + failfast=args.failfast, |
70 | ) |
71 | |
72 | - return ConfigurableTestRunner(**kwargs) |
73 | - |
74 | - |
75 | -def get_output_format(format): |
76 | - """Return a Result object for each format we support.""" |
77 | - |
78 | - if format == "text": |
79 | - return type('VerboseTextTestResult', (TextTestResult,), |
80 | - dict(AutopilotVerboseResult.__dict__)) |
81 | - |
82 | - elif format == "xml": |
83 | - from junitxml import JUnitXmlResult |
84 | - return type('VerboseXmlResult', (JUnitXmlResult,), |
85 | - dict(AutopilotVerboseResult.__dict__)) |
86 | - |
87 | - raise KeyError("Unknown format name '%s'" % format) |
88 | - |
89 | |
90 | def get_output_stream(args): |
91 | global _output_stream |
92 | @@ -118,30 +101,6 @@ |
93 | return _output_stream |
94 | |
95 | |
96 | -class ConfigurableTestRunner(object): |
97 | - """A configurable test runner class. |
98 | - |
99 | - This class alows us to configure the output format and whether of not we |
100 | - collect coverage information for the test run. |
101 | - |
102 | - """ |
103 | - |
104 | - def __init__(self, stdout, output_format): |
105 | - self.stdout = stdout |
106 | - self.result_class = output_format |
107 | - |
108 | - def run(self, test, failfast=False): |
109 | - "Run the given test case or test suite." |
110 | - result = self.result_class(self.stdout) |
111 | - result.startTestRun() |
112 | - result.failfast = failfast |
113 | - try: |
114 | - test_result = test.run(result) |
115 | - finally: |
116 | - result.stopTestRun() |
117 | - return test_result |
118 | - |
119 | - |
120 | def get_package_location(import_name): |
121 | """Get the on-disk location of a package from a test id name. |
122 | |
123 | @@ -218,6 +177,7 @@ |
124 | self.args = parse_arguments() |
125 | |
126 | def run(self): |
127 | + setup_logging(self.args.verbose) |
128 | if self.args.mode == 'list': |
129 | self.list_tests() |
130 | elif self.args.mode == 'run': |
131 | @@ -228,7 +188,6 @@ |
132 | self.launch_app() |
133 | |
134 | def run_vis(self): |
135 | - setup_logging(self.args.verbose) |
136 | # importing this requires that DISPLAY is set. Since we don't always |
137 | # want that requirement, do the import here: |
138 | from autopilot.vis import vis_main |
139 | @@ -251,7 +210,6 @@ |
140 | get_application_launcher_from_string_hint, |
141 | ) |
142 | |
143 | - setup_logging(self.args.verbose) |
144 | app_name = self.args.application[0] |
145 | if not os.path.isabs(app_name) or not os.path.exists(app_name): |
146 | try: |
147 | @@ -325,9 +283,13 @@ |
148 | if self.args.verbose: |
149 | autopilot.globals.set_log_verbose(True) |
150 | |
151 | - setup_logging(self.args.verbose) |
152 | - runner = construct_test_runner(self.args) |
153 | - test_result = runner.run(test_suite, self.args.failfast) |
154 | + result = construct_test_result(self.args) |
155 | + result.startTestRun() |
156 | + try: |
157 | + test_result = test_suite.run(result) |
158 | + finally: |
159 | + result.stopTestRun() |
160 | + |
161 | if not test_result.wasSuccessful(): |
162 | exit(1) |
163 | |
164 | |
165 | === modified file 'autopilot/testresult.py' |
166 | --- autopilot/testresult.py 2013-09-16 17:26:27 +0000 |
167 | +++ autopilot/testresult.py 2013-12-02 02:12:42 +0000 |
168 | @@ -25,11 +25,17 @@ |
169 | import logging |
170 | |
171 | from autopilot.globals import get_log_verbose |
172 | - |
173 | - |
174 | -class AutopilotVerboseResult(object): |
175 | - """A result class that logs failures, errors and success via the python |
176 | - logging framework.""" |
177 | +from testtools import ( |
178 | + ExtendedToOriginalDecorator, |
179 | + ExtendedToStreamDecorator, |
180 | + TestResultDecorator, |
181 | + TextTestResult, |
182 | + try_import, |
183 | +) |
184 | + |
185 | + |
186 | +class LoggedTestResultDecorator(TestResultDecorator): |
187 | + """A decorator that logs messages to python's logging system.""" |
188 | |
189 | def _log(self, level, message): |
190 | """Performs the actual message logging""" |
191 | @@ -46,40 +52,61 @@ |
192 | self._log(level, text) |
193 | |
194 | def addSuccess(self, test, details=None): |
195 | - """Called for a successful test""" |
196 | - # Allow for different calling syntax used by the base class. |
197 | - if details is None: |
198 | - super(type(self), self).addSuccess(test) |
199 | - else: |
200 | - super(type(self), self).addSuccess(test, details) |
201 | self._log(logging.INFO, "OK: %s" % (test.id())) |
202 | + return super(LoggedTestResultDecorator, self).addSuccess(test, details) |
203 | |
204 | def addError(self, test, err=None, details=None): |
205 | - """Called for a test which failed with an error""" |
206 | - # Allow for different calling syntax used by the base class. |
207 | - # The xml path only uses 'err'. Use of 'err' can be |
208 | - # forced by raising TypeError when it is not specified. |
209 | - if err is None: |
210 | - raise TypeError |
211 | - if details is None: |
212 | - super(type(self), self).addError(test, err) |
213 | - else: |
214 | - super(type(self), self).addError(test, err, details) |
215 | self._log(logging.ERROR, "ERROR: %s" % (test.id())) |
216 | if hasattr(test, "getDetails"): |
217 | self._log_details(logging.ERROR, test.getDetails()) |
218 | + return super(type(self), self).addError(test, err, details) |
219 | |
220 | def addFailure(self, test, err=None, details=None): |
221 | """Called for a test which failed an assert""" |
222 | - # Allow for different calling syntax used by the base class. |
223 | - # The xml path only uses 'err' or 'details'. Use of 'err' can be |
224 | - # forced by raising TypeError when it is not specified. |
225 | - if err is None: |
226 | - raise TypeError |
227 | - if details is None: |
228 | - super(type(self), self).addFailure(test, err) |
229 | - else: |
230 | - super(type(self), self).addFailure(test, err, details) |
231 | self._log(logging.ERROR, "FAIL: %s" % (test.id())) |
232 | if hasattr(test, "getDetails"): |
233 | self._log_details(logging.ERROR, test.getDetails()) |
234 | + return super(type(self), self).addFailure(test, err, details) |
235 | + |
236 | + |
237 | +def get_output_formats(): |
238 | + """Get information regarding the different output formats supported.""" |
239 | + supported_formats = {} |
240 | + |
241 | + supported_formats['text'] = _construct_text |
242 | + |
243 | + if try_import('junitxml'): |
244 | + supported_formats['xml'] = _construct_xml |
245 | + if try_import('subunit'): |
246 | + supported_formats['subunit'] = _construct_subunit |
247 | + return supported_formats |
248 | + |
249 | + |
250 | +def get_default_format(): |
251 | + return 'text' |
252 | + |
253 | + |
254 | +def _construct_xml(**kwargs): |
255 | + from junitxml import JUnitXmlResult |
256 | + stream = kwargs.pop('stream') |
257 | + return LoggedTestResultDecorator( |
258 | + ExtendedToOriginalDecorator( |
259 | + JUnitXmlResult(stream) |
260 | + ) |
261 | + ) |
262 | + |
263 | + |
264 | +def _construct_text(**kwargs): |
265 | + stream = kwargs.pop('stream') |
266 | + failfast = kwargs.pop('failfast') |
267 | + return LoggedTestResultDecorator(TextTestResult(stream, failfast)) |
268 | + |
269 | + |
270 | +def _construct_subunit(**kwargs): |
271 | + from subunit import StreamResultToBytes |
272 | + stream = kwargs.pop('stream') |
273 | + return LoggedTestResultDecorator( |
274 | + ExtendedToStreamDecorator( |
275 | + StreamResultToBytes(stream) |
276 | + ) |
277 | + ) |
278 | |
279 | === added file 'autopilot/tests/unit/test_testresults.py' |
280 | --- autopilot/tests/unit/test_testresults.py 1970-01-01 00:00:00 +0000 |
281 | +++ autopilot/tests/unit/test_testresults.py 2013-12-02 02:12:42 +0000 |
282 | @@ -0,0 +1,92 @@ |
283 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
284 | +# |
285 | +# Autopilot Functional Test Tool |
286 | +# Copyright (C) 2013 Canonical |
287 | +# |
288 | +# This program is free software: you can redistribute it and/or modify |
289 | +# it under the terms of the GNU General Public License as published by |
290 | +# the Free Software Foundation, either version 3 of the License, or |
291 | +# (at your option) any later version. |
292 | +# |
293 | +# This program is distributed in the hope that it will be useful, |
294 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
295 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
296 | +# GNU General Public License for more details. |
297 | +# |
298 | +# You should have received a copy of the GNU General Public License |
299 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
300 | +# |
301 | + |
302 | +from mock import Mock, patch |
303 | +from testtools import TestCase, PlaceHolder |
304 | +from testtools.content import text_content |
305 | + |
306 | +from autopilot.testresult import ( |
307 | + get_default_format, |
308 | + get_output_formats, |
309 | + LoggedTestResultDecorator, |
310 | +) |
311 | + |
312 | + |
313 | +class LoggedTestResultDecoratorTests(TestCase): |
314 | + |
315 | + def construct_simple_content_object(self): |
316 | + return text_content(self.getUniqueString) |
317 | + |
318 | + def test_can_construct(self): |
319 | + LoggedTestResultDecorator(Mock()) |
320 | + |
321 | + def test_addSuccess_calls_decorated_test(self): |
322 | + wrapped = Mock() |
323 | + result = LoggedTestResultDecorator(wrapped) |
324 | + fake_test = PlaceHolder('fake_test') |
325 | + fake_details = self.construct_simple_content_object() |
326 | + |
327 | + result.addSuccess(fake_test, fake_details) |
328 | + |
329 | + wrapped.addSuccess.assert_called_once_with( |
330 | + fake_test, |
331 | + details=fake_details |
332 | + ) |
333 | + |
334 | + def test_addError_calls_decorated_test(self): |
335 | + wrapped = Mock() |
336 | + result = LoggedTestResultDecorator(wrapped) |
337 | + fake_test = PlaceHolder('fake_test') |
338 | + fake_error = object() |
339 | + fake_details = self.construct_simple_content_object() |
340 | + |
341 | + result.addError(fake_test, fake_error, fake_details) |
342 | + |
343 | + wrapped.addError.assert_called_once_with( |
344 | + fake_test, |
345 | + fake_error, |
346 | + details=fake_details |
347 | + ) |
348 | + |
349 | + def test_addFailure_calls_decorated_test(self): |
350 | + wrapped = Mock() |
351 | + result = LoggedTestResultDecorator(wrapped) |
352 | + fake_test = PlaceHolder('fake_test') |
353 | + fake_error = object() |
354 | + fake_details = self.construct_simple_content_object() |
355 | + |
356 | + result.addFailure(fake_test, fake_error, fake_details) |
357 | + |
358 | + wrapped.addFailure.assert_called_once_with( |
359 | + fake_test, |
360 | + fake_error, |
361 | + details=fake_details |
362 | + ) |
363 | + |
364 | + |
365 | +class OutputFormatFactoryTests(TestCase): |
366 | + |
367 | + def test_has_text_format(self): |
368 | + self.assertTrue('text' in get_output_formats()) |
369 | + |
370 | + def test_has_xml_format(self): |
371 | + self.assertTrue('xml' in get_output_formats()) |
372 | + |
373 | + def test_default_format_is_available(self): |
374 | + self.assertTrue(get_default_format() in get_output_formats()) |
375 | |
376 | === modified file 'debian/control' |
377 | --- debian/control 2013-11-27 20:15:34 +0000 |
378 | +++ debian/control 2013-12-02 02:12:42 +0000 |
379 | @@ -16,23 +16,25 @@ |
380 | python-debian, |
381 | python-dev, |
382 | python-gi, |
383 | + python-junitxml, |
384 | python-mock, |
385 | python-psutil, |
386 | python-setuptools, |
387 | - python-subunit, |
388 | python-six, |
389 | python-sphinx, |
390 | + python-subunit, |
391 | python-testscenarios, |
392 | python-testtools, |
393 | python-xlib, |
394 | python3-all-dev (>= 3.3), |
395 | python3-dbus, |
396 | python3-gi, |
397 | + python3-junitxml, |
398 | python3-mock, |
399 | python3-psutil, |
400 | python3-setuptools, |
401 | + python3-six, |
402 | python3-subunit, |
403 | - python3-six, |
404 | python3-testscenarios, |
405 | python3-testtools, |
406 | python3-xlib, |
FAILED: Continuous integration, rev:380 jenkins. qa.ubuntu. com/job/ autopilot- ci/320/ jenkins. qa.ubuntu. com/job/ autopilot- trusty- amd64-ci/ 46/console jenkins. qa.ubuntu. com/job/ autopilot- trusty- armhf-ci/ 46/console jenkins. qa.ubuntu. com/job/ autopilot- trusty- i386-ci/ 46/console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/autopilot- ci/320/ rebuild
http://