xref: /libCEED/tests/junit-xml/junit_xml/__init__.py (revision 82c7dee410f96ee6ba674770bd6de676ee78bc82)
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