1dfafb49cSJed Brown#!/usr/bin/env python 2*82c7dee4SJed Brown# -*- coding: utf-8 -*- 3dfafb49cSJed Brownfrom collections import defaultdict 4dfafb49cSJed Brownimport sys 5dfafb49cSJed Brownimport re 6dfafb49cSJed Brownimport xml.etree.ElementTree as ET 7dfafb49cSJed Brownimport xml.dom.minidom 8dfafb49cSJed Brown 9dfafb49cSJed Browntry: 10dfafb49cSJed Brown # Python 2 11dfafb49cSJed Brown unichr 12*82c7dee4SJed Brown PY2 = True 13dfafb49cSJed Brownexcept NameError: # pragma: nocover 14dfafb49cSJed Brown # Python 3 15dfafb49cSJed Brown unichr = chr 16*82c7dee4SJed Brown PY2 = False 17dfafb49cSJed Brown 18dfafb49cSJed Brown""" 19dfafb49cSJed BrownBased on the understanding of what Jenkins can parse for JUnit XML files. 20dfafb49cSJed Brown 21dfafb49cSJed Brown<?xml version="1.0" encoding="utf-8"?> 22dfafb49cSJed Brown<testsuites errors="1" failures="1" tests="4" time="45"> 23dfafb49cSJed Brown <testsuite errors="1" failures="1" hostname="localhost" id="0" name="test1" 24dfafb49cSJed Brown package="testdb" tests="4" timestamp="2012-11-15T01:02:29"> 25dfafb49cSJed Brown <properties> 26dfafb49cSJed Brown <property name="assert-passed" value="1"/> 27dfafb49cSJed Brown </properties> 28dfafb49cSJed Brown <testcase classname="testdb.directory" name="1-passed-test" time="10"/> 29dfafb49cSJed Brown <testcase classname="testdb.directory" name="2-failed-test" time="20"> 30dfafb49cSJed Brown <failure message="Assertion FAILED: failed assert" type="failure"> 31dfafb49cSJed Brown the output of the testcase 32dfafb49cSJed Brown </failure> 33dfafb49cSJed Brown </testcase> 34dfafb49cSJed Brown <testcase classname="package.directory" name="3-errord-test" time="15"> 35dfafb49cSJed Brown <error message="Assertion ERROR: error assert" type="error"> 36dfafb49cSJed Brown the output of the testcase 37dfafb49cSJed Brown </error> 38dfafb49cSJed Brown </testcase> 39dfafb49cSJed Brown <testcase classname="package.directory" name="3-skipped-test" time="0"> 40dfafb49cSJed Brown <skipped message="SKIPPED Test" type="skipped"> 41dfafb49cSJed Brown the output of the testcase 42dfafb49cSJed Brown </skipped> 43dfafb49cSJed Brown </testcase> 44dfafb49cSJed Brown <testcase classname="testdb.directory" name="3-passed-test" time="10"> 45dfafb49cSJed Brown <system-out> 46dfafb49cSJed Brown I am system output 47dfafb49cSJed Brown </system-out> 48dfafb49cSJed Brown <system-err> 49dfafb49cSJed Brown I am the error output 50dfafb49cSJed Brown </system-err> 51dfafb49cSJed Brown </testcase> 52dfafb49cSJed Brown </testsuite> 53dfafb49cSJed Brown</testsuites> 54dfafb49cSJed Brown""" 55dfafb49cSJed Brown 56dfafb49cSJed Brown 57dfafb49cSJed Browndef decode(var, encoding): 58dfafb49cSJed Brown """ 59dfafb49cSJed Brown If not already unicode, decode it. 60dfafb49cSJed Brown """ 61dfafb49cSJed Brown if PY2: 62dfafb49cSJed Brown if isinstance(var, unicode): 63dfafb49cSJed Brown ret = var 64dfafb49cSJed Brown elif isinstance(var, str): 65dfafb49cSJed Brown if encoding: 66dfafb49cSJed Brown ret = var.decode(encoding) 67dfafb49cSJed Brown else: 68dfafb49cSJed Brown ret = unicode(var) 69dfafb49cSJed Brown else: 70dfafb49cSJed Brown ret = unicode(var) 71dfafb49cSJed Brown else: 72dfafb49cSJed Brown ret = str(var) 73dfafb49cSJed Brown return ret 74dfafb49cSJed Brown 75dfafb49cSJed Brown 76dfafb49cSJed Brownclass TestSuite(object): 77dfafb49cSJed Brown """ 78dfafb49cSJed Brown Suite of test cases. 79dfafb49cSJed Brown Can handle unicode strings or binary strings if their encoding is provided. 80dfafb49cSJed Brown """ 81dfafb49cSJed Brown 82dfafb49cSJed Brown def __init__(self, name, test_cases=None, hostname=None, id=None, 83dfafb49cSJed Brown package=None, timestamp=None, properties=None, file=None, 84dfafb49cSJed Brown log=None, url=None, stdout=None, stderr=None): 85dfafb49cSJed Brown self.name = name 86dfafb49cSJed Brown if not test_cases: 87dfafb49cSJed Brown test_cases = [] 88dfafb49cSJed Brown try: 89dfafb49cSJed Brown iter(test_cases) 90dfafb49cSJed Brown except TypeError: 91dfafb49cSJed Brown raise Exception('test_cases must be a list of test cases') 92dfafb49cSJed Brown self.test_cases = test_cases 93dfafb49cSJed Brown self.timestamp = timestamp 94dfafb49cSJed Brown self.hostname = hostname 95dfafb49cSJed Brown self.id = id 96dfafb49cSJed Brown self.package = package 97dfafb49cSJed Brown self.file = file 98dfafb49cSJed Brown self.log = log 99dfafb49cSJed Brown self.url = url 100dfafb49cSJed Brown self.stdout = stdout 101dfafb49cSJed Brown self.stderr = stderr 102dfafb49cSJed Brown self.properties = properties 103dfafb49cSJed Brown 104dfafb49cSJed Brown def build_xml_doc(self, encoding=None): 105dfafb49cSJed Brown """ 106dfafb49cSJed Brown Builds the XML document for the JUnit test suite. 107dfafb49cSJed Brown Produces clean unicode strings and decodes non-unicode with the help of encoding. 108dfafb49cSJed Brown @param encoding: Used to decode encoded strings. 109dfafb49cSJed Brown @return: XML document with unicode string elements 110dfafb49cSJed Brown """ 111dfafb49cSJed Brown 112dfafb49cSJed Brown # build the test suite element 113dfafb49cSJed Brown test_suite_attributes = dict() 114dfafb49cSJed Brown test_suite_attributes['name'] = decode(self.name, encoding) 115dfafb49cSJed Brown if any(c.assertions for c in self.test_cases): 116dfafb49cSJed Brown test_suite_attributes['assertions'] = \ 117dfafb49cSJed Brown str(sum([int(c.assertions) for c in self.test_cases if c.assertions])) 118dfafb49cSJed Brown test_suite_attributes['disabled'] = \ 119dfafb49cSJed Brown str(len([c for c in self.test_cases if not c.is_enabled])) 120dfafb49cSJed Brown test_suite_attributes['failures'] = \ 121dfafb49cSJed Brown str(len([c for c in self.test_cases if c.is_failure()])) 122dfafb49cSJed Brown test_suite_attributes['errors'] = \ 123dfafb49cSJed Brown str(len([c for c in self.test_cases if c.is_error()])) 124dfafb49cSJed Brown test_suite_attributes['skipped'] = \ 125dfafb49cSJed Brown str(len([c for c in self.test_cases if c.is_skipped()])) 126dfafb49cSJed Brown test_suite_attributes['time'] = \ 127dfafb49cSJed Brown str(sum(c.elapsed_sec for c in self.test_cases if c.elapsed_sec)) 128dfafb49cSJed Brown test_suite_attributes['tests'] = str(len(self.test_cases)) 129dfafb49cSJed Brown 130dfafb49cSJed Brown if self.hostname: 131dfafb49cSJed Brown test_suite_attributes['hostname'] = decode(self.hostname, encoding) 132dfafb49cSJed Brown if self.id: 133dfafb49cSJed Brown test_suite_attributes['id'] = decode(self.id, encoding) 134dfafb49cSJed Brown if self.package: 135dfafb49cSJed Brown test_suite_attributes['package'] = decode(self.package, encoding) 136dfafb49cSJed Brown if self.timestamp: 137dfafb49cSJed Brown test_suite_attributes['timestamp'] = decode(self.timestamp, encoding) 138dfafb49cSJed Brown if self.file: 139dfafb49cSJed Brown test_suite_attributes['file'] = decode(self.file, encoding) 140dfafb49cSJed Brown if self.log: 141dfafb49cSJed Brown test_suite_attributes['log'] = decode(self.log, encoding) 142dfafb49cSJed Brown if self.url: 143dfafb49cSJed Brown test_suite_attributes['url'] = decode(self.url, encoding) 144dfafb49cSJed Brown 145dfafb49cSJed Brown xml_element = ET.Element("testsuite", test_suite_attributes) 146dfafb49cSJed Brown 147dfafb49cSJed Brown # add any properties 148dfafb49cSJed Brown if self.properties: 149dfafb49cSJed Brown props_element = ET.SubElement(xml_element, "properties") 150dfafb49cSJed Brown for k, v in self.properties.items(): 151dfafb49cSJed Brown attrs = {'name': decode(k, encoding), 'value': decode(v, encoding)} 152dfafb49cSJed Brown ET.SubElement(props_element, "property", attrs) 153dfafb49cSJed Brown 154dfafb49cSJed Brown # add test suite stdout 155dfafb49cSJed Brown if self.stdout: 156dfafb49cSJed Brown stdout_element = ET.SubElement(xml_element, "system-out") 157dfafb49cSJed Brown stdout_element.text = decode(self.stdout, encoding) 158dfafb49cSJed Brown 159dfafb49cSJed Brown # add test suite stderr 160dfafb49cSJed Brown if self.stderr: 161dfafb49cSJed Brown stderr_element = ET.SubElement(xml_element, "system-err") 162dfafb49cSJed Brown stderr_element.text = decode(self.stderr, encoding) 163dfafb49cSJed Brown 164dfafb49cSJed Brown # test cases 165dfafb49cSJed Brown for case in self.test_cases: 166dfafb49cSJed Brown test_case_attributes = dict() 167dfafb49cSJed Brown test_case_attributes['name'] = decode(case.name, encoding) 168dfafb49cSJed Brown if case.assertions: 169dfafb49cSJed Brown # Number of assertions in the test case 170dfafb49cSJed Brown test_case_attributes['assertions'] = "%d" % case.assertions 171dfafb49cSJed Brown if case.elapsed_sec: 172dfafb49cSJed Brown test_case_attributes['time'] = "%f" % case.elapsed_sec 173dfafb49cSJed Brown if case.timestamp: 174dfafb49cSJed Brown test_case_attributes['timestamp'] = decode(case.timestamp, encoding) 175dfafb49cSJed Brown if case.classname: 176dfafb49cSJed Brown test_case_attributes['classname'] = decode(case.classname, encoding) 177dfafb49cSJed Brown if case.status: 178dfafb49cSJed Brown test_case_attributes['status'] = decode(case.status, encoding) 179dfafb49cSJed Brown if case.category: 180dfafb49cSJed Brown test_case_attributes['class'] = decode(case.category, encoding) 181dfafb49cSJed Brown if case.file: 182dfafb49cSJed Brown test_case_attributes['file'] = decode(case.file, encoding) 183dfafb49cSJed Brown if case.line: 184dfafb49cSJed Brown test_case_attributes['line'] = decode(case.line, encoding) 185dfafb49cSJed Brown if case.log: 186dfafb49cSJed Brown test_case_attributes['log'] = decode(case.log, encoding) 187dfafb49cSJed Brown if case.url: 188dfafb49cSJed Brown test_case_attributes['url'] = decode(case.url, encoding) 189dfafb49cSJed Brown 190dfafb49cSJed Brown test_case_element = ET.SubElement( 191dfafb49cSJed Brown xml_element, "testcase", test_case_attributes) 192dfafb49cSJed Brown 193dfafb49cSJed Brown # failures 194dfafb49cSJed Brown if case.is_failure(): 195dfafb49cSJed Brown attrs = {'type': 'failure'} 196dfafb49cSJed Brown if case.failure_message: 197dfafb49cSJed Brown attrs['message'] = decode(case.failure_message, encoding) 198dfafb49cSJed Brown if case.failure_type: 199dfafb49cSJed Brown attrs['type'] = decode(case.failure_type, encoding) 200dfafb49cSJed Brown failure_element = ET.Element("failure", attrs) 201dfafb49cSJed Brown if case.failure_output: 202dfafb49cSJed Brown failure_element.text = decode(case.failure_output, encoding) 203dfafb49cSJed Brown test_case_element.append(failure_element) 204dfafb49cSJed Brown 205dfafb49cSJed Brown # errors 206dfafb49cSJed Brown if case.is_error(): 207dfafb49cSJed Brown attrs = {'type': 'error'} 208dfafb49cSJed Brown if case.error_message: 209dfafb49cSJed Brown attrs['message'] = decode(case.error_message, encoding) 210dfafb49cSJed Brown if case.error_type: 211dfafb49cSJed Brown attrs['type'] = decode(case.error_type, encoding) 212dfafb49cSJed Brown error_element = ET.Element("error", attrs) 213dfafb49cSJed Brown if case.error_output: 214dfafb49cSJed Brown error_element.text = decode(case.error_output, encoding) 215dfafb49cSJed Brown test_case_element.append(error_element) 216dfafb49cSJed Brown 217dfafb49cSJed Brown # skippeds 218dfafb49cSJed Brown if case.is_skipped(): 219dfafb49cSJed Brown attrs = {'type': 'skipped'} 220dfafb49cSJed Brown if case.skipped_message: 221dfafb49cSJed Brown attrs['message'] = decode(case.skipped_message, encoding) 222dfafb49cSJed Brown skipped_element = ET.Element("skipped", attrs) 223dfafb49cSJed Brown if case.skipped_output: 224dfafb49cSJed Brown skipped_element.text = decode(case.skipped_output, encoding) 225dfafb49cSJed Brown test_case_element.append(skipped_element) 226dfafb49cSJed Brown 227dfafb49cSJed Brown # test stdout 228dfafb49cSJed Brown if case.stdout: 229dfafb49cSJed Brown stdout_element = ET.Element("system-out") 230dfafb49cSJed Brown stdout_element.text = decode(case.stdout, encoding) 231dfafb49cSJed Brown test_case_element.append(stdout_element) 232dfafb49cSJed Brown 233dfafb49cSJed Brown # test stderr 234dfafb49cSJed Brown if case.stderr: 235dfafb49cSJed Brown stderr_element = ET.Element("system-err") 236dfafb49cSJed Brown stderr_element.text = decode(case.stderr, encoding) 237dfafb49cSJed Brown test_case_element.append(stderr_element) 238dfafb49cSJed Brown 239dfafb49cSJed Brown return xml_element 240dfafb49cSJed Brown 241dfafb49cSJed Brown @staticmethod 242dfafb49cSJed Brown def to_xml_string(test_suites, prettyprint=True, encoding=None): 243dfafb49cSJed Brown """ 244dfafb49cSJed Brown Returns the string representation of the JUnit XML document. 245dfafb49cSJed Brown @param encoding: The encoding of the input. 246dfafb49cSJed Brown @return: unicode string 247dfafb49cSJed Brown """ 248dfafb49cSJed Brown 249dfafb49cSJed Brown try: 250dfafb49cSJed Brown iter(test_suites) 251dfafb49cSJed Brown except TypeError: 252dfafb49cSJed Brown raise Exception('test_suites must be a list of test suites') 253dfafb49cSJed Brown 254dfafb49cSJed Brown xml_element = ET.Element("testsuites") 255dfafb49cSJed Brown attributes = defaultdict(int) 256dfafb49cSJed Brown for ts in test_suites: 257dfafb49cSJed Brown ts_xml = ts.build_xml_doc(encoding=encoding) 258dfafb49cSJed Brown for key in ['failures', 'errors', 'tests', 'disabled']: 259dfafb49cSJed Brown attributes[key] += int(ts_xml.get(key, 0)) 260dfafb49cSJed Brown for key in ['time']: 261dfafb49cSJed Brown attributes[key] += float(ts_xml.get(key, 0)) 262dfafb49cSJed Brown xml_element.append(ts_xml) 263*82c7dee4SJed Brown for key, value in attributes.items(): 264dfafb49cSJed Brown xml_element.set(key, str(value)) 265dfafb49cSJed Brown 266dfafb49cSJed Brown xml_string = ET.tostring(xml_element, encoding=encoding) 267dfafb49cSJed Brown # is encoded now 268dfafb49cSJed Brown xml_string = TestSuite._clean_illegal_xml_chars( 269dfafb49cSJed Brown xml_string.decode(encoding or 'utf-8')) 270dfafb49cSJed Brown # is unicode now 271dfafb49cSJed Brown 272dfafb49cSJed Brown if prettyprint: 273dfafb49cSJed Brown # minidom.parseString() works just on correctly encoded binary strings 274dfafb49cSJed Brown xml_string = xml_string.encode(encoding or 'utf-8') 275dfafb49cSJed Brown xml_string = xml.dom.minidom.parseString(xml_string) 276dfafb49cSJed Brown # toprettyxml() produces unicode if no encoding is being passed or binary string with an encoding 277dfafb49cSJed Brown xml_string = xml_string.toprettyxml(encoding=encoding) 278dfafb49cSJed Brown if encoding: 279dfafb49cSJed Brown xml_string = xml_string.decode(encoding) 280dfafb49cSJed Brown # is unicode now 281dfafb49cSJed Brown return xml_string 282dfafb49cSJed Brown 283dfafb49cSJed Brown @staticmethod 284dfafb49cSJed Brown def to_file(file_descriptor, test_suites, prettyprint=True, encoding=None): 285dfafb49cSJed Brown """ 286dfafb49cSJed Brown Writes the JUnit XML document to a file. 287dfafb49cSJed Brown """ 288dfafb49cSJed Brown xml_string = TestSuite.to_xml_string( 289dfafb49cSJed Brown test_suites, prettyprint=prettyprint, encoding=encoding) 290dfafb49cSJed Brown # has problems with encoded str with non-ASCII (non-default-encoding) characters! 291dfafb49cSJed Brown file_descriptor.write(xml_string) 292dfafb49cSJed Brown 293dfafb49cSJed Brown @staticmethod 294dfafb49cSJed Brown def _clean_illegal_xml_chars(string_to_clean): 295dfafb49cSJed Brown """ 296dfafb49cSJed Brown Removes any illegal unicode characters from the given XML string. 297dfafb49cSJed Brown 298dfafb49cSJed Brown @see: http://stackoverflow.com/questions/1707890/fast-way-to-filter-illegal-xml-unicode-chars-in-python 299dfafb49cSJed Brown """ 300dfafb49cSJed Brown 301dfafb49cSJed Brown illegal_unichrs = [ 302dfafb49cSJed Brown (0x00, 0x08), (0x0B, 0x1F), (0x7F, 0x84), (0x86, 0x9F), 303dfafb49cSJed Brown (0xD800, 0xDFFF), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF), 304dfafb49cSJed Brown (0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF), 305dfafb49cSJed Brown (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF), 306dfafb49cSJed Brown (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF), 307dfafb49cSJed Brown (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF), 308dfafb49cSJed Brown (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF), 309dfafb49cSJed Brown (0x10FFFE, 0x10FFFF)] 310dfafb49cSJed Brown 311dfafb49cSJed Brown illegal_ranges = ["%s-%s" % (unichr(low), unichr(high)) 312dfafb49cSJed Brown for (low, high) in illegal_unichrs 313dfafb49cSJed Brown if low < sys.maxunicode] 314dfafb49cSJed Brown 315*82c7dee4SJed Brown illegal_xml_re = re.compile(u'[%s]' % u''.join(illegal_ranges)) 316dfafb49cSJed Brown return illegal_xml_re.sub('', string_to_clean) 317dfafb49cSJed Brown 318dfafb49cSJed Brown 319dfafb49cSJed Brownclass TestCase(object): 320dfafb49cSJed Brown """A JUnit test case with a result and possibly some stdout or stderr""" 321dfafb49cSJed Brown 322dfafb49cSJed Brown def __init__(self, name, classname=None, elapsed_sec=None, stdout=None, 323dfafb49cSJed Brown stderr=None, assertions=None, timestamp=None, status=None, 324dfafb49cSJed Brown category=None, file=None, line=None, log=None, group=None, 325dfafb49cSJed Brown url=None): 326dfafb49cSJed Brown self.name = name 327dfafb49cSJed Brown self.assertions = assertions 328dfafb49cSJed Brown self.elapsed_sec = elapsed_sec 329dfafb49cSJed Brown self.timestamp = timestamp 330dfafb49cSJed Brown self.classname = classname 331dfafb49cSJed Brown self.status = status 332dfafb49cSJed Brown self.category = category 333dfafb49cSJed Brown self.file = file 334dfafb49cSJed Brown self.line = line 335dfafb49cSJed Brown self.log = log 336dfafb49cSJed Brown self.url = url 337dfafb49cSJed Brown self.stdout = stdout 338dfafb49cSJed Brown self.stderr = stderr 339dfafb49cSJed Brown 340dfafb49cSJed Brown self.is_enabled = True 341dfafb49cSJed Brown self.error_message = None 342dfafb49cSJed Brown self.error_output = None 343dfafb49cSJed Brown self.error_type = None 344dfafb49cSJed Brown self.failure_message = None 345dfafb49cSJed Brown self.failure_output = None 346dfafb49cSJed Brown self.failure_type = None 347dfafb49cSJed Brown self.skipped_message = None 348dfafb49cSJed Brown self.skipped_output = None 349dfafb49cSJed Brown 350dfafb49cSJed Brown def add_error_info(self, message=None, output=None, error_type=None): 351dfafb49cSJed Brown """Adds an error message, output, or both to the test case""" 352dfafb49cSJed Brown if message: 353dfafb49cSJed Brown self.error_message = message 354dfafb49cSJed Brown if output: 355dfafb49cSJed Brown self.error_output = output 356dfafb49cSJed Brown if error_type: 357dfafb49cSJed Brown self.error_type = error_type 358dfafb49cSJed Brown 359dfafb49cSJed Brown def add_failure_info(self, message=None, output=None, failure_type=None): 360dfafb49cSJed Brown """Adds a failure message, output, or both to the test case""" 361dfafb49cSJed Brown if message: 362dfafb49cSJed Brown self.failure_message = message 363dfafb49cSJed Brown if output: 364dfafb49cSJed Brown self.failure_output = output 365dfafb49cSJed Brown if failure_type: 366dfafb49cSJed Brown self.failure_type = failure_type 367dfafb49cSJed Brown 368dfafb49cSJed Brown def add_skipped_info(self, message=None, output=None): 369dfafb49cSJed Brown """Adds a skipped message, output, or both to the test case""" 370dfafb49cSJed Brown if message: 371dfafb49cSJed Brown self.skipped_message = message 372dfafb49cSJed Brown if output: 373dfafb49cSJed Brown self.skipped_output = output 374dfafb49cSJed Brown 375dfafb49cSJed Brown def is_failure(self): 376dfafb49cSJed Brown """returns true if this test case is a failure""" 377dfafb49cSJed Brown return self.failure_output or self.failure_message 378dfafb49cSJed Brown 379dfafb49cSJed Brown def is_error(self): 380dfafb49cSJed Brown """returns true if this test case is an error""" 381dfafb49cSJed Brown return self.error_output or self.error_message 382dfafb49cSJed Brown 383dfafb49cSJed Brown def is_skipped(self): 384dfafb49cSJed Brown """returns true if this test case has been skipped""" 385dfafb49cSJed Brown return self.skipped_output or self.skipped_message 386