00001
00002
00003 """
00004 XML Test Runner for PyUnit
00005 """
00006
00007
00008
00009
00010 __revision__ = "$Id: /private/python/stdlib/xmlrunner.py 16654 2007-11-12T12:46:35.368945Z srittau $"
00011
00012 import os.path
00013 import re
00014 import sys
00015 import time
00016 import traceback
00017 import unittest
00018 from StringIO import StringIO
00019 from xml.sax.saxutils import escape
00020
00021 from StringIO import StringIO
00022
00023 def strip_ml_tags(in_text):
00024 """Description: Removes all HTML/XML-like tags from the input text.
00025 Inputs: s --> string of text
00026 Outputs: text string without the tags
00027
00028 # doctest unit testing framework
00029
00030 >>> test_text = "Keep this Text <remove><me /> KEEP </remove> 123"
00031 >>> strip_ml_tags(test_text)
00032 'Keep this Text KEEP 123'
00033 """
00034
00035 s_list = list(in_text)
00036 i,j = 0,0
00037
00038 while i < len(s_list):
00039
00040 if s_list[i] == '<':
00041 while s_list[i] != '>':
00042
00043 s_list.pop(i)
00044
00045
00046 s_list.pop(i)
00047 else:
00048 i=i+1
00049
00050
00051 join_char=''
00052 return join_char.join(s_list)
00053
00054
00055 class _TestInfo(object):
00056
00057 """Information about a particular test.
00058
00059 Used by _XMLTestResult.
00060
00061 """
00062
00063 def __init__(self, test, time):
00064 (self._class, self._method) = test.id().rsplit(".", 1)
00065 self._time = time
00066 self._error = None
00067 self._failure = None
00068
00069 @staticmethod
00070 def create_success(test, time):
00071 """Create a _TestInfo instance for a successful test."""
00072 return _TestInfo(test, time)
00073
00074 @staticmethod
00075 def create_failure(test, time, failure):
00076 """Create a _TestInfo instance for a failed test."""
00077 info = _TestInfo(test, time)
00078 info._failure = failure
00079 return info
00080
00081 @staticmethod
00082 def create_error(test, time, error):
00083 """Create a _TestInfo instance for an erroneous test."""
00084 info = _TestInfo(test, time)
00085 info._error = error
00086 return info
00087
00088 def print_report(self, stream):
00089 """Print information about this test case in XML format to the
00090 supplied stream.
00091
00092 """
00093 stream.write(' <testcase classname="%(class)s" name="%(method)s" time="%(time).4f">' % \
00094 {
00095 "class": self._class,
00096 "method": self._method,
00097 "time": self._time,
00098 })
00099 if self._failure != None:
00100 self._print_error(stream, 'failure', self._failure)
00101 if self._error != None:
00102 self._print_error(stream, 'error', self._error)
00103 stream.write('</testcase>\n')
00104
00105 def _print_error(self, stream, tagname, error):
00106 """Print information from a failure or error to the supplied stream."""
00107 text = escape(str(error[1]))
00108 stream.write('\n')
00109 stream.write(' <%s type="%s">%s\n' \
00110 % (tagname, str(error[0]).strip("<>"), strip_ml_tags(text)))
00111 tb_stream = StringIO()
00112 traceback.print_tb(error[2], None, tb_stream)
00113 stream.write(escape(tb_stream.getvalue()))
00114 stream.write(' </%s>\n' % tagname)
00115 stream.write(' ')
00116
00117
00118 class _XMLTestResult(unittest.TestResult):
00119
00120 """A test result class that stores result as XML.
00121
00122 Used by XMLTestRunner.
00123
00124 """
00125
00126 def __init__(self, classname):
00127 unittest.TestResult.__init__(self)
00128 self._test_name = classname
00129 self._start_time = None
00130 self._tests = []
00131 self._error = None
00132 self._failure = None
00133
00134 def startTest(self, test):
00135 unittest.TestResult.startTest(self, test)
00136 self._error = None
00137 self._failure = None
00138 self._start_time = time.time()
00139
00140 def stopTest(self, test):
00141 time_taken = time.time() - self._start_time
00142 unittest.TestResult.stopTest(self, test)
00143 if self._error:
00144 info = _TestInfo.create_error(test, time_taken, self._error)
00145 elif self._failure:
00146 info = _TestInfo.create_failure(test, time_taken, self._failure)
00147 else:
00148 info = _TestInfo.create_success(test, time_taken)
00149 self._tests.append(info)
00150
00151 def addError(self, test, err):
00152 unittest.TestResult.addError(self, test, err)
00153 self._error = err
00154
00155 def addFailure(self, test, err):
00156 unittest.TestResult.addFailure(self, test, err)
00157 self._failure = err
00158
00159 def print_report(self, stream, time_taken, out, err):
00160 """Prints the XML report to the supplied stream.
00161
00162 The time the tests took to perform as well as the captured standard
00163 output and standard error streams must be passed in.a
00164
00165 """
00166 stream.write('<testsuite errors="%(e)d" failures="%(f)d" ' % \
00167 { "e": len(self.errors), "f": len(self.failures) })
00168 stream.write('name="%(n)s" tests="%(t)d" time="%(time).3f">\n' % \
00169 {
00170 "n": self._test_name,
00171 "t": self.testsRun,
00172 "time": time_taken,
00173 })
00174 for info in self._tests:
00175 info.print_report(stream)
00176 stream.write(' <system-out><![CDATA[%s]]></system-out>\n' % out)
00177 stream.write(' <system-err><![CDATA[%s]]></system-err>\n' % err)
00178 stream.write('</testsuite>\n')
00179
00180
00181 class XMLTestRunner(object):
00182
00183 """A test runner that stores results in XML format compatible with JUnit.
00184
00185 XMLTestRunner(stream=None) -> XML test runner
00186
00187 The XML file is written to the supplied stream. If stream is None, the
00188 results are stored in a file called TEST-<module>.<class>.xml in the
00189 current working directory (if not overridden with the path property),
00190 where <module> and <class> are the module and class name of the test class.
00191
00192 """
00193
00194 def __init__(self, stream=None):
00195 self._stream = stream
00196 self._path = "."
00197
00198 def run(self, test):
00199 """Run the given test case or test suite."""
00200 class_ = test.__class__
00201 classname = class_.__module__ + "." + class_.__name__
00202 if self._stream == None:
00203 filename = "TEST-%s.xml" % classname
00204 stream = file(os.path.join(self._path, filename), "w")
00205 stream.write('<?xml version="1.0" encoding="utf-8"?>\n')
00206 else:
00207 stream = self._stream
00208
00209 result = _XMLTestResult(classname)
00210 start_time = time.time()
00211
00212
00213 old_stdout = sys.stdout
00214 old_stderr = sys.stderr
00215 sys.stdout = StringIO()
00216 sys.stderr = StringIO()
00217
00218 try:
00219 test(result)
00220 try:
00221 out_s = sys.stdout.getvalue()
00222 except AttributeError:
00223 out_s = ""
00224 try:
00225 err_s = sys.stderr.getvalue()
00226 except AttributeError:
00227 err_s = ""
00228 finally:
00229 sys.stdout = old_stdout
00230 sys.stderr = old_stderr
00231
00232 time_taken = time.time() - start_time
00233 result.print_report(stream, time_taken, out_s, err_s)
00234 if self._stream == None:
00235 stream.close()
00236
00237 return result
00238
00239 def _set_path(self, path):
00240 self._path = path
00241
00242 path = property(lambda self: self._path, _set_path, None,
00243 """The path where the XML files are stored.
00244
00245 This property is ignored when the XML file is written to a file
00246 stream.""")
00247
00248
00249 class XMLTestRunnerTest(unittest.TestCase):
00250 def setUp(self):
00251 self._stream = StringIO()
00252
00253 def _try_test_run(self, test_class, expected):
00254
00255 """Run the test suite against the supplied test class and compare the
00256 XML result against the expected XML string. Fail if the expected
00257 string doesn't match the actual string. All time attribute in the
00258 expected string should have the value "0.000". All error and failure
00259 messages are reduced to "Foobar".
00260
00261 """
00262
00263 runner = XMLTestRunner(self._stream)
00264 runner.run(unittest.makeSuite(test_class))
00265
00266 got = self._stream.getvalue()
00267
00268
00269 got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got)
00270
00271
00272 got = re.sub(r'(?s)<failure (.*?)>.*?</failure>', r'<failure \1>Foobar</failure>', got)
00273 got = re.sub(r'(?s)<error (.*?)>.*?</error>', r'<error \1>Foobar</error>', got)
00274
00275 self.assertEqual(expected, got)
00276
00277 def test_no_tests(self):
00278 """Regression test: Check whether a test run without any tests
00279 matches a previous run.
00280
00281 """
00282 class TestTest(unittest.TestCase):
00283 pass
00284 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="0" time="0.000">
00285 <system-out><![CDATA[]]></system-out>
00286 <system-err><![CDATA[]]></system-err>
00287 </testsuite>
00288 """)
00289
00290 def test_success(self):
00291 """Regression test: Check whether a test run with a successful test
00292 matches a previous run.
00293
00294 """
00295 class TestTest(unittest.TestCase):
00296 def test_foo(self):
00297 pass
00298 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
00299 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
00300 <system-out><![CDATA[]]></system-out>
00301 <system-err><![CDATA[]]></system-err>
00302 </testsuite>
00303 """)
00304
00305 def test_failure(self):
00306 """Regression test: Check whether a test run with a failing test
00307 matches a previous run.
00308
00309 """
00310 class TestTest(unittest.TestCase):
00311 def test_foo(self):
00312 self.assert_(False)
00313 self._try_test_run(TestTest, """<testsuite errors="0" failures="1" name="unittest.TestSuite" tests="1" time="0.000">
00314 <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
00315 <failure type="exceptions.AssertionError">Foobar</failure>
00316 </testcase>
00317 <system-out><![CDATA[]]></system-out>
00318 <system-err><![CDATA[]]></system-err>
00319 </testsuite>
00320 """)
00321
00322 def test_error(self):
00323 """Regression test: Check whether a test run with a erroneous test
00324 matches a previous run.
00325
00326 """
00327 class TestTest(unittest.TestCase):
00328 def test_foo(self):
00329 raise IndexError()
00330 self._try_test_run(TestTest, """<testsuite errors="1" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
00331 <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
00332 <error type="exceptions.IndexError">Foobar</error>
00333 </testcase>
00334 <system-out><![CDATA[]]></system-out>
00335 <system-err><![CDATA[]]></system-err>
00336 </testsuite>
00337 """)
00338
00339 def test_stdout_capture(self):
00340 """Regression test: Check whether a test run with output to stdout
00341 matches a previous run.
00342
00343 """
00344 class TestTest(unittest.TestCase):
00345 def test_foo(self):
00346 print "Test"
00347 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
00348 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
00349 <system-out><![CDATA[Test
00350 ]]></system-out>
00351 <system-err><![CDATA[]]></system-err>
00352 </testsuite>
00353 """)
00354
00355 def test_stderr_capture(self):
00356 """Regression test: Check whether a test run with output to stderr
00357 matches a previous run.
00358
00359 """
00360 class TestTest(unittest.TestCase):
00361 def test_foo(self):
00362 print >>sys.stderr, "Test"
00363 self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
00364 <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
00365 <system-out><![CDATA[]]></system-out>
00366 <system-err><![CDATA[Test
00367 ]]></system-err>
00368 </testsuite>
00369 """)
00370
00371 class NullStream(object):
00372 """A file-like object that discards everything written to it."""
00373 def write(self, buffer):
00374 pass
00375
00376 def test_unittests_changing_stdout(self):
00377 """Check whether the XMLTestRunner recovers gracefully from unit tests
00378 that change stdout, but don't change it back properly.
00379
00380 """
00381 class TestTest(unittest.TestCase):
00382 def test_foo(self):
00383 sys.stdout = XMLTestRunnerTest.NullStream()
00384
00385 runner = XMLTestRunner(self._stream)
00386 runner.run(unittest.makeSuite(TestTest))
00387
00388 def test_unittests_changing_stderr(self):
00389 """Check whether the XMLTestRunner recovers gracefully from unit tests
00390 that change stderr, but don't change it back properly.
00391
00392 """
00393 class TestTest(unittest.TestCase):
00394 def test_foo(self):
00395 sys.stderr = XMLTestRunnerTest.NullStream()
00396
00397 runner = XMLTestRunner(self._stream)
00398 runner.run(unittest.makeSuite(TestTest))
00399
00400
00401 class XMLTestProgram(unittest.TestProgram):
00402 def runTests(self):
00403 if self.testRunner is None:
00404 self.testRunner = XMLTestRunner()
00405 unittest.TestProgram.runTests(self)
00406
00407 main = XMLTestProgram
00408
00409
00410 if __name__ == "__main__":
00411 main(module=None)