2 XML Test Runner for PyUnit
5 # Written by Sebastian Rittau <srittau@jroger.in-berlin.de> and placed in
6 # the Public Domain. With contributions by Paolo Borelli and others.
7 # Added to GNU Radio Oct. 3, 2010
9 from __future__ import with_statement
19 from xml.sax.saxutils import escape
22 from StringIO import StringIO
24 from io import StringIO
27 class _TestInfo(object):
29 """Information about a particular test.
31 Used by _XMLTestResult.
35 def __init__(self, test, time):
36 (self._class, self._method) = test.id().rsplit(".", 1)
42 def create_success(test, time):
43 """Create a _TestInfo instance for a successful test."""
44 return _TestInfo(test, time)
47 def create_failure(test, time, failure):
48 """Create a _TestInfo instance for a failed test."""
49 info = _TestInfo(test, time)
50 info._failure = failure
54 def create_error(test, time, error):
55 """Create a _TestInfo instance for an erroneous test."""
56 info = _TestInfo(test, time)
60 def print_report(self, stream):
61 """Print information about this test case in XML format to the
65 stream.write(' <testcase classname="%(class)s" name="%(method)s" time="%(time).4f">' % \
68 "method": self._method,
71 if self._failure is not None:
72 self._print_error(stream, 'failure', self._failure)
73 if self._error is not None:
74 self._print_error(stream, 'error', self._error)
75 stream.write('</testcase>\n')
77 def _print_error(self, stream, tagname, error):
78 """Print information from a failure or error to the supplied stream."""
79 text = escape(str(error[1]))
81 stream.write(' <%s type="%s">%s\n' \
82 % (tagname, _clsname(error[0]), text))
83 tb_stream = StringIO()
84 traceback.print_tb(error[2], None, tb_stream)
85 stream.write(escape(tb_stream.getvalue()))
86 stream.write(' </%s>\n' % tagname)
91 return cls.__module__ + "." + cls.__name__
94 class _XMLTestResult(unittest.TestResult):
96 """A test result class that stores result as XML.
98 Used by XMLTestRunner.
102 def __init__(self, classname):
103 unittest.TestResult.__init__(self)
104 self._test_name = classname
105 self._start_time = None
110 def startTest(self, test):
111 unittest.TestResult.startTest(self, test)
114 self._start_time = time.time()
116 def stopTest(self, test):
117 time_taken = time.time() - self._start_time
118 unittest.TestResult.stopTest(self, test)
120 info = _TestInfo.create_error(test, time_taken, self._error)
122 info = _TestInfo.create_failure(test, time_taken, self._failure)
124 info = _TestInfo.create_success(test, time_taken)
125 self._tests.append(info)
127 def addError(self, test, err):
128 unittest.TestResult.addError(self, test, err)
131 def addFailure(self, test, err):
132 unittest.TestResult.addFailure(self, test, err)
135 def print_report(self, stream, time_taken, out, err):
136 """Prints the XML report to the supplied stream.
138 The time the tests took to perform as well as the captured standard
139 output and standard error streams must be passed in.a
142 stream.write('<testsuite errors="%(e)d" failures="%(f)d" ' % \
143 { "e": len(self.errors), "f": len(self.failures) })
144 stream.write('name="%(n)s" tests="%(t)d" time="%(time).3f">\n' % \
146 "n": self._test_name,
150 for info in self._tests:
151 info.print_report(stream)
152 stream.write(' <system-out><![CDATA[%s]]></system-out>\n' % out)
153 stream.write(' <system-err><![CDATA[%s]]></system-err>\n' % err)
154 stream.write('</testsuite>\n')
157 class XMLTestRunner(object):
159 """A test runner that stores results in XML format compatible with JUnit.
161 XMLTestRunner(stream=None) -> XML test runner
163 The XML file is written to the supplied stream. If stream is None, the
164 results are stored in a file called TEST-<module>.<class>.xml in the
165 current working directory (if not overridden with the path property),
166 where <module> and <class> are the module and class name of the test class.
170 def __init__(self, stream=None):
171 self._stream = stream
175 """Run the given test case or test suite."""
176 class_ = test.__class__
177 classname = class_.__module__ + "." + class_.__name__
178 if self._stream == None:
179 filename = "TEST-%s.xml" % classname
180 stream = file(os.path.join(self._path, filename), "w")
181 stream.write('<?xml version="1.0" encoding="utf-8"?>\n')
183 stream = self._stream
185 result = _XMLTestResult(classname)
186 start_time = time.time()
188 with _fake_std_streams():
191 out_s = sys.stdout.getvalue()
192 except AttributeError:
195 err_s = sys.stderr.getvalue()
196 except AttributeError:
199 time_taken = time.time() - start_time
200 result.print_report(stream, time_taken, out_s, err_s)
201 if self._stream is None:
206 def _set_path(self, path):
209 path = property(lambda self: self._path, _set_path, None,
210 """The path where the XML files are stored.
212 This property is ignored when the XML file is written to a file
216 class _fake_std_streams(object):
219 self._orig_stdout = sys.stdout
220 self._orig_stderr = sys.stderr
221 sys.stdout = StringIO()
222 sys.stderr = StringIO()
224 def __exit__(self, exc_type, exc_val, exc_tb):
225 sys.stdout = self._orig_stdout
226 sys.stderr = self._orig_stderr
229 class XMLTestRunnerTest(unittest.TestCase):
232 self._stream = StringIO()
234 def _try_test_run(self, test_class, expected):
236 """Run the test suite against the supplied test class and compare the
237 XML result against the expected XML string. Fail if the expected
238 string doesn't match the actual string. All time attributes in the
239 expected string should have the value "0.000". All error and failure
240 messages are reduced to "Foobar".
244 runner = XMLTestRunner(self._stream)
245 runner.run(unittest.makeSuite(test_class))
247 got = self._stream.getvalue()
248 # Replace all time="X.YYY" attributes by time="0.000" to enable a
249 # simple string comparison.
250 got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got)
251 # Likewise, replace all failure and error messages by a simple "Foobar"
253 got = re.sub(r'(?s)<failure (.*?)>.*?</failure>', r'<failure \1>Foobar</failure>', got)
254 got = re.sub(r'(?s)<error (.*?)>.*?</error>', r'<error \1>Foobar</error>', got)
255 # And finally Python 3 compatibility.
256 got = got.replace('type="builtins.', 'type="exceptions.')
258 self.assertEqual(expected, got)
260 def test_no_tests(self):
261 """Regression test: Check whether a test run without any tests
262 matches a previous run.
265 class TestTest(unittest.TestCase):
267 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="0" time="0.000">
268 <system-out><![CDATA[]]></system-out>
269 <system-err><![CDATA[]]></system-err>
273 def test_success(self):
274 """Regression test: Check whether a test run with a successful test
275 matches a previous run.
278 class TestTest(unittest.TestCase):
281 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
282 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
283 <system-out><![CDATA[]]></system-out>
284 <system-err><![CDATA[]]></system-err>
288 def test_failure(self):
289 """Regression test: Check whether a test run with a failing test
290 matches a previous run.
293 class TestTest(unittest.TestCase):
296 self._try_test_run(TestTest, """<testsuite errors="0" failures="1" name="unittest.TestSuite" tests="1" time="0.000">
297 <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
298 <failure type="exceptions.AssertionError">Foobar</failure>
300 <system-out><![CDATA[]]></system-out>
301 <system-err><![CDATA[]]></system-err>
305 def test_error(self):
306 """Regression test: Check whether a test run with a erroneous test
307 matches a previous run.
310 class TestTest(unittest.TestCase):
313 self._try_test_run(TestTest, """<testsuite errors="1" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
314 <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
315 <error type="exceptions.IndexError">Foobar</error>
317 <system-out><![CDATA[]]></system-out>
318 <system-err><![CDATA[]]></system-err>
322 def test_stdout_capture(self):
323 """Regression test: Check whether a test run with output to stdout
324 matches a previous run.
327 class TestTest(unittest.TestCase):
329 sys.stdout.write("Test\n")
330 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
331 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
332 <system-out><![CDATA[Test
334 <system-err><![CDATA[]]></system-err>
338 def test_stderr_capture(self):
339 """Regression test: Check whether a test run with output to stderr
340 matches a previous run.
343 class TestTest(unittest.TestCase):
345 sys.stderr.write("Test\n")
346 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
347 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
348 <system-out><![CDATA[]]></system-out>
349 <system-err><![CDATA[Test
354 class NullStream(object):
355 """A file-like object that discards everything written to it."""
356 def write(self, buffer):
359 def test_unittests_changing_stdout(self):
360 """Check whether the XMLTestRunner recovers gracefully from unit tests
361 that change stdout, but don't change it back properly.
364 class TestTest(unittest.TestCase):
366 sys.stdout = XMLTestRunnerTest.NullStream()
368 runner = XMLTestRunner(self._stream)
369 runner.run(unittest.makeSuite(TestTest))
371 def test_unittests_changing_stderr(self):
372 """Check whether the XMLTestRunner recovers gracefully from unit tests
373 that change stderr, but don't change it back properly.
376 class TestTest(unittest.TestCase):
378 sys.stderr = XMLTestRunnerTest.NullStream()
380 runner = XMLTestRunner(self._stream)
381 runner.run(unittest.makeSuite(TestTest))
384 if __name__ == "__main__":