junitparser 3.1.2__tar.gz → 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- junitparser-4.0.0/MANIFEST.in +1 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/PKG-INFO +25 -13
- {junitparser-3.1.2 → junitparser-4.0.0}/README.rst +14 -6
- junitparser-4.0.0/junitparser/__init__.py +38 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser/cli.py +25 -33
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser/junitparser.py +105 -76
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser/xunit2.py +125 -96
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser.egg-info/PKG-INFO +25 -13
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser.egg-info/SOURCES.txt +9 -4
- junitparser-4.0.0/pyproject.toml +38 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/setup.cfg +0 -3
- junitparser-4.0.0/tests/data/jenkins.xml +23 -0
- junitparser-4.0.0/tests/data/no_fails.xml +16 -0
- junitparser-4.0.0/tests/data/no_suites_tag.xml +15 -0
- junitparser-4.0.0/tests/data/normal.xml +18 -0
- junitparser-4.0.0/tests/data/pytest_error.xml +8 -0
- junitparser-4.0.0/tests/data/pytest_success.xml +6 -0
- junitparser-4.0.0/tests/test_cli.py +107 -0
- junitparser-4.0.0/tests/test_fromfile.py +203 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/tests/test_general.py +44 -14
- junitparser-4.0.0/tests/test_write.py +284 -0
- junitparser-4.0.0/tests/test_xunit2.py +240 -0
- junitparser-3.1.2/junitparser/__init__.py +0 -17
- junitparser-3.1.2/junitparser.egg-info/not-zip-safe +0 -1
- junitparser-3.1.2/pyproject.toml +0 -3
- junitparser-3.1.2/setup.py +0 -36
- junitparser-3.1.2/tests/test_cli.py +0 -14
- junitparser-3.1.2/tests/test_fromfile.py +0 -223
- junitparser-3.1.2/tests/test_xunit2.py +0 -103
- {junitparser-3.1.2 → junitparser-4.0.0}/LICENSE +0 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser/__main__.py +0 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser/py.typed +0 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser.egg-info/dependency_links.txt +0 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser.egg-info/entry_points.txt +0 -0
- {junitparser-3.1.2 → junitparser-4.0.0}/junitparser.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include tests/data/*.xml
|
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: junitparser
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Manipulates JUnit/xUnit Result XML files
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Author-email: Weiwei Wang <gastlygem@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/weiwei/junitparser
|
|
8
|
+
Project-URL: Issues, https://github.com/weiwei/junitparser/issues
|
|
9
|
+
Project-URL: Documentation, https://junitparser.readthedocs.io/
|
|
10
|
+
Keywords: junit,xunit,xml,parser
|
|
10
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
12
|
Classifier: Intended Audience :: Developers
|
|
12
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
14
|
Classifier: Topic :: Text Processing
|
|
14
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.6
|
|
17
|
+
Description-Content-Type: text/x-rst
|
|
15
18
|
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
16
20
|
|
|
17
21
|
junitparser -- Pythonic JUnit/xUnit Result XML Parser
|
|
18
22
|
======================================================
|
|
19
23
|
|
|
20
|
-
.. image:: https://github.com/weiwei/junitparser/workflows/
|
|
24
|
+
.. image:: https://github.com/weiwei/junitparser/actions/workflows/ci.yml/badge.svg?branch=master
|
|
21
25
|
:target: https://github.com/weiwei/junitparser/actions
|
|
22
26
|
.. image:: https://codecov.io/gh/weiwei/junitparser/branch/master/graph/badge.svg?token=UotlfRXNnK
|
|
23
27
|
:target: https://codecov.io/gh/weiwei/junitparser
|
|
@@ -157,8 +161,7 @@ Junitparser also support extra schemas:
|
|
|
157
161
|
|
|
158
162
|
.. code-block:: python
|
|
159
163
|
|
|
160
|
-
from junitparser.xunit2 import
|
|
161
|
-
RerunFailure
|
|
164
|
+
from junitparser.xunit2 import TestCase, TestSuite, RerunFailure
|
|
162
165
|
# These classes are redefined to support extra properties and attributes
|
|
163
166
|
# of the xunit2 schema.
|
|
164
167
|
suite = TestSuite("mySuite")
|
|
@@ -168,7 +171,7 @@ Junitparser also support extra schemas:
|
|
|
168
171
|
rerun_failure.stack_trace = "Stack"
|
|
169
172
|
rerun_failure.system_err = "E404"
|
|
170
173
|
rerun_failure.system_out = "NOT FOUND"
|
|
171
|
-
case.
|
|
174
|
+
case.add_interim_result(rerun_failure)
|
|
172
175
|
|
|
173
176
|
Currently supported schemas including:
|
|
174
177
|
|
|
@@ -295,9 +298,18 @@ Command Line
|
|
|
295
298
|
Test
|
|
296
299
|
----
|
|
297
300
|
|
|
298
|
-
The tests are written with python ``unittest``, to run them, use
|
|
301
|
+
The tests are written with python ``unittest``, to run them, use
|
|
302
|
+
`pytest <https://pypi.org/project/pytest/>`_::
|
|
299
303
|
|
|
300
|
-
pytest
|
|
304
|
+
pytest
|
|
305
|
+
|
|
306
|
+
If you get a failure like ``unsupported locale setting`` you may need to add
|
|
307
|
+
extra locales that the tests use. Refer to the steps used in the
|
|
308
|
+
`CI build workflow <.github/workflows/build.yml>`_::
|
|
309
|
+
|
|
310
|
+
sudo locale-gen en_US.UTF-8
|
|
311
|
+
sudo locale-gen de_DE.UTF-8
|
|
312
|
+
sudo update-locale
|
|
301
313
|
|
|
302
314
|
Contribute
|
|
303
315
|
----------
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
junitparser -- Pythonic JUnit/xUnit Result XML Parser
|
|
2
2
|
======================================================
|
|
3
3
|
|
|
4
|
-
.. image:: https://github.com/weiwei/junitparser/workflows/
|
|
4
|
+
.. image:: https://github.com/weiwei/junitparser/actions/workflows/ci.yml/badge.svg?branch=master
|
|
5
5
|
:target: https://github.com/weiwei/junitparser/actions
|
|
6
6
|
.. image:: https://codecov.io/gh/weiwei/junitparser/branch/master/graph/badge.svg?token=UotlfRXNnK
|
|
7
7
|
:target: https://codecov.io/gh/weiwei/junitparser
|
|
@@ -141,8 +141,7 @@ Junitparser also support extra schemas:
|
|
|
141
141
|
|
|
142
142
|
.. code-block:: python
|
|
143
143
|
|
|
144
|
-
from junitparser.xunit2 import
|
|
145
|
-
RerunFailure
|
|
144
|
+
from junitparser.xunit2 import TestCase, TestSuite, RerunFailure
|
|
146
145
|
# These classes are redefined to support extra properties and attributes
|
|
147
146
|
# of the xunit2 schema.
|
|
148
147
|
suite = TestSuite("mySuite")
|
|
@@ -152,7 +151,7 @@ Junitparser also support extra schemas:
|
|
|
152
151
|
rerun_failure.stack_trace = "Stack"
|
|
153
152
|
rerun_failure.system_err = "E404"
|
|
154
153
|
rerun_failure.system_out = "NOT FOUND"
|
|
155
|
-
case.
|
|
154
|
+
case.add_interim_result(rerun_failure)
|
|
156
155
|
|
|
157
156
|
Currently supported schemas including:
|
|
158
157
|
|
|
@@ -279,9 +278,18 @@ Command Line
|
|
|
279
278
|
Test
|
|
280
279
|
----
|
|
281
280
|
|
|
282
|
-
The tests are written with python ``unittest``, to run them, use
|
|
281
|
+
The tests are written with python ``unittest``, to run them, use
|
|
282
|
+
`pytest <https://pypi.org/project/pytest/>`_::
|
|
283
283
|
|
|
284
|
-
pytest
|
|
284
|
+
pytest
|
|
285
|
+
|
|
286
|
+
If you get a failure like ``unsupported locale setting`` you may need to add
|
|
287
|
+
extra locales that the tests use. Refer to the steps used in the
|
|
288
|
+
`CI build workflow <.github/workflows/build.yml>`_::
|
|
289
|
+
|
|
290
|
+
sudo locale-gen en_US.UTF-8
|
|
291
|
+
sudo locale-gen de_DE.UTF-8
|
|
292
|
+
sudo update-locale
|
|
285
293
|
|
|
286
294
|
Contribute
|
|
287
295
|
----------
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from .junitparser import (
|
|
2
|
+
Attr,
|
|
3
|
+
Element,
|
|
4
|
+
Error,
|
|
5
|
+
Failure,
|
|
6
|
+
FloatAttr,
|
|
7
|
+
IntAttr,
|
|
8
|
+
JUnitXml,
|
|
9
|
+
JUnitXmlError,
|
|
10
|
+
Properties,
|
|
11
|
+
Property,
|
|
12
|
+
Skipped,
|
|
13
|
+
SystemOut,
|
|
14
|
+
SystemErr,
|
|
15
|
+
TestCase,
|
|
16
|
+
TestSuite,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
version = "4.0.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Attr",
|
|
23
|
+
"Element",
|
|
24
|
+
"Error",
|
|
25
|
+
"Failure",
|
|
26
|
+
"FloatAttr",
|
|
27
|
+
"IntAttr",
|
|
28
|
+
"JUnitXml",
|
|
29
|
+
"JUnitXmlError",
|
|
30
|
+
"Properties",
|
|
31
|
+
"Property",
|
|
32
|
+
"Skipped",
|
|
33
|
+
"SystemOut",
|
|
34
|
+
"SystemErr",
|
|
35
|
+
"TestCase",
|
|
36
|
+
"TestSuite",
|
|
37
|
+
"version"
|
|
38
|
+
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from argparse import ArgumentParser
|
|
2
3
|
from glob import iglob
|
|
3
4
|
from itertools import chain
|
|
@@ -5,8 +6,8 @@ from itertools import chain
|
|
|
5
6
|
from . import JUnitXml, version
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def merge(paths, output, suite_name):
|
|
9
|
-
"""Merge XML
|
|
9
|
+
def merge(paths, output, suite_name=""):
|
|
10
|
+
"""Merge XML reports."""
|
|
10
11
|
result = JUnitXml()
|
|
11
12
|
for path in paths:
|
|
12
13
|
result += JUnitXml.fromfile(path)
|
|
@@ -14,7 +15,7 @@ def merge(paths, output, suite_name):
|
|
|
14
15
|
result.update_statistics()
|
|
15
16
|
if suite_name:
|
|
16
17
|
result.name = suite_name
|
|
17
|
-
result.write(
|
|
18
|
+
result.write(sys.stdout if output == "-" else output)
|
|
18
19
|
return 0
|
|
19
20
|
|
|
20
21
|
|
|
@@ -43,18 +44,23 @@ def _parser(prog_name=None): # pragma: no cover
|
|
|
43
44
|
command_parser = parser.add_subparsers(dest="command", help="command")
|
|
44
45
|
command_parser.required = True
|
|
45
46
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
merge_parser.add_argument(
|
|
47
|
+
# an abstract object that defines common arguments used by multiple commands
|
|
48
|
+
abstract_parser = ArgumentParser(add_help=False)
|
|
49
|
+
abstract_parser.add_argument(
|
|
51
50
|
"--glob",
|
|
52
51
|
help="Treat original XML path(s) as glob(s).",
|
|
53
52
|
dest="paths_are_globs",
|
|
54
53
|
action="store_true",
|
|
55
54
|
default=False,
|
|
56
55
|
)
|
|
57
|
-
|
|
56
|
+
abstract_parser.add_argument("paths", help="Original XML path(s).", nargs="+")
|
|
57
|
+
|
|
58
|
+
# command: merge
|
|
59
|
+
merge_parser = command_parser.add_parser(
|
|
60
|
+
"merge",
|
|
61
|
+
help="Merge JUnit XML format reports with junitparser.",
|
|
62
|
+
parents=[abstract_parser],
|
|
63
|
+
)
|
|
58
64
|
merge_parser.add_argument(
|
|
59
65
|
"output", help='Merged XML Path, setting to "-" will output to the console'
|
|
60
66
|
)
|
|
@@ -64,19 +70,10 @@ def _parser(prog_name=None): # pragma: no cover
|
|
|
64
70
|
)
|
|
65
71
|
|
|
66
72
|
# command: verify
|
|
67
|
-
|
|
73
|
+
verify_parser = command_parser.add_parser( # noqa: F841
|
|
68
74
|
"verify",
|
|
69
75
|
help="Return a non-zero exit code if one of the testcases failed or errored.",
|
|
70
|
-
|
|
71
|
-
merge_parser.add_argument(
|
|
72
|
-
"--glob",
|
|
73
|
-
help="Treat original XML path(s) as glob(s).",
|
|
74
|
-
dest="paths_are_globs",
|
|
75
|
-
action="store_true",
|
|
76
|
-
default=False,
|
|
77
|
-
)
|
|
78
|
-
merge_parser.add_argument(
|
|
79
|
-
"paths", nargs="+", help="XML path(s) of reports to verify."
|
|
76
|
+
parents=[abstract_parser],
|
|
80
77
|
)
|
|
81
78
|
|
|
82
79
|
return parser
|
|
@@ -84,19 +81,14 @@ def _parser(prog_name=None): # pragma: no cover
|
|
|
84
81
|
|
|
85
82
|
def main(args=None, prog_name=None):
|
|
86
83
|
"""CLI's main runner."""
|
|
87
|
-
args =
|
|
84
|
+
args = _parser(prog_name=prog_name).parse_args(args)
|
|
85
|
+
paths = (
|
|
86
|
+
chain.from_iterable(iglob(path) for path in args.paths)
|
|
87
|
+
if args.paths_are_globs
|
|
88
|
+
else args.paths
|
|
89
|
+
)
|
|
88
90
|
if args.command == "merge":
|
|
89
|
-
return merge(
|
|
90
|
-
chain.from_iterable(iglob(path) for path in args.paths)
|
|
91
|
-
if args.paths_are_globs
|
|
92
|
-
else args.paths,
|
|
93
|
-
args.output,
|
|
94
|
-
args.suite_name,
|
|
95
|
-
)
|
|
91
|
+
return merge(paths, args.output, args.suite_name)
|
|
96
92
|
if args.command == "verify":
|
|
97
|
-
return verify(
|
|
98
|
-
chain.from_iterable(iglob(path) for path in args.paths)
|
|
99
|
-
if args.paths_are_globs
|
|
100
|
-
else args.paths
|
|
101
|
-
)
|
|
93
|
+
return verify(paths)
|
|
102
94
|
return 255
|
|
@@ -8,10 +8,10 @@ This, according to the document, is Apache Ant's JUnit output.
|
|
|
8
8
|
|
|
9
9
|
See the documentation for other supported schemas.
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
import io
|
|
12
12
|
import itertools
|
|
13
13
|
from copy import deepcopy
|
|
14
|
-
from typing import List
|
|
14
|
+
from typing import List, Union, Iterator, IO, Optional
|
|
15
15
|
|
|
16
16
|
try:
|
|
17
17
|
from lxml import etree
|
|
@@ -19,12 +19,12 @@ except ImportError:
|
|
|
19
19
|
from xml.etree import ElementTree as etree
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def write_xml(obj,
|
|
22
|
+
def write_xml(obj, file_or_filename: Optional[Union[str, IO]] = None, *, pretty: bool = False):
|
|
23
23
|
tree = etree.ElementTree(obj._elem)
|
|
24
|
-
if
|
|
25
|
-
|
|
26
|
-
if
|
|
27
|
-
raise JUnitXmlError("Missing
|
|
24
|
+
if file_or_filename is None:
|
|
25
|
+
file_or_filename = obj.filepath
|
|
26
|
+
if file_or_filename is None:
|
|
27
|
+
raise JUnitXmlError("Missing file argument.")
|
|
28
28
|
|
|
29
29
|
if pretty:
|
|
30
30
|
from xml.dom.minidom import parseString
|
|
@@ -32,20 +32,23 @@ def write_xml(obj, filepath=None, pretty=False, to_console=False):
|
|
|
32
32
|
text = etree.tostring(obj._elem)
|
|
33
33
|
xml = parseString(text) # nosec
|
|
34
34
|
content = xml.toprettyxml(encoding="utf-8")
|
|
35
|
-
if
|
|
36
|
-
|
|
37
|
-
else:
|
|
38
|
-
with open(filepath, "wb") as xmlfile:
|
|
35
|
+
if isinstance(file_or_filename, str):
|
|
36
|
+
with open(file_or_filename, encoding="utf-8", mode="wb") as xmlfile:
|
|
39
37
|
xmlfile.write(content)
|
|
38
|
+
else:
|
|
39
|
+
if isinstance(file_or_filename, io.TextIOWrapper):
|
|
40
|
+
if file_or_filename.encoding is not None and file_or_filename.encoding.lower() != "utf-8":
|
|
41
|
+
raise ValueError(f"Only utf-8 encoding is supported: {file_or_filename.encoding}")
|
|
42
|
+
file_or_filename.buffer.write(content)
|
|
43
|
+
else:
|
|
44
|
+
file_or_filename.write(content)
|
|
40
45
|
else:
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
).decode("utf-8")
|
|
46
|
-
)
|
|
46
|
+
if isinstance(file_or_filename, io.TextIOWrapper):
|
|
47
|
+
if file_or_filename.encoding is not None and file_or_filename.encoding.lower() != "utf-8":
|
|
48
|
+
raise ValueError(f"Only utf-8 encoding is supported: {file_or_filename.encoding}")
|
|
49
|
+
tree.write(file_or_filename.buffer, encoding="utf-8", xml_declaration=True)
|
|
47
50
|
else:
|
|
48
|
-
tree.write(
|
|
51
|
+
tree.write(file_or_filename, encoding="utf-8", xml_declaration=True)
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
class JUnitXmlError(Exception):
|
|
@@ -242,7 +245,13 @@ class Result(Element):
|
|
|
242
245
|
self._elem.text = value
|
|
243
246
|
|
|
244
247
|
|
|
245
|
-
class
|
|
248
|
+
class FinalResult(Result):
|
|
249
|
+
"""Base class for final test result (in contrast to XUnit2 InterimResult)."""
|
|
250
|
+
|
|
251
|
+
_tag = None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class Skipped(FinalResult):
|
|
246
255
|
"""Test result when the case is skipped."""
|
|
247
256
|
|
|
248
257
|
_tag = "skipped"
|
|
@@ -251,7 +260,7 @@ class Skipped(Result):
|
|
|
251
260
|
return super().__eq__(other)
|
|
252
261
|
|
|
253
262
|
|
|
254
|
-
class Failure(
|
|
263
|
+
class Failure(FinalResult):
|
|
255
264
|
"""Test result when the case failed."""
|
|
256
265
|
|
|
257
266
|
_tag = "failure"
|
|
@@ -260,7 +269,7 @@ class Failure(Result):
|
|
|
260
269
|
return super().__eq__(other)
|
|
261
270
|
|
|
262
271
|
|
|
263
|
-
class Error(
|
|
272
|
+
class Error(FinalResult):
|
|
264
273
|
"""Test result when the case has errors during execution."""
|
|
265
274
|
|
|
266
275
|
_tag = "error"
|
|
@@ -269,9 +278,6 @@ class Error(Result):
|
|
|
269
278
|
return super().__eq__(other)
|
|
270
279
|
|
|
271
280
|
|
|
272
|
-
POSSIBLE_RESULTS = {Failure, Error, Skipped}
|
|
273
|
-
|
|
274
|
-
|
|
275
281
|
class System(Element):
|
|
276
282
|
"""Parent class for :class:`SystemOut` and :class:`SystemErr`.
|
|
277
283
|
|
|
@@ -317,6 +323,9 @@ class TestCase(Element):
|
|
|
317
323
|
time = FloatAttr()
|
|
318
324
|
__test__ = False
|
|
319
325
|
|
|
326
|
+
# JUnit TestCase children are final results, SystemOut and SystemErr
|
|
327
|
+
ITER_TYPES = {t._tag: t for t in (Failure, Error, Skipped, SystemOut, SystemErr)}
|
|
328
|
+
|
|
320
329
|
def __init__(self, name: str = None, classname: str = None, time: float = None):
|
|
321
330
|
super().__init__(self._tag)
|
|
322
331
|
if name is not None:
|
|
@@ -329,12 +338,10 @@ class TestCase(Element):
|
|
|
329
338
|
def __hash__(self):
|
|
330
339
|
return super().__hash__()
|
|
331
340
|
|
|
332
|
-
def __iter__(self):
|
|
333
|
-
all_types = set.union(POSSIBLE_RESULTS, {SystemOut}, {SystemErr})
|
|
341
|
+
def __iter__(self) -> Iterator[Union[Result, System]]:
|
|
334
342
|
for elem in self._elem.iter():
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
yield entry_type.fromelem(elem)
|
|
343
|
+
if elem.tag in self.ITER_TYPES:
|
|
344
|
+
yield self.ITER_TYPES[elem.tag].fromelem(elem)
|
|
338
345
|
|
|
339
346
|
def __eq__(self, other):
|
|
340
347
|
# TODO: May not work correctly if unreliable hash method is used.
|
|
@@ -345,32 +352,40 @@ class TestCase(Element):
|
|
|
345
352
|
"""Whether this testcase was a success (i.e. if it isn't skipped, failed, or errored)."""
|
|
346
353
|
return not self.result
|
|
347
354
|
|
|
355
|
+
@property
|
|
356
|
+
def is_failure(self):
|
|
357
|
+
"""Whether this testcase failed."""
|
|
358
|
+
return any(isinstance(r, Failure) for r in self.result)
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def is_error(self):
|
|
362
|
+
"""Whether this testcase errored."""
|
|
363
|
+
return any(isinstance(r, Error) for r in self.result)
|
|
364
|
+
|
|
348
365
|
@property
|
|
349
366
|
def is_skipped(self):
|
|
350
367
|
"""Whether this testcase was skipped."""
|
|
351
|
-
for r in self.result
|
|
352
|
-
if isinstance(r, Skipped):
|
|
353
|
-
return True
|
|
354
|
-
return False
|
|
368
|
+
return any(isinstance(r, Skipped) for r in self.result)
|
|
355
369
|
|
|
356
370
|
@property
|
|
357
|
-
def result(self):
|
|
371
|
+
def result(self) -> List[FinalResult]:
|
|
358
372
|
"""A list of :class:`Failure`, :class:`Skipped`, or :class:`Error` objects."""
|
|
359
|
-
|
|
360
|
-
for entry in self:
|
|
361
|
-
if isinstance(entry, tuple(POSSIBLE_RESULTS)):
|
|
362
|
-
results.append(entry)
|
|
363
|
-
|
|
364
|
-
return results
|
|
373
|
+
return [entry for entry in self if isinstance(entry, FinalResult)]
|
|
365
374
|
|
|
366
375
|
@result.setter
|
|
367
|
-
def result(self, value:
|
|
376
|
+
def result(self, value: Union[FinalResult, List[FinalResult]]):
|
|
377
|
+
# Check typing
|
|
378
|
+
if not (isinstance(value, FinalResult) or
|
|
379
|
+
isinstance(value, list) and all(isinstance(item, FinalResult) for item in value)):
|
|
380
|
+
raise ValueError("Value must be either FinalResult or list of FinalResult")
|
|
381
|
+
|
|
368
382
|
# First remove all existing results
|
|
369
383
|
for entry in self.result:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
384
|
+
self.remove(entry)
|
|
385
|
+
if isinstance(value, FinalResult):
|
|
386
|
+
self.append(value)
|
|
387
|
+
else:
|
|
388
|
+
for entry in value:
|
|
374
389
|
self.append(entry)
|
|
375
390
|
|
|
376
391
|
@property
|
|
@@ -452,7 +467,7 @@ class Properties(Element):
|
|
|
452
467
|
def add_property(self, property_: Property):
|
|
453
468
|
self.append(property_)
|
|
454
469
|
|
|
455
|
-
def __iter__(self):
|
|
470
|
+
def __iter__(self) -> Iterator[Property]:
|
|
456
471
|
return super().iterchildren(Property)
|
|
457
472
|
|
|
458
473
|
def __eq__(self, other):
|
|
@@ -493,15 +508,18 @@ class TestSuite(Element):
|
|
|
493
508
|
skipped = IntAttr()
|
|
494
509
|
__test__ = False
|
|
495
510
|
|
|
511
|
+
testcase = TestCase
|
|
512
|
+
|
|
496
513
|
def __init__(self, name=None):
|
|
497
514
|
super().__init__(self._tag)
|
|
498
515
|
self.name = name
|
|
499
516
|
self.filepath = None
|
|
517
|
+
self.root = JUnitXml
|
|
500
518
|
|
|
501
|
-
def __iter__(self):
|
|
519
|
+
def __iter__(self) -> Iterator[TestCase]:
|
|
502
520
|
return itertools.chain(
|
|
503
|
-
super().iterchildren(
|
|
504
|
-
(case for suite in super().iterchildren(
|
|
521
|
+
super().iterchildren(self.testcase),
|
|
522
|
+
(case for suite in super().iterchildren(type(self)) for case in suite),
|
|
505
523
|
)
|
|
506
524
|
|
|
507
525
|
def __len__(self):
|
|
@@ -535,7 +553,7 @@ class TestSuite(Element):
|
|
|
535
553
|
result.update_statistics()
|
|
536
554
|
else:
|
|
537
555
|
# Create a new test result containing two testsuites
|
|
538
|
-
result =
|
|
556
|
+
result = self.root()
|
|
539
557
|
result.add_testsuite(self)
|
|
540
558
|
result.add_testsuite(other)
|
|
541
559
|
return result
|
|
@@ -549,7 +567,7 @@ class TestSuite(Element):
|
|
|
549
567
|
self.update_statistics()
|
|
550
568
|
return self
|
|
551
569
|
|
|
552
|
-
result =
|
|
570
|
+
result = self.root()
|
|
553
571
|
result.filepath = self.filepath
|
|
554
572
|
result.add_testsuite(self)
|
|
555
573
|
result.add_testsuite(other)
|
|
@@ -636,11 +654,10 @@ class TestSuite(Element):
|
|
|
636
654
|
|
|
637
655
|
def testsuites(self):
|
|
638
656
|
"""Iterate through all testsuites."""
|
|
639
|
-
|
|
640
|
-
yield suite
|
|
657
|
+
yield from self.iterchildren(type(self))
|
|
641
658
|
|
|
642
|
-
def write(self,
|
|
643
|
-
write_xml(self,
|
|
659
|
+
def write(self, file_or_filename: Optional[Union[str, IO]] = None, *, pretty: bool = False):
|
|
660
|
+
write_xml(self, file_or_filename=file_or_filename, pretty=pretty)
|
|
644
661
|
|
|
645
662
|
|
|
646
663
|
class JUnitXml(Element):
|
|
@@ -665,19 +682,21 @@ class JUnitXml(Element):
|
|
|
665
682
|
errors = IntAttr()
|
|
666
683
|
skipped = IntAttr()
|
|
667
684
|
|
|
685
|
+
testsuite = TestSuite
|
|
686
|
+
|
|
668
687
|
def __init__(self, name=None):
|
|
669
688
|
super().__init__(self._tag)
|
|
670
689
|
self.filepath = None
|
|
671
690
|
self.name = name
|
|
672
691
|
|
|
673
|
-
def __iter__(self):
|
|
674
|
-
return super().iterchildren(
|
|
692
|
+
def __iter__(self) -> Iterator[TestSuite]:
|
|
693
|
+
return super().iterchildren(self.testsuite)
|
|
675
694
|
|
|
676
695
|
def __len__(self):
|
|
677
696
|
return len(list(self.__iter__()))
|
|
678
697
|
|
|
679
698
|
def __add__(self, other):
|
|
680
|
-
result =
|
|
699
|
+
result = type(self)()
|
|
681
700
|
for suite in self:
|
|
682
701
|
result.add_testsuite(suite)
|
|
683
702
|
for suite in other:
|
|
@@ -689,7 +708,7 @@ class JUnitXml(Element):
|
|
|
689
708
|
for suite in other:
|
|
690
709
|
self.add_testsuite(suite)
|
|
691
710
|
elif other._elem.tag == "testsuite":
|
|
692
|
-
suite =
|
|
711
|
+
suite = self.testsuite(name=other.name)
|
|
693
712
|
for case in other:
|
|
694
713
|
suite._add_testcase_no_update_stats(case)
|
|
695
714
|
self.add_testsuite(suite)
|
|
@@ -724,39 +743,49 @@ class JUnitXml(Element):
|
|
|
724
743
|
self.time = round(time, 3)
|
|
725
744
|
|
|
726
745
|
@classmethod
|
|
727
|
-
def fromroot(cls, root_elem: Element):
|
|
746
|
+
def fromroot(cls, root_elem: Element) -> "JUnitXml":
|
|
728
747
|
"""Construct JUnit objects from an elementTree root element."""
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
748
|
+
instance = cls()
|
|
749
|
+
if root_elem.tag == "testsuite":
|
|
750
|
+
testsuite_element = root_elem
|
|
751
|
+
root_elem = testsuite_element.makeelement("testsuites", {})
|
|
752
|
+
root_elem.append(testsuite_element)
|
|
753
|
+
if not root_elem.tag == "testsuites":
|
|
734
754
|
raise JUnitXmlError("Invalid format.")
|
|
735
755
|
instance._elem = root_elem
|
|
736
756
|
return instance
|
|
737
757
|
|
|
738
758
|
@classmethod
|
|
739
|
-
def fromstring(cls, text: str):
|
|
740
|
-
"""Construct JUnit objects from an XML string."""
|
|
759
|
+
def fromstring(cls, text: Union[str, bytes]) -> "JUnitXml":
|
|
760
|
+
"""Construct JUnit objects from an XML string (str or bytes)."""
|
|
741
761
|
root_elem = etree.fromstring(text) # nosec
|
|
742
762
|
return cls.fromroot(root_elem)
|
|
743
763
|
|
|
744
764
|
@classmethod
|
|
745
|
-
def fromfile(cls,
|
|
746
|
-
"""
|
|
747
|
-
|
|
748
|
-
|
|
765
|
+
def fromfile(cls, file: Union[str, IO], parse_func=None) -> "JUnitXml":
|
|
766
|
+
"""
|
|
767
|
+
Construct JUnit objects from an XML file.
|
|
768
|
+
|
|
769
|
+
The ``file`` can be any of the following:
|
|
770
|
+
|
|
771
|
+
- a file name/path
|
|
772
|
+
- a file object
|
|
773
|
+
- a file-like object
|
|
774
|
+
- a URL using the HTTP or FTP protocol (with lxml only)
|
|
775
|
+
"""
|
|
776
|
+
if parse_func is not None:
|
|
777
|
+
tree = parse_func(file)
|
|
749
778
|
else:
|
|
750
|
-
tree = etree.parse(
|
|
779
|
+
tree = etree.parse(file) # nosec
|
|
751
780
|
root_elem = tree.getroot()
|
|
752
781
|
instance = cls.fromroot(root_elem)
|
|
753
|
-
instance.filepath =
|
|
782
|
+
instance.filepath = file if isinstance(file, str) else None
|
|
754
783
|
return instance
|
|
755
784
|
|
|
756
|
-
def write(self,
|
|
785
|
+
def write(self, file_or_filename: Optional[Union[str, IO]] = None, *, pretty: bool = False):
|
|
757
786
|
"""Write the object into a JUnit XML file.
|
|
758
787
|
|
|
759
|
-
If `
|
|
788
|
+
If `file_or_filename` is not specified, it will write to the original filename.
|
|
760
789
|
If `pretty` is True, the result file will be more human friendly.
|
|
761
790
|
"""
|
|
762
|
-
write_xml(self,
|
|
791
|
+
write_xml(self, file_or_filename=file_or_filename, pretty=pretty)
|