1 """
2 XML Test Runner for PyUnit
3 """
4
5
6
7
8 from __future__ import print_function
9
10 __revision__ = "$Id$"
11
12 import os.path
13 import re
14 import sys
15 import time
16 import traceback
17 import unittest
18 try:
19 from cStringIO import StringIO
20 except ImportError:
21 from io import StringIO
22 from xml.sax.saxutils import escape
23 import xml.etree.ElementTree as ET
26 return '<![CDATA[\n{}\n]]>'.format(cdata_text)
27
29
30 """Information about a particular test.
31
32 Used by _XMLTestResult.
33
34 """
35
37 (self._class, self._method) = test.id().rsplit(".", 1)
38 self._time = time
39 self._error = None
40 self._failure = None
41
42 @staticmethod
44 """Create a _TestInfo instance for a successful test."""
45 return _TestInfo(test, time)
46
47 @staticmethod
49 """Create a _TestInfo instance for a failed test."""
50 info = _TestInfo(test, time)
51 info._failure = failure
52 return info
53
54 @staticmethod
56 """Create a _TestInfo instance for an erroneous test."""
57 info = _TestInfo(test, time)
58 info._error = error
59 return info
60
62 """Create an XML tag with information about this test case.
63
64 """
65 testcase = ET.Element("testcase")
66 testcase.set('classname', self._class)
67 testcase.set('name', self._method)
68 testcase.set('time', '%.4f' % self._time)
69 if self._failure != None:
70 self._print_error(testcase, 'failure', self._failure)
71 if self._error != None:
72 self._print_error(testcase, 'error', self._error)
73 return testcase
74
76 """Print information about this test case in XML format to the
77 supplied stream.
78
79 """
80 stream.write(ET.tostring(self.xml()))
81
82 - def print_report_text(self, stream):
83
84
85
86
87
88
89 stream.write(self._method)
90 if self._failure != None:
91 stream.write(' ... FAILURE!\n')
92 self._print_error_text(stream, 'failure', self._failure)
93 if self._error != None:
94 stream.write(' ... ERROR!\n')
95 self._print_error_text(stream, 'error', self._error)
96 if self._failure == None and self._error == None:
97 stream.write(' ... ok\n')
98
100 """
101 Append an XML tag with information from a failure or error to the
102 supplied testcase.
103 """
104 tag = ET.SubElement(testcase, tagname)
105 tag.set('type', str(error[0].__name__))
106 tb_stream = StringIO()
107 traceback.print_tb(error[2], None, tb_stream)
108 tag.text ='%s\n%s' % (str(error[1]), tb_stream.getvalue())
109
110 - def _print_error_text(self, stream, tagname, error):
111 """Print information from a failure or error to the supplied stream."""
112 text = escape(str(error[1]))
113 stream.write('%s: %s\n' \
114 % (tagname.upper(), text))
115 tb_stream = StringIO()
116 traceback.print_tb(error[2], None, tb_stream)
117 stream.write(escape(tb_stream.getvalue()))
118 stream.write('-'*80 + '\n')
119
121
122 """A test result class that stores result as XML.
123
124 Used by XMLTestRunner.
125
126 """
127
129 unittest.TestResult.__init__(self)
130 self._test_name = classname
131 self._start_time = None
132 self._tests = []
133 self._error = None
134 self._failure = None
135
137 unittest.TestResult.startTest(self, test)
138 self._error = None
139 self._failure = None
140 self._start_time = time.time()
141
143 time_taken = time.time() - self._start_time
144 unittest.TestResult.stopTest(self, test)
145 if self._error:
146 info = _TestInfo.create_error(test, time_taken, self._error)
147 elif self._failure:
148 info = _TestInfo.create_failure(test, time_taken, self._failure)
149 else:
150 info = _TestInfo.create_success(test, time_taken)
151 self._tests.append(info)
152
154 unittest.TestResult.addError(self, test, err)
155 self._error = err
156
158 unittest.TestResult.addFailure(self, test, err)
159 self._failure = err
160
162 invalid_chars = re.compile(ur'[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]')
163 def invalid_char_replacer(m):
164 return "&#x"+('%04X' % ord(m.group(0)))+";"
165 return re.sub(invalid_chars, invalid_char_replacer, str(text))
166
167 - def xml(self, time_taken, out, err):
168 """
169 @return XML tag representing the object
170 @rtype: xml.etree.ElementTree.Element
171 """
172 test_suite = ET.Element('testsuite')
173 test_suite.set('errors', str(len(self.errors)))
174 test_suite.set('failures', str(len(self.failures)))
175 test_suite.set('name', self._test_name)
176 test_suite.set('tests', str(self.testsRun))
177 test_suite.set('time', '%.3f' % time_taken)
178 for info in self._tests:
179 test_suite.append(info.xml())
180 system_out = ET.SubElement(test_suite, 'system-out')
181 system_out.text = cdata(self.filter_nonprintable_text(out))
182 system_err = ET.SubElement(test_suite, 'system-err')
183 system_err.text = cdata(self.filter_nonprintable_text(err))
184 return ET.ElementTree(test_suite)
185
187 """Prints the XML report to the supplied stream.
188
189 The time the tests took to perform as well as the captured standard
190 output and standard error streams must be passed in.a
191
192 """
193 stream.write(ET.tostring(self.xml(time_taken, out, err).getroot(), encoding='utf-8', method='xml'))
194
195 - def print_report_text(self, stream, time_taken, out, err):
196 """Prints the text report to the supplied stream.
197
198 The time the tests took to perform as well as the captured standard
199 output and standard error streams must be passed in.a
200
201 """
202
203
204
205
206
207
208
209
210 for info in self._tests:
211 info.print_report_text(stream)
212
215
216 """A test runner that stores results in XML format compatible with JUnit.
217
218 XMLTestRunner(stream=None) -> XML test runner
219
220 The XML file is written to the supplied stream. If stream is None, the
221 results are stored in a file called TEST-<module>.<class>.xml in the
222 current working directory (if not overridden with the path property),
223 where <module> and <class> are the module and class name of the test class.
224
225 """
226
228 self._stream = stream
229 self._path = "."
230
231 - def run(self, test):
232 """Run the given test case or test suite."""
233 class_ = test.__class__
234 classname = class_.__module__ + "." + class_.__name__
235 if self._stream == None:
236 filename = "TEST-%s.xml" % classname
237 stream = file(os.path.join(self._path, filename), "w")
238 stream.write('<?xml version="1.0" encoding="utf-8"?>\n')
239 else:
240 stream = self._stream
241
242 result = _XMLTestResult(classname)
243 start_time = time.time()
244
245
246 old_stdout = sys.stdout
247 old_stderr = sys.stderr
248 sys.stdout = StringIO()
249 sys.stderr = StringIO()
250
251 try:
252 test(result)
253 try:
254 out_s = sys.stdout.getvalue()
255 except AttributeError:
256 out_s = ""
257 try:
258 err_s = sys.stderr.getvalue()
259 except AttributeError:
260 err_s = ""
261 finally:
262 sys.stdout = old_stdout
263 sys.stderr = old_stderr
264
265 time_taken = time.time() - start_time
266 result.print_report(stream, time_taken, out_s, err_s)
267
268 result.print_report_text(sys.stdout, time_taken, out_s, err_s)
269
270 return result
271
274
275 path = property(lambda self: self._path, _set_path, None,
276 """The path where the XML files are stored.
277
278 This property is ignored when the XML file is written to a file
279 stream.""")
280
284 self._stream = StringIO()
285
287
288 """Run the test suite against the supplied test class and compare the
289 XML result against the expected XML string. Fail if the expected
290 string doesn't match the actual string. All time attribute in the
291 expected string should have the value "0.000". All error and failure
292 messages are reduced to "Foobar".
293
294 """
295
296 runner = XMLTestRunner(self._stream)
297 runner.run(unittest.makeSuite(test_class))
298
299 got = self._stream.getvalue()
300
301
302 got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got)
303
304
305 got = re.sub(r'(?s)<failure (.*?)>.*?</failure>', r'<failure \1>Foobar</failure>', got)
306 got = re.sub(r'(?s)<error (.*?)>.*?</error>', r'<error \1>Foobar</error>', got)
307
308 self.assertEqual(expected, got)
309
311 """Regression test: Check whether a test run without any tests
312 matches a previous run.
313
314 """
315 class TestTest(unittest.TestCase):
316 pass
317 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="0" time="0.000">
318 <system-out><![CDATA[]]></system-out>
319 <system-err><![CDATA[]]></system-err>
320 </testsuite>
321 """)
322
324 """Regression test: Check whether a test run with a successful test
325 matches a previous run.
326
327 """
328 class TestTest(unittest.TestCase):
329 def test_foo(self):
330 pass
331 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
332 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
333 <system-out><![CDATA[]]></system-out>
334 <system-err><![CDATA[]]></system-err>
335 </testsuite>
336 """)
337
339 """Regression test: Check whether a test run with a failing test
340 matches a previous run.
341
342 """
343 class TestTest(unittest.TestCase):
344 def test_foo(self):
345 self.assert_(False)
346 self._try_test_run(TestTest, """<testsuite errors="0" failures="1" name="unittest.TestSuite" tests="1" time="0.000">
347 <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
348 <failure type="exceptions.AssertionError">Foobar</failure>
349 </testcase>
350 <system-out><![CDATA[]]></system-out>
351 <system-err><![CDATA[]]></system-err>
352 </testsuite>
353 """)
354
356 """Regression test: Check whether a test run with a erroneous test
357 matches a previous run.
358
359 """
360 class TestTest(unittest.TestCase):
361 def test_foo(self):
362 raise IndexError()
363 self._try_test_run(TestTest, """<testsuite errors="1" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
364 <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
365 <error type="exceptions.IndexError">Foobar</error>
366 </testcase>
367 <system-out><![CDATA[]]></system-out>
368 <system-err><![CDATA[]]></system-err>
369 </testsuite>
370 """)
371
373 """Regression test: Check whether a test run with output to stdout
374 matches a previous run.
375
376 """
377 class TestTest(unittest.TestCase):
378 def test_foo(self):
379 print("Test")
380 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
381 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
382 <system-out><![CDATA[Test
383 ]]></system-out>
384 <system-err><![CDATA[]]></system-err>
385 </testsuite>
386 """)
387
389 """Regression test: Check whether a test run with output to stderr
390 matches a previous run.
391
392 """
393 class TestTest(unittest.TestCase):
394 def test_foo(self):
395 print("Test", file=sys.stderr)
396 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
397 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
398 <system-out><![CDATA[]]></system-out>
399 <system-err><![CDATA[Test
400 ]]></system-err>
401 </testsuite>
402 """)
403
405 """A file-like object that discards everything written to it."""
406 - def write(self, buffer):
408
410 """Check whether the XMLTestRunner recovers gracefully from unit tests
411 that change stdout, but don't change it back properly.
412
413 """
414 class TestTest(unittest.TestCase):
415 def test_foo(self):
416 sys.stdout = XMLTestRunnerTest.NullStream()
417
418 runner = XMLTestRunner(self._stream)
419 runner.run(unittest.makeSuite(TestTest))
420
422 """Check whether the XMLTestRunner recovers gracefully from unit tests
423 that change stderr, but don't change it back properly.
424
425 """
426 class TestTest(unittest.TestCase):
427 def test_foo(self):
428 sys.stderr = XMLTestRunnerTest.NullStream()
429
430 runner = XMLTestRunner(self._stream)
431 runner.run(unittest.makeSuite(TestTest))
432
436 if self.testRunner is None:
437 self.testRunner = XMLTestRunner()
438 unittest.TestProgram.runTests(self)
439
440 main = XMLTestProgram
441
442
443 if __name__ == "__main__":
444 main(module=None)
445