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