another xml test runner fix to make stdout/stderr output more accessible
[cython.git] / Cython / Tests / xmlrunner.py
1 # -*- coding: utf-8 -*-
2
3 """unittest-xml-reporting is a PyUnit-based TestRunner that can export test
4 results to XML files that can be consumed by a wide range of tools, such as
5 build systems, IDEs and Continuous Integration servers.
6
7 This module provides the XMLTestRunner class, which is heavily based on the
8 default TextTestRunner. This makes the XMLTestRunner very simple to use.
9
10 The script below, adapted from the unittest documentation, shows how to use
11 XMLTestRunner in a very simple way. In fact, the only difference between this
12 script and the original one is the last line:
13
14 import random
15 import unittest
16 import xmlrunner
17
18 class TestSequenceFunctions(unittest.TestCase):
19     def setUp(self):
20         self.seq = range(10)
21
22     def test_shuffle(self):
23         # make sure the shuffled sequence does not lose any elements
24         random.shuffle(self.seq)
25         self.seq.sort()
26         self.assertEqual(self.seq, range(10))
27
28     def test_choice(self):
29         element = random.choice(self.seq)
30         self.assert_(element in self.seq)
31
32     def test_sample(self):
33         self.assertRaises(ValueError, random.sample, self.seq, 20)
34         for element in random.sample(self.seq, 5):
35             self.assert_(element in self.seq)
36
37 if __name__ == '__main__':
38     unittest.main(testRunner=xmlrunner.XMLTestRunner(output='test-reports'))
39 """
40
41 import os
42 import sys
43 import time
44 from unittest import TestResult, _TextTestResult, TextTestRunner
45 from cStringIO import StringIO
46
47
48 class _TestInfo(object):
49     """This class is used to keep useful information about the execution of a
50     test method.
51     """
52
53     # Possible test outcomes
54     (SUCCESS, FAILURE, ERROR) = range(3)
55
56     def __init__(self, test_result, test_method, outcome=SUCCESS, err=None):
57         "Create a new instance of _TestInfo."
58         self.test_result = test_result
59         self.test_method = test_method
60         self.outcome = outcome
61         self.err = err
62         self.stdout = test_result.stdout and test_result.stdout.getvalue().strip() or ''
63         self.stderr = test_result.stdout and test_result.stderr.getvalue().strip() or ''
64
65     def get_elapsed_time(self):
66         """Return the time that shows how long the test method took to
67         execute.
68         """
69         return self.test_result.stop_time - self.test_result.start_time
70
71     def get_description(self):
72         "Return a text representation of the test method."
73         return self.test_result.getDescription(self.test_method)
74
75     def get_error_info(self):
76         """Return a text representation of an exception thrown by a test
77         method.
78         """
79         if not self.err:
80             return ''
81         if sys.version_info < (2,4):
82             return self.test_result._exc_info_to_string(self.err)
83         else:
84             return self.test_result._exc_info_to_string(
85                 self.err, self.test_method)
86
87
88 class _XMLTestResult(_TextTestResult):
89     """A test result class that can express test results in a XML report.
90
91     Used by XMLTestRunner.
92     """
93     def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1, \
94         elapsed_times=True):
95         "Create a new instance of _XMLTestResult."
96         _TextTestResult.__init__(self, stream, descriptions, verbosity)
97         self.successes = []
98         self.callback = None
99         self.elapsed_times = elapsed_times
100
101     def _prepare_callback(self, test_info, target_list, verbose_str,
102         short_str):
103         """Append a _TestInfo to the given target list and sets a callback
104         method to be called by stopTest method.
105         """
106         target_list.append(test_info)
107         def callback():
108             """This callback prints the test method outcome to the stream,
109             as well as the elapsed time.
110             """
111
112             # Ignore the elapsed times for a more reliable unit testing
113             if not self.elapsed_times:
114                 self.start_time = self.stop_time = 0
115
116             if self.showAll:
117                 self.stream.writeln('(%.3fs) %s' % \
118                     (test_info.get_elapsed_time(), verbose_str))
119             elif self.dots:
120                 self.stream.write(short_str)
121         self.callback = callback
122
123     def _patch_standard_output(self):
124         """Replace the stdout and stderr streams with string-based streams
125         in order to capture the tests' output.
126         """
127         (self.old_stdout, self.old_stderr) = (sys.stdout, sys.stderr)
128         (sys.stdout, sys.stderr) = (self.stdout, self.stderr) = \
129             (StringIO(), StringIO())
130
131     def _restore_standard_output(self):
132         "Restore the stdout and stderr streams."
133         (sys.stdout, sys.stderr) = (self.old_stdout, self.old_stderr)
134
135     def startTest(self, test):
136         "Called before execute each test method."
137         self._patch_standard_output()
138         self.start_time = time.time()
139         TestResult.startTest(self, test)
140
141         if self.showAll:
142             self.stream.write('  ' + self.getDescription(test))
143             self.stream.write(" ... ")
144
145     def stopTest(self, test):
146         "Called after execute each test method."
147         self._restore_standard_output()
148         _TextTestResult.stopTest(self, test)
149         self.stop_time = time.time()
150
151         if self.callback and callable(self.callback):
152             self.callback()
153             self.callback = None
154
155     def addSuccess(self, test):
156         "Called when a test executes successfully."
157         self._prepare_callback(_TestInfo(self, test),
158                                self.successes, 'OK', '.')
159
160     def addFailure(self, test, err):
161         "Called when a test method fails."
162         self._prepare_callback(_TestInfo(self, test, _TestInfo.FAILURE, err),
163                                self.failures, 'FAIL', 'F')
164
165     def addError(self, test, err):
166         "Called when a test method raises an error."
167         self._prepare_callback(_TestInfo(self, test, _TestInfo.ERROR, err),
168                                self.errors, 'ERROR', 'E')
169
170     def printErrorList(self, flavour, errors):
171         "Write some information about the FAIL or ERROR to the stream."
172         for test_info in errors:
173             self.stream.writeln(self.separator1)
174             self.stream.writeln('%s [%.3fs]: %s' % \
175                 (flavour, test_info.get_elapsed_time(), \
176                 test_info.get_description()))
177             self.stream.writeln(self.separator2)
178             self.stream.writeln('%s' % test_info.get_error_info())
179
180     def _get_info_by_testcase(self):
181         """This method organizes test results by TestCase module. This
182         information is used during the report generation, where a XML report
183         will be generated for each TestCase.
184         """
185         tests_by_testcase = {}
186
187         for tests in (self.successes, self.failures, self.errors):
188             for test_info in tests:
189                 testcase = type(test_info.test_method)
190
191                 # Ignore module name if it is '__main__'
192                 module = testcase.__module__ + '.'
193                 if module == '__main__.':
194                     module = ''
195                 testcase_name = module + testcase.__name__
196
197                 if not tests_by_testcase.has_key(testcase_name):
198                     tests_by_testcase[testcase_name] = []
199                 tests_by_testcase[testcase_name].append(test_info)
200
201         return tests_by_testcase
202
203     def _report_testsuite(suite_name, tests, xml_document):
204         "Appends the testsuite section to the XML document."
205         testsuite = xml_document.createElement('testsuite')
206         xml_document.appendChild(testsuite)
207
208         testsuite.setAttribute('name', str(suite_name))
209         testsuite.setAttribute('tests', str(len(tests)))
210
211         testsuite.setAttribute('time', '%.3f' % \
212             sum(map(lambda e: e.get_elapsed_time(), tests)))
213
214         failures = filter(lambda e: e.outcome==_TestInfo.FAILURE, tests)
215         testsuite.setAttribute('failures', str(len(failures)))
216
217         errors = filter(lambda e: e.outcome==_TestInfo.ERROR, tests)
218         testsuite.setAttribute('errors', str(len(errors)))
219
220         return testsuite
221
222     _report_testsuite = staticmethod(_report_testsuite)
223
224     def _report_testcase(suite_name, test_result, xml_testsuite, xml_document):
225         "Appends a testcase section to the XML document."
226         testcase = xml_document.createElement('testcase')
227         xml_testsuite.appendChild(testcase)
228
229         testcase.setAttribute('classname', str(suite_name))
230         testcase.setAttribute('name', test_result.test_method.shortDescription()
231                               or getattr(test_result.test_method, '_testMethodName',
232                                          str(test_result.test_method)))
233         testcase.setAttribute('time', '%.3f' % test_result.get_elapsed_time())
234
235         if (test_result.outcome != _TestInfo.SUCCESS):
236             elem_name = ('failure', 'error')[test_result.outcome-1]
237             failure = xml_document.createElement(elem_name)
238             testcase.appendChild(failure)
239
240             failure.setAttribute('type', str(test_result.err[0].__name__))
241             failure.setAttribute('message', str(test_result.err[1]))
242
243             error_info = test_result.get_error_info()
244             failureText = xml_document.createCDATASection(error_info)
245             failure.appendChild(failureText)
246
247     _report_testcase = staticmethod(_report_testcase)
248
249     def _report_output(test_runner, xml_testsuite, xml_document, stdout, stderr):
250         "Appends the system-out and system-err sections to the XML document."
251         systemout = xml_document.createElement('system-out')
252         xml_testsuite.appendChild(systemout)
253
254         systemout_text = xml_document.createCDATASection(stdout)
255         systemout.appendChild(systemout_text)
256
257         systemerr = xml_document.createElement('system-err')
258         xml_testsuite.appendChild(systemerr)
259
260         systemerr_text = xml_document.createCDATASection(stderr)
261         systemerr.appendChild(systemerr_text)
262
263     _report_output = staticmethod(_report_output)
264
265     def generate_reports(self, test_runner):
266         "Generates the XML reports to a given XMLTestRunner object."
267         from xml.dom.minidom import Document
268         all_results = self._get_info_by_testcase()
269
270         if type(test_runner.output) == str and not \
271             os.path.exists(test_runner.output):
272             os.makedirs(test_runner.output)
273
274         for suite, tests in all_results.items():
275             doc = Document()
276
277             # Build the XML file
278             testsuite = _XMLTestResult._report_testsuite(suite, tests, doc)
279             stdout, stderr = [], []
280             for test in tests:
281                 _XMLTestResult._report_testcase(suite, test, testsuite, doc)
282                 if test.stdout:
283                     stdout.extend(['*****************', test.get_description(), test.stdout])
284                 if test.stderr:
285                     stderr.extend(['*****************', test.get_description(), test.stderr])
286             _XMLTestResult._report_output(test_runner, testsuite, doc,
287                                           '\n'.join(stdout), '\n'.join(stderr))
288             xml_content = doc.toprettyxml(indent='\t')
289
290             if type(test_runner.output) is str:
291                 report_file = open('%s%sTEST-%s.xml' % \
292                     (test_runner.output, os.sep, suite), 'w')
293                 try:
294                     report_file.write(xml_content)
295                 finally:
296                     report_file.close()
297             else:
298                 # Assume that test_runner.output is a stream
299                 test_runner.output.write(xml_content)
300
301
302 class XMLTestRunner(TextTestRunner):
303     """A test runner class that outputs the results in JUnit like XML files.
304     """
305     def __init__(self, output='.', stream=sys.stderr, descriptions=True, \
306         verbose=False, elapsed_times=True):
307         "Create a new instance of XMLTestRunner."
308         verbosity = (1, 2)[verbose]
309         TextTestRunner.__init__(self, stream, descriptions, verbosity)
310         self.output = output
311         self.elapsed_times = elapsed_times
312
313     def _make_result(self):
314         """Create the TestResult object which will be used to store
315         information about the executed tests.
316         """
317         return _XMLTestResult(self.stream, self.descriptions, \
318             self.verbosity, self.elapsed_times)
319
320     def run(self, test):
321         "Run the given test case or test suite."
322         # Prepare the test execution
323         result = self._make_result()
324
325         # Print a nice header
326         self.stream.writeln()
327         self.stream.writeln('Running tests...')
328         self.stream.writeln(result.separator2)
329
330         # Execute tests
331         start_time = time.time()
332         test(result)
333         stop_time = time.time()
334         time_taken = stop_time - start_time
335
336         # Print results
337         result.printErrors()
338         self.stream.writeln(result.separator2)
339         run = result.testsRun
340         self.stream.writeln("Ran %d test%s in %.3fs" %
341             (run, run != 1 and "s" or "", time_taken))
342         self.stream.writeln()
343
344         # Error traces
345         if not result.wasSuccessful():
346             self.stream.write("FAILED (")
347             failed, errored = (len(result.failures), len(result.errors))
348             if failed:
349                 self.stream.write("failures=%d" % failed)
350             if errored:
351                 if failed:
352                     self.stream.write(", ")
353                 self.stream.write("errors=%d" % errored)
354             self.stream.writeln(")")
355         else:
356             self.stream.writeln("OK")
357
358         # Generate reports
359         self.stream.writeln()
360         self.stream.writeln('Generating XML reports...')
361         result.generate_reports(self)
362
363         return result