Adding XML output to Python unittests.
[debian/gnuradio] / gnuradio-core / src / python / gnuradio / gr_xmlrunner.py
1 """
2 XML Test Runner for PyUnit
3 """
4
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
8
9 from __future__ import with_statement
10
11 __version__ = "0.1"
12
13 import os.path
14 import re
15 import sys
16 import time
17 import traceback
18 import unittest
19 from xml.sax.saxutils import escape
20
21 try:
22     from StringIO import StringIO
23 except ImportError:
24     from io import StringIO
25
26
27 class _TestInfo(object):
28
29     """Information about a particular test.
30     
31     Used by _XMLTestResult.
32     
33     """
34
35     def __init__(self, test, time):
36         (self._class, self._method) = test.id().rsplit(".", 1)
37         self._time = time
38         self._error = None
39         self._failure = None
40
41     @staticmethod
42     def create_success(test, time):
43         """Create a _TestInfo instance for a successful test."""
44         return _TestInfo(test, time)
45
46     @staticmethod
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
51         return info
52
53     @staticmethod
54     def create_error(test, time, error):
55         """Create a _TestInfo instance for an erroneous test."""
56         info = _TestInfo(test, time)
57         info._error = error
58         return info
59
60     def print_report(self, stream):
61         """Print information about this test case in XML format to the
62         supplied stream.
63
64         """
65         stream.write('  <testcase classname="%(class)s" name="%(method)s" time="%(time).4f">' % \
66             {
67                 "class": self._class,
68                 "method": self._method,
69                 "time": self._time,
70             })
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')
76
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]))
80         stream.write('\n')
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)
87         stream.write('  ')
88
89
90 def _clsname(cls):
91     return cls.__module__ + "." + cls.__name__
92
93
94 class _XMLTestResult(unittest.TestResult):
95
96     """A test result class that stores result as XML.
97
98     Used by XMLTestRunner.
99
100     """
101
102     def __init__(self, classname):
103         unittest.TestResult.__init__(self)
104         self._test_name = classname
105         self._start_time = None
106         self._tests = []
107         self._error = None
108         self._failure = None
109
110     def startTest(self, test):
111         unittest.TestResult.startTest(self, test)
112         self._error = None
113         self._failure = None
114         self._start_time = time.time()
115
116     def stopTest(self, test):
117         time_taken = time.time() - self._start_time
118         unittest.TestResult.stopTest(self, test)
119         if self._error:
120             info = _TestInfo.create_error(test, time_taken, self._error)
121         elif self._failure:
122             info = _TestInfo.create_failure(test, time_taken, self._failure)
123         else:
124             info = _TestInfo.create_success(test, time_taken)
125         self._tests.append(info)
126
127     def addError(self, test, err):
128         unittest.TestResult.addError(self, test, err)
129         self._error = err
130
131     def addFailure(self, test, err):
132         unittest.TestResult.addFailure(self, test, err)
133         self._failure = err
134
135     def print_report(self, stream, time_taken, out, err):
136         """Prints the XML report to the supplied stream.
137         
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
140
141         """
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' % \
145             {
146                 "n": self._test_name,
147                 "t": self.testsRun,
148                 "time": time_taken,
149             })
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')
155
156
157 class XMLTestRunner(object):
158
159     """A test runner that stores results in XML format compatible with JUnit.
160
161     XMLTestRunner(stream=None) -> XML test runner
162
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.
167
168     """
169
170     def __init__(self, stream=None):
171         self._stream = stream
172         self._path = "."
173
174     def run(self, test):
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')
182         else:
183             stream = self._stream
184
185         result = _XMLTestResult(classname)
186         start_time = time.time()
187
188         with _fake_std_streams():
189             test(result)
190             try:
191                 out_s = sys.stdout.getvalue()
192             except AttributeError:
193                 out_s = ""
194             try:
195                 err_s = sys.stderr.getvalue()
196             except AttributeError:
197                 err_s = ""
198
199         time_taken = time.time() - start_time
200         result.print_report(stream, time_taken, out_s, err_s)
201         if self._stream is None:
202             stream.close()
203
204         return result
205
206     def _set_path(self, path):
207         self._path = path
208
209     path = property(lambda self: self._path, _set_path, None,
210             """The path where the XML files are stored.
211             
212             This property is ignored when the XML file is written to a file
213             stream.""")
214
215
216 class _fake_std_streams(object):
217
218     def __enter__(self):
219         self._orig_stdout = sys.stdout
220         self._orig_stderr = sys.stderr
221         sys.stdout = StringIO()
222         sys.stderr = StringIO()
223
224     def __exit__(self, exc_type, exc_val, exc_tb):
225         sys.stdout = self._orig_stdout
226         sys.stderr = self._orig_stderr
227
228
229 class XMLTestRunnerTest(unittest.TestCase):
230
231     def setUp(self):
232         self._stream = StringIO()
233
234     def _try_test_run(self, test_class, expected):
235
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".
241
242         """
243
244         runner = XMLTestRunner(self._stream)
245         runner.run(unittest.makeSuite(test_class))
246
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"
252         # string.
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.')
257
258         self.assertEqual(expected, got)
259
260     def test_no_tests(self):
261         """Regression test: Check whether a test run without any tests
262         matches a previous run.
263         
264         """
265         class TestTest(unittest.TestCase):
266             pass
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>
270 </testsuite>
271 """)
272
273     def test_success(self):
274         """Regression test: Check whether a test run with a successful test
275         matches a previous run.
276         
277         """
278         class TestTest(unittest.TestCase):
279             def test_foo(self):
280                 pass
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>
285 </testsuite>
286 """)
287
288     def test_failure(self):
289         """Regression test: Check whether a test run with a failing test
290         matches a previous run.
291         
292         """
293         class TestTest(unittest.TestCase):
294             def test_foo(self):
295                 self.assert_(False)
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>
299   </testcase>
300   <system-out><![CDATA[]]></system-out>
301   <system-err><![CDATA[]]></system-err>
302 </testsuite>
303 """)
304
305     def test_error(self):
306         """Regression test: Check whether a test run with a erroneous test
307         matches a previous run.
308         
309         """
310         class TestTest(unittest.TestCase):
311             def test_foo(self):
312                 raise IndexError()
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>
316   </testcase>
317   <system-out><![CDATA[]]></system-out>
318   <system-err><![CDATA[]]></system-err>
319 </testsuite>
320 """)
321
322     def test_stdout_capture(self):
323         """Regression test: Check whether a test run with output to stdout
324         matches a previous run.
325         
326         """
327         class TestTest(unittest.TestCase):
328             def test_foo(self):
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
333 ]]></system-out>
334   <system-err><![CDATA[]]></system-err>
335 </testsuite>
336 """)
337
338     def test_stderr_capture(self):
339         """Regression test: Check whether a test run with output to stderr
340         matches a previous run.
341         
342         """
343         class TestTest(unittest.TestCase):
344             def test_foo(self):
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
350 ]]></system-err>
351 </testsuite>
352 """)
353
354     class NullStream(object):
355         """A file-like object that discards everything written to it."""
356         def write(self, buffer):
357             pass
358
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.
362
363         """
364         class TestTest(unittest.TestCase):
365             def test_foo(self):
366                 sys.stdout = XMLTestRunnerTest.NullStream()
367
368         runner = XMLTestRunner(self._stream)
369         runner.run(unittest.makeSuite(TestTest))
370
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.
374
375         """
376         class TestTest(unittest.TestCase):
377             def test_foo(self):
378                 sys.stderr = XMLTestRunnerTest.NullStream()
379
380         runner = XMLTestRunner(self._stream)
381         runner.run(unittest.makeSuite(TestTest))
382
383
384 if __name__ == "__main__":
385     unittest.main()