Package rosunit :: Module xmlrunner
[frames] | no frames]

Source Code for Module rosunit.xmlrunner

  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. 
  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 
24 25 -def cdata(cdata_text):
26 return '<![CDATA[\n{}\n]]>'.format(cdata_text)
27
28 -class _TestInfo(object):
29 30 """Information about a particular test. 31 32 Used by _XMLTestResult. 33 34 """ 35
36 - def __init__(self, test, time):
37 (self._class, self._method) = test.id().rsplit(".", 1) 38 self._time = time 39 self._error = None 40 self._failure = None
41 42 @staticmethod
43 - def create_success(test, time):
44 """Create a _TestInfo instance for a successful test.""" 45 return _TestInfo(test, time)
46 47 @staticmethod
48 - def create_failure(test, time, failure):
49 """Create a _TestInfo instance for a failed test.""" 50 info = _TestInfo(test, time) 51 info._failure = failure 52 return info
53 54 @staticmethod
55 - def create_error(test, time, error):
56 """Create a _TestInfo instance for an erroneous test.""" 57 info = _TestInfo(test, time) 58 info._error = error 59 return info
60
61 - def xml(self):
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
75 - def print_report(self, stream):
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 #stream.write(' <testcase classname="%(class)s" name="%(method)s" time="%(time).4f">' % \ 84 # { 85 # "class": self._class, 86 # "method": self._method, 87 # "time": self._time, 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
99 - def _print_error(self, testcase, tagname, error):
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
120 -class _XMLTestResult(unittest.TestResult):
121 122 """A test result class that stores result as XML. 123 124 Used by XMLTestRunner. 125 126 """ 127
128 - def __init__(self, classname):
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
136 - def startTest(self, test):
137 unittest.TestResult.startTest(self, test) 138 self._error = None 139 self._failure = None 140 self._start_time = time.time()
141
142 - def stopTest(self, test):
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
153 - def addError(self, test, err):
154 unittest.TestResult.addError(self, test, err) 155 self._error = err
156
157 - def addFailure(self, test, err):
158 unittest.TestResult.addFailure(self, test, err) 159 self._failure = err
160
161 - def filter_nonprintable_text(self, text):
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
186 - def print_report(self, stream, time_taken, out, err):
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 #stream.write('<testsuite errors="%(e)d" failures="%(f)d" ' % \ 203 # { "e": len(self.errors), "f": len(self.failures) }) 204 #stream.write('name="%(n)s" tests="%(t)d" time="%(time).3f">\n' % \ 205 # { 206 # "n": self._test_name, 207 # "t": self.testsRun, 208 # "time": time_taken, 209 # }) 210 for info in self._tests: 211 info.print_report_text(stream)
212
213 214 -class XMLTestRunner(object):
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
227 - def __init__(self, stream=None):
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 # TODO: Python 2.5: Use the with statement 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
272 - def _set_path(self, path):
273 self._path = path
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
281 282 -class XMLTestRunnerTest(unittest.TestCase):
283 - def setUp(self):
284 self._stream = StringIO()
285
286 - def _try_test_run(self, test_class, expected):
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 # Replace all time="X.YYY" attributes by time="0.000" to enable a 301 # simple string comparison. 302 got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got) 303 # Likewise, replace all failure and error messages by a simple "Foobar" 304 # string. 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
310 - def test_no_tests(self):
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
323 - def test_success(self):
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
338 - def test_failure(self):
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
355 - def test_error(self):
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
372 - def test_stdout_capture(self):
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
388 - def test_stderr_capture(self):
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
404 - class NullStream(object):
405 """A file-like object that discards everything written to it."""
406 - def write(self, buffer):
407 pass
408
409 - def test_unittests_changing_stdout(self):
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
421 - def test_unittests_changing_stderr(self):
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
433 434 -class XMLTestProgram(unittest.TestProgram):
435 - def runTests(self):
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