"""Support for writing JUnit XML test results for the regrtest""" import os import re import sys import time import traceback import unittest from StringIO import StringIO from xml.sax import saxutils # Invalid XML characters (control chars) EVIL_CHARACTERS_RE = re.compile(r"[\000-\010\013\014\016-\037]") class JUnitXMLTestRunner: """A unittest runner that writes results to a JUnit XML file in xml_dir """ def __init__(self, xml_dir): self.xml_dir = xml_dir def run(self, test): result = JUnitXMLTestResult(self.xml_dir) test(result) result.write_xml() return result class JUnitXMLTestResult(unittest.TestResult): """JUnit XML test result writer. The name of the file written to is determined from the full module name of the first test ran """ def __init__(self, xml_dir): unittest.TestResult.__init__(self) self.xml_dir = xml_dir # The module name of the first test ran self.module_name = None # All TestCases self.tests = [] # Start time self.start = None self.old_stdout = sys.stdout self.old_stderr = sys.stderr sys.stdout = self.stdout = Tee(sys.stdout) sys.stderr = self.stderr = Tee(sys.stderr) def startTest(self, test): unittest.TestResult.startTest(self, test) self.ensure_module_name(test) self.error, self.failure = None, None self.start = time.time() def stopTest(self, test): took = time.time() - self.start unittest.TestResult.stopTest(self, test) args = [test, took] if self.error: args.extend(['error', self.error]) elif self.failure: args.extend(['failure', self.failure]) self.tests.append(TestInfo.from_testcase(*args)) def addError(self, test, err): unittest.TestResult.addError(self, test, err) self.error = err def addFailure(self, test, err): unittest.TestResult.addFailure(self, test, err) self.failure = err def ensure_module_name(self, test): """Set self.module_name from test if not already set""" if not self.module_name: self.module_name = '.'.join(test.id().split('.')[:-1]) def write_xml(self): if not self.module_name: # No tests ran, nothing to write return took = time.time() - self.start stdout = self.stdout.getvalue() stderr = self.stderr.getvalue() sys.stdout = self.old_stdout sys.stderr = self.old_stderr ensure_dir(self.xml_dir) filename = os.path.join(self.xml_dir, 'TEST-%s.xml' % self.module_name) stream = open(filename, 'w') write_testsuite_xml(stream, len(self.tests), len(self.errors), len(self.failures), 0, self.module_name, took) for info in self.tests: info.write_xml(stream) write_stdouterr_xml(stream, stdout, stderr) stream.write('') stream.close() class TestInfo(object): """The JUnit XML model.""" def __init__(self, name, took, type=None, exc_info=None): # The name of the test self.name = name # How long it took self.took = took # Type of test: 'error', 'failure' 'skipped', or None for a # success self.type = type if exc_info: self.exc_name = exc_name(exc_info) self.message = exc_message(exc_info) self.traceback = safe_str(''.join( traceback.format_exception(*exc_info))) else: self.exc_name = self.message = self.traceback = '' @classmethod def from_testcase(cls, testcase, took, type=None, exc_info=None): name = testcase.id().split('.')[-1] return cls(name, took, type, exc_info) def write_xml(self, stream): stream.write(' \n') return stream.write('>\n <%s type="%s" message=%s>\n' % (self.type, self.exc_name, saxutils.quoteattr(self.message), escape_cdata(self.traceback), self.type)) stream.write(' \n') class Tee(StringIO): """Writes data to this StringIO and a separate stream""" def __init__(self, stream): StringIO.__init__(self) self.stream = stream def write(self, data): StringIO.write(self, data) self.stream.write(data) def flush(self): StringIO.flush(self) self.stream.flush() def write_testsuite_xml(stream, tests, errors, failures, skipped, name, took): """Write the XML header ()""" stream.write('\n') stream.write('\n' % (skipped, name, took)) def write_stdouterr_xml(stream, stdout, stderr): """Write the stdout/err tags""" if stdout: stream.write(' \n' % escape_cdata(safe_str(stdout))) if stderr: stream.write(' \n' % escape_cdata(safe_str(stderr))) def write_direct_test(junit_xml_dir, name, took, type=None, exc_info=None, stdout=None, stderr=None): """Write XML for a regrtest 'direct' test; a test which was ran on import (which we label as __main__.__import__) """ return write_manual_test(junit_xml_dir, '%s.__main__' % name, '__import__', took, type, exc_info, stdout, stderr) def write_doctest(junit_xml_dir, name, took, type=None, exc_info=None, stdout=None, stderr=None): """Write XML for a regrtest doctest, labeled as __main__.__doc__""" return write_manual_test(junit_xml_dir, '%s.__main__' % name, '__doc__', took, type, exc_info, stdout, stderr) def write_manual_test(junit_xml_dir, module_name, test_name, took, type=None, exc_info=None, stdout=None, stderr=None): """Manually write XML for one test, outside of unittest""" errors = type == 'error' and 1 or 0 failures = type == 'failure' and 1 or 0 skipped = type == 'skipped' and 1 or 0 ensure_dir(junit_xml_dir) stream = open(os.path.join(junit_xml_dir, 'TEST-%s.xml' % module_name), 'w') write_testsuite_xml(stream, 1, errors, failures, skipped, module_name, took) info = TestInfo(test_name, took, type, exc_info) info.write_xml(stream) write_stdouterr_xml(stream, stdout, stderr) stream.write('') stream.close() def ensure_dir(dir): """Ensure dir exists""" if not os.path.exists(dir): os.mkdir(dir) def exc_name(exc_info): """Determine the full name of the exception that caused exc_info""" exc = exc_info[1] name = getattr(exc.__class__, '__module__', '') if name: name += '.' return name + exc.__class__.__name__ def exc_message(exc_info): """Safely return a short message passed through safe_str describing exc_info, being careful of unicode values. """ exc = exc_info[1] if exc is None: return safe_str(exc_info[0]) if isinstance(exc, BaseException) and isinstance(exc.message, unicode): return safe_str(exc.message) try: return safe_str(str(exc)) except UnicodeEncodeError: try: val = unicode(exc) return safe_str(val) except UnicodeDecodeError: return '?' def escape_cdata(cdata): """Escape a string for an XML CDATA section""" return cdata.replace(']]>', ']]>]]>