File: xunit.py

package info (click to toggle)
python-pbcommand 2.1.1%2Bgit20231020.28d1635-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,016 kB
  • sloc: python: 7,676; makefile: 220; sh: 73
file content (436 lines) | stat: -rw-r--r-- 14,384 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
"""
IO layer for converting a TestResult instance to XUnit and an XUnit.xml
parser
"""

import copy
import datetime
import logging
import os
import re
import unittest
from xml.dom import minidom
from xml.etree.ElementTree import Element, ElementTree, ParseError

log = logging.getLogger(__name__)

_RESULT_STATES = 'success skipped error failure'.split()


class XunitTestSuite:

    def __init__(self, name, tests, run_time=None, pb_requirements=()):
        """

        :param name: (str) Name of test suite
        :param tests: (list) of XunitTestCase instances
        """
        self.name = name
        for t in tests:
            if not isinstance(t, XunitTestCase):
                raise TypeError(
                    "Expected {k} got {t}.".format(
                        t=type(t),
                        k=XunitTestCase.__class__.__name__))
        self._tests = tests
        self._run_time = run_time
        self._pb_requirements = pb_requirements

    @staticmethod
    def from_xml(file_name):
        if os.path.exists(file_name):
            file_name = os.path.abspath(file_name)
            suite = _parser(file_name)
            return suite
        else:
            raise IOError("Unable to find {x}".format(x=file_name))

    @property
    def tests(self):
        return self._tests

    def _get_tests_by_result(self, result):
        if result in _RESULT_STATES:
            return [test for test in self.tests if test.result == result]
        else:
            raise ValueError(
                "{r} is Invalid state. Supported states {s}.".format(
                    r=result, s=_RESULT_STATES))

    def __len__(self):
        return len(self.tests)

    def __repr__(self):
        return "<{c} tests:{n} successful:{s} failed:{f} errors:{e} skipped:{sk} >".format(
            c=self.__class__.__name__, n=self.ntests, s=self.nsuccess, f=self.nfailure, e=self.nerrors, sk=self.nskipped)

    def __str__(self):
        outs = list()
        outs.append("TestName : {n}".format(n=self.name))
        outs.append(
            "{c} tests {n} successful {s} failed {f} errors {e} skipped {sk}".format(
                c=self.__class__.__name__,
                n=self.ntests,
                s=self.nsuccess,
                f=self.nfailure,
                e=self.nerrors,
                sk=self.nskipped))
        for i, t in enumerate(self.tests):
            outs.append(" : ".join([str(i + 1).rjust(5), str(t)]))
        outs.append("")
        outs.append("End of Summary.")
        return "\n".join(outs)

    @property
    def ntests(self):
        return len(self.tests)

    @property
    def errors(self):
        return self._get_tests_by_result('error')

    @property
    def nerrors(self):
        return len(self.errors)

    @property
    def skipped(self):
        return self._get_tests_by_result('skipped')

    @property
    def nskipped(self):
        return len(self.skipped)

    @property
    def success(self):
        return self._get_tests_by_result('success')

    @property
    def nsuccess(self):
        return len(self.success)

    @property
    def failure(self):
        return self._get_tests_by_result('failure')

    @property
    def nfailure(self):
        return len(self.failure)

    @property
    def requirements(self):
        return self._pb_requirements

    def to_xml(self):
        """Return an XML instance of the suite"""
        doc = minidom.Document()
        x = doc.createElement("testsuite")
        x.setAttribute("name", self.name)
        x.setAttribute("tests", str(self.ntests))
        x.setAttribute("errors", str(self.nerrors))
        x.setAttribute("failures", str(self.nfailure))
        x.setAttribute("skip", str(self.nskipped))
        doc.appendChild(x)

        for test_case in self.tests:
            classname = test_case.classname
            # sanitize for XML
            text = "" if test_case.text is None else test_case.text
            etype = "" if test_case.etype is None else test_case.etype

            tc = doc.createElement("testcase")
            tc.setAttribute("classname", classname)
            tc.setAttribute("name", test_case.name)
            tc.setAttribute("result", test_case.result)
            tc.setAttribute("etype", etype)
            tc.setAttribute("text", text)
            x.appendChild(tc)
            child_tag_name = None
            if test_case.result in 'failures':
                child_tag_name = "error"
            elif test_case.result in {"failure", "skipped", "error"}:
                child_tag_name = test_case.result
            else:
                # Successful testcase
                pass
            if child_tag_name is not None:
                ct = doc.createElement(child_tag_name)
                ct.setAttribute("type", test_case.etype)
                ct.setAttribute("message", test_case.message)
                tc.appendChild(ct)
        if len(self._pb_requirements):
            props = doc.createElement("properties")
            x.appendChild(props)
            for req in self._pb_requirements:
                prop = doc.createElement("property")
                prop.setAttribute("name", "Requirement")
                prop.setAttribute("value", req)
                props.appendChild(prop)
        return x

    def to_dict(self):
        was_successful = self.ntests == self.nsuccess
        d = dict(nfailures=self.nfailure, nsuccess=self.nsuccess,
                 nskipped=self.nskipped, ntests=self.ntests,
                 nerrors=self.nerrors, was_successful=was_successful,
                 tests=[t.to_dict() for t in self.tests])
        return d


class XunitTestCase:
    RESULTS = 'success skipped error failure'.split()

    def __init__(self, classname, name, result, etype=None, text=None,
                 message=None, run_time=None):
        self.classname = classname
        self.name = name
        if result in XunitTestCase.RESULTS:
            self.result = result
        else:
            raise ValueError(
                "{r} is Invalid state. Supported states {s}.".format(
                    r=result, s=_RESULT_STATES))
        self.text = text
        self.etype = etype
        self.message = message
        self.run_time = run_time

    def __str__(self):
        outs = list()
        x = [self.result.rjust(10), self.name]
        outs.append(" : ".join(x))
        if self.result != 'success':
            outs.append("\n\n {x}\n".format(x=self.message))
        return " ".join(outs)

    def __repr__(self):
        return "<{k} {n} {r}>".format(
            k=self.classname, n=self.name, r=self.result)

    def to_dict(self):
        was_successful = self.result == 'success'
        d = dict(classname=self.classname, name=self.name,
                 text=self.text, etype=self.etype,
                 result=self.result,
                 message=self.message, run_time=self.run_time,
                 was_successful=was_successful)
        return d


def to_timedelta(val):
    if val is None:
        return None
    return datetime.timedelta(seconds=float(val))


def _parser(file_name):
    """Parse the nose xml file and return a XunitTestSuite"""
    xml = ElementTree(file=file_name)
    root = xml.getroot()

    if root.tag == 'testsuite':
        suite_name = root.attrib['name']
        # suite_run_time = root.attrib['time']
        suite_run_time = None
        tests = []

        # iterator over every test case
        for el in root.findall('testcase'):
            classname = el.attrib['classname']
            name = el.attrib['name']
            result = 'success'
            text = None
            message = None
            etype = None
            rtime = None

            for e in el:
                if e.tag in ('failure', 'skipped', 'error'):
                    result = e.tag
                    text = e.text
                    message = e.attrib['message']
                    etype = e.attrib['type']

            # t = (classname, name, result, text, message, etype)
            t = XunitTestCase(classname, name, result, text=text,
                              message=message, etype=etype)
            tests.append(t)

        # Properties linking to JIRA issues
        pb_requirements = []
        for el in root.findall("properties"):
            for p in el.findall("property"):
                pb_requirements.append(p.attrib['value'])

        xunit_test_suite = XunitTestSuite(suite_name, tests,
                                          run_time=suite_run_time,
                                          pb_requirements=pb_requirements)

    else:
        msg = "Unable to find tag 'testsuite' in {f}".format(f=file_name)
        log.error(msg)
        raise ValueError(msg)

    # log.debug(xunit_test_suite)
    return xunit_test_suite


def convert_suite_and_result_to_xunit(suite,
                                      result,
                                      name="PysivXunitTestSuite",
                                      requirements=()):
    """Custom a test suite and result to XML.

    The name is used to set the xml suitename for jenkins.

    <testsuite errors="1" failures="1" name="1234" skip="1" tests="5">

    :param suite: unittest.TestSuite
    :param result: unittest.TestResult
    :param name:

    :return: XML instance
    """

    # When a test fails in setUpClass, the result is a
    # unittest.suite._ErrorHolder rather than a TestCase. We need to handle
    # those differently.
    def parse_setupclass_error(klass_id):
        """Return what's inside the parentheses."""
        return re.search(r"(?<=\().*(?=\))", klass_id).group(0)

    # Test cls names/id
    names = 'errors skipped failures'.split()
    klass_results = {}
    for n in names:
        klass_results[n] = []
        for klass, out in getattr(result, n):
            if isinstance(klass, unittest.suite._ErrorHolder):
                klass_results[n].append(parse_setupclass_error(klass.id()))
            else:
                klass_results[n].append(klass.id())

    nskipped = len(klass_results['skipped'])
    nerrors = len(klass_results['errors'])
    nfailures = len(klass_results['failures'])

    def _to_key(test_case):
        # If the test_case is an _ErrorHolder, the key should be parsed from
        # the description. It won't have a _testMethodName
        if isinstance(test_case, unittest.suite._ErrorHolder):
            return parse_setupclass_error(test_case.description)
        m = test_case.__module__
        n = test_case.__class__.__name__
        mn = test_case._testMethodName
        # d = test_case._testMethodDoc
        # return m, n, mn, d
        return ".".join([m, n, mn])

    requirements = set(requirements)
    all_test_cases = {}
    for s in suite:
        if isinstance(s, unittest.suite.TestSuite):
            for tc in s:
                key = _to_key(tc)
                all_test_cases[key] = None
                m = getattr(tc, tc._testMethodName)
                requirements.update(getattr(m, "__pb_requirements__", []))
        else:
            raise TypeError(
                "Unsupported test suite case ({x})".format(
                    x=type(s)))

    ntests = len(all_test_cases)

    # loop over failures, errors, skipped, assign message
    for n in names:
        # get all testcases with state
        ts = getattr(result, n)
        for t, msg in ts:
            k = _to_key(t)
            all_test_cases[k] = msg

    # import ipdb; ipdb.set_trace()

    # Create XML
    doc = minidom.Document()
    x = doc.createElement("testsuite")
    x.setAttribute("name", name)
    x.setAttribute("tests", str(ntests))
    x.setAttribute("errors", str(nerrors))
    x.setAttribute("failures", str(nfailures))
    x.setAttribute("skip", str(nskipped))
    doc.appendChild(x)

    for idx, message in all_test_cases.items():
        test_method = idx.split('.')[-1]
        tc = doc.createElement("testcase")
        tc.setAttribute("classname", idx)
        tc.setAttribute("name", test_method)
        tc.setAttribute("time", "1.000")
        x.appendChild(tc)
        child_tag_name = error_type = None
        if idx in klass_results['errors']:
            child_tag_name, error_type = "error", "exceptions.Exception"
        elif idx in klass_results['failures']:
            child_tag_name, error_type = "failure", "exceptions.Exception"
        elif idx in klass_results['skipped']:
            child_tag_name, error_type = "skipped", "unittest.case.SkipTest"
        else:
            # print "Success", idx
            pass
        if child_tag_name is not None:
            ct = doc.createElement(child_tag_name)
            ct.setAttribute("type", error_type)
            ct.setAttribute("message", message)
            tc.appendChild(ct)
    props = doc.createElement("properties")
    x.appendChild(props)
    for req in requirements:
        prop = doc.createElement("property")
        prop.setAttribute("name", "Requirement")
        prop.setAttribute("value", req)
        props.appendChild(prop)
    return x


def xunit_file_to_jenkins(xunit_file, job_name):
    """
    To better support jenkins, the standard Xunit file needs to be modified in
    two fundamental ways.

    1. The testsuite.name is change to the job name
    2. The job name/id is appended to every testcase.

    :param xunit_file:
    :param job_name:
    :return: xml instance
    """
    xsuite = XunitTestSuite.from_xml(xunit_file)

    tests = []
    for test_case in xsuite.tests:
        t = copy.copy(test_case)
        t.name = "_".join([test_case.name, job_name])
        t.classname = "_".join([test_case.classname, job_name])
        tests.append(t)

    jenkins_suite = XunitTestSuite(job_name, tests,
                                   pb_requirements=xsuite.requirements)
    return jenkins_suite.to_xml()


def merge_junit_files(output_file, input_files):
    root_out = Element("testsuites")
    xml_out = ElementTree(root_out)
    for input_file in input_files:
        xml_in = ElementTree(file=input_file)
        root = xml_in.getroot()
        if root.tag == 'testsuite':
            root_out.append(root)
        else:
            assert root.tag == "testsuites"
            for suite in root.findall("testsuite"):
                root_out.append(suite)
    with open(output_file, "wb") as x:
        xml_out.write(x)