Home | History | Annotate | Download | only in Python
      1 """
      2 XML Test Runner for PyUnit
      3 """
      4 
      5 # Written by Sebastian Rittau <srittau (at] jroger.in-berlin.de> and placed in
      6 # the Public Domain. With contributions by Paolo Borelli.
      7 
      8 __revision__ = "$Id: /private/python/stdlib/xmlrunner.py 16654 2007-11-12T12:46:35.368945Z srittau  $"
      9 
     10 import os.path
     11 import re
     12 import sys
     13 import time
     14 import traceback
     15 import unittest
     16 from StringIO import StringIO
     17 from xml.sax.saxutils import escape
     18 
     19 from StringIO import StringIO
     20 
     21 
     22 class _TestInfo(object):
     23 
     24     """Information about a particular test.
     25     
     26     Used by _XMLTestResult.
     27     
     28     """
     29 
     30     def __init__(self, test, time):
     31         (self._class, self._method) = test.id().rsplit(".", 1)
     32         self._time = time
     33         self._error = None
     34         self._failure = None
     35 
     36     @staticmethod
     37     def create_success(test, time):
     38         """Create a _TestInfo instance for a successful test."""
     39         return _TestInfo(test, time)
     40 
     41     @staticmethod
     42     def create_failure(test, time, failure):
     43         """Create a _TestInfo instance for a failed test."""
     44         info = _TestInfo(test, time)
     45         info._failure = failure
     46         return info
     47 
     48     @staticmethod
     49     def create_error(test, time, error):
     50         """Create a _TestInfo instance for an erroneous test."""
     51         info = _TestInfo(test, time)
     52         info._error = error
     53         return info
     54 
     55     def print_report(self, stream):
     56         """Print information about this test case in XML format to the
     57         supplied stream.
     58 
     59         """
     60         stream.write('  <testcase classname="%(class)s" name="%(method)s" time="%(time).4f">' % \
     61             {
     62                 "class": self._class,
     63                 "method": self._method,
     64                 "time": self._time,
     65             })
     66         if self._failure != None:
     67             self._print_error(stream, 'failure', self._failure)
     68         if self._error != None:
     69             self._print_error(stream, 'error', self._error)
     70         stream.write('</testcase>\n')
     71 
     72     def _print_error(self, stream, tagname, error):
     73         """Print information from a failure or error to the supplied stream."""
     74         text = escape(str(error[1]))
     75         stream.write('\n')
     76         stream.write('    <%s type="%s">%s\n' \
     77             % (tagname, str(error[0]), text))
     78         tb_stream = StringIO()
     79         traceback.print_tb(error[2], None, tb_stream)
     80         stream.write(escape(tb_stream.getvalue()))
     81         stream.write('    </%s>\n' % tagname)
     82         stream.write('  ')
     83 
     84 
     85 class _XMLTestResult(unittest.TestResult):
     86 
     87     """A test result class that stores result as XML.
     88 
     89     Used by XMLTestRunner.
     90 
     91     """
     92 
     93     def __init__(self, classname):
     94         unittest.TestResult.__init__(self)
     95         self._test_name = classname
     96         self._start_time = None
     97         self._tests = []
     98         self._error = None
     99         self._failure = None
    100 
    101     def startTest(self, test):
    102         unittest.TestResult.startTest(self, test)
    103         self._error = None
    104         self._failure = None
    105         self._start_time = time.time()
    106 
    107     def stopTest(self, test):
    108         time_taken = time.time() - self._start_time
    109         unittest.TestResult.stopTest(self, test)
    110         if self._error:
    111             info = _TestInfo.create_error(test, time_taken, self._error)
    112         elif self._failure:
    113             info = _TestInfo.create_failure(test, time_taken, self._failure)
    114         else:
    115             info = _TestInfo.create_success(test, time_taken)
    116         self._tests.append(info)
    117 
    118     def addError(self, test, err):
    119         unittest.TestResult.addError(self, test, err)
    120         self._error = err
    121 
    122     def addFailure(self, test, err):
    123         unittest.TestResult.addFailure(self, test, err)
    124         self._failure = err
    125 
    126     def print_report(self, stream, time_taken, out, err):
    127         """Prints the XML report to the supplied stream.
    128         
    129         The time the tests took to perform as well as the captured standard
    130         output and standard error streams must be passed in.a
    131 
    132         """
    133         stream.write('<testsuite errors="%(e)d" failures="%(f)d" ' % \
    134             { "e": len(self.errors), "f": len(self.failures) })
    135         stream.write('name="%(n)s" tests="%(t)d" time="%(time).3f">\n' % \
    136             {
    137                 "n": self._test_name,
    138                 "t": self.testsRun,
    139                 "time": time_taken,
    140             })
    141         for info in self._tests:
    142             info.print_report(stream)
    143         stream.write('  <system-out><![CDATA[%s]]></system-out>\n' % out)
    144         stream.write('  <system-err><![CDATA[%s]]></system-err>\n' % err)
    145         stream.write('</testsuite>\n')
    146 
    147 
    148 class XMLTestRunner(object):
    149 
    150     """A test runner that stores results in XML format compatible with JUnit.
    151 
    152     XMLTestRunner(stream=None) -> XML test runner
    153 
    154     The XML file is written to the supplied stream. If stream is None, the
    155     results are stored in a file called TEST-<module>.<class>.xml in the
    156     current working directory (if not overridden with the path property),
    157     where <module> and <class> are the module and class name of the test class.
    158 
    159     """
    160 
    161     def __init__(self, stream=None):
    162         self._stream = stream
    163         self._path = "."
    164 
    165     def run(self, test):
    166         """Run the given test case or test suite."""
    167         class_ = test.__class__
    168         classname = class_.__module__ + "." + class_.__name__
    169         if self._stream == None:
    170             filename = "TEST-%s.xml" % classname
    171             stream = file(os.path.join(self._path, filename), "w")
    172             stream.write('<?xml version="1.0" encoding="utf-8"?>\n')
    173         else:
    174             stream = self._stream
    175 
    176         result = _XMLTestResult(classname)
    177         start_time = time.time()
    178 
    179         # TODO: Python 2.5: Use the with statement
    180         old_stdout = sys.stdout
    181         old_stderr = sys.stderr
    182         sys.stdout = StringIO()
    183         sys.stderr = StringIO()
    184 
    185         try:
    186             test(result)
    187             try:
    188                 out_s = sys.stdout.getvalue()
    189             except AttributeError:
    190                 out_s = ""
    191             try:
    192                 err_s = sys.stderr.getvalue()
    193             except AttributeError:
    194                 err_s = ""
    195         finally:
    196             sys.stdout = old_stdout
    197             sys.stderr = old_stderr
    198 
    199         time_taken = time.time() - start_time
    200         result.print_report(stream, time_taken, out_s, err_s)
    201         if self._stream == 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 XMLTestRunnerTest(unittest.TestCase):
    217     def setUp(self):
    218         self._stream = StringIO()
    219 
    220     def _try_test_run(self, test_class, expected):
    221 
    222         """Run the test suite against the supplied test class and compare the
    223         XML result against the expected XML string. Fail if the expected
    224         string doesn't match the actual string. All time attribute in the
    225         expected string should have the value "0.000". All error and failure
    226         messages are reduced to "Foobar".
    227 
    228         """
    229 
    230         runner = XMLTestRunner(self._stream)
    231         runner.run(unittest.makeSuite(test_class))
    232 
    233         got = self._stream.getvalue()
    234         # Replace all time="X.YYY" attributes by time="0.000" to enable a
    235         # simple string comparison.
    236         got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got)
    237         # Likewise, replace all failure and error messages by a simple "Foobar"
    238         # string.
    239         got = re.sub(r'(?s)<failure (.*?)>.*?</failure>', r'<failure \1>Foobar</failure>', got)
    240         got = re.sub(r'(?s)<error (.*?)>.*?</error>', r'<error \1>Foobar</error>', got)
    241 
    242         self.assertEqual(expected, got)
    243 
    244     def test_no_tests(self):
    245         """Regression test: Check whether a test run without any tests
    246         matches a previous run.
    247         
    248         """
    249         class TestTest(unittest.TestCase):
    250             pass
    251         self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="0" time="0.000">
    252   <system-out><![CDATA[]]></system-out>
    253   <system-err><![CDATA[]]></system-err>
    254 </testsuite>
    255 """)
    256 
    257     def test_success(self):
    258         """Regression test: Check whether a test run with a successful test
    259         matches a previous run.
    260         
    261         """
    262         class TestTest(unittest.TestCase):
    263             def test_foo(self):
    264                 pass
    265         self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
    266   <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
    267   <system-out><![CDATA[]]></system-out>
    268   <system-err><![CDATA[]]></system-err>
    269 </testsuite>
    270 """)
    271 
    272     def test_failure(self):
    273         """Regression test: Check whether a test run with a failing test
    274         matches a previous run.
    275         
    276         """
    277         class TestTest(unittest.TestCase):
    278             def test_foo(self):
    279                 self.assert_(False)
    280         self._try_test_run(TestTest, """<testsuite errors="0" failures="1" name="unittest.TestSuite" tests="1" time="0.000">
    281   <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
    282     <failure type="exceptions.AssertionError">Foobar</failure>
    283   </testcase>
    284   <system-out><![CDATA[]]></system-out>
    285   <system-err><![CDATA[]]></system-err>
    286 </testsuite>
    287 """)
    288 
    289     def test_error(self):
    290         """Regression test: Check whether a test run with a erroneous test
    291         matches a previous run.
    292         
    293         """
    294         class TestTest(unittest.TestCase):
    295             def test_foo(self):
    296                 raise IndexError()
    297         self._try_test_run(TestTest, """<testsuite errors="1" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
    298   <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
    299     <error type="exceptions.IndexError">Foobar</error>
    300   </testcase>
    301   <system-out><![CDATA[]]></system-out>
    302   <system-err><![CDATA[]]></system-err>
    303 </testsuite>
    304 """)
    305 
    306     def test_stdout_capture(self):
    307         """Regression test: Check whether a test run with output to stdout
    308         matches a previous run.
    309         
    310         """
    311         class TestTest(unittest.TestCase):
    312             def test_foo(self):
    313                 print "Test"
    314         self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
    315   <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
    316   <system-out><![CDATA[Test
    317 ]]></system-out>
    318   <system-err><![CDATA[]]></system-err>
    319 </testsuite>
    320 """)
    321 
    322     def test_stderr_capture(self):
    323         """Regression test: Check whether a test run with output to stderr
    324         matches a previous run.
    325         
    326         """
    327         class TestTest(unittest.TestCase):
    328             def test_foo(self):
    329                 print >>sys.stderr, "Test"
    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[]]></system-out>
    333   <system-err><![CDATA[Test
    334 ]]></system-err>
    335 </testsuite>
    336 """)
    337 
    338     class NullStream(object):
    339         """A file-like object that discards everything written to it."""
    340         def write(self, buffer):
    341             pass
    342 
    343     def test_unittests_changing_stdout(self):
    344         """Check whether the XMLTestRunner recovers gracefully from unit tests
    345         that change stdout, but don't change it back properly.
    346 
    347         """
    348         class TestTest(unittest.TestCase):
    349             def test_foo(self):
    350                 sys.stdout = XMLTestRunnerTest.NullStream()
    351 
    352         runner = XMLTestRunner(self._stream)
    353         runner.run(unittest.makeSuite(TestTest))
    354 
    355     def test_unittests_changing_stderr(self):
    356         """Check whether the XMLTestRunner recovers gracefully from unit tests
    357         that change stderr, but don't change it back properly.
    358 
    359         """
    360         class TestTest(unittest.TestCase):
    361             def test_foo(self):
    362                 sys.stderr = XMLTestRunnerTest.NullStream()
    363 
    364         runner = XMLTestRunner(self._stream)
    365         runner.run(unittest.makeSuite(TestTest))
    366 
    367 
    368 class XMLTestProgram(unittest.TestProgram):
    369     def runTests(self):
    370         if self.testRunner is None:
    371             self.testRunner = XMLTestRunner()
    372         unittest.TestProgram.runTests(self)
    373 
    374 main = XMLTestProgram
    375 
    376 
    377 if __name__ == "__main__":
    378     main(module=None)
    379