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