junitparser 3.2.0__tar.gz → 4.0.1__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.1/MANIFEST.in +1 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/PKG-INFO +25 -13
- {junitparser-3.2.0 → junitparser-4.0.1}/README.rst +14 -6
- junitparser-4.0.1/junitparser/__init__.py +38 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser/cli.py +25 -33
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser/junitparser.py +102 -77
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser/xunit2.py +123 -96
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser.egg-info/PKG-INFO +25 -13
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser.egg-info/SOURCES.txt +9 -4
- junitparser-4.0.1/pyproject.toml +38 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/setup.cfg +0 -3
- junitparser-4.0.1/tests/data/jenkins.xml +23 -0
- junitparser-4.0.1/tests/data/no_fails.xml +16 -0
- junitparser-4.0.1/tests/data/no_suites_tag.xml +15 -0
- junitparser-4.0.1/tests/data/normal.xml +18 -0
- junitparser-4.0.1/tests/data/pytest_error.xml +8 -0
- junitparser-4.0.1/tests/data/pytest_success.xml +6 -0
- junitparser-4.0.1/tests/test_cli.py +107 -0
- junitparser-4.0.1/tests/test_fromfile.py +203 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/tests/test_general.py +42 -13
- junitparser-4.0.1/tests/test_write.py +284 -0
- junitparser-4.0.1/tests/test_xunit2.py +240 -0
- junitparser-3.2.0/junitparser/__init__.py +0 -17
- junitparser-3.2.0/junitparser.egg-info/not-zip-safe +0 -1
- junitparser-3.2.0/pyproject.toml +0 -3
- junitparser-3.2.0/setup.py +0 -36
- junitparser-3.2.0/tests/test_cli.py +0 -14
- junitparser-3.2.0/tests/test_fromfile.py +0 -223
- junitparser-3.2.0/tests/test_xunit2.py +0 -114
- {junitparser-3.2.0 → junitparser-4.0.1}/LICENSE +0 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser/__main__.py +0 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser/py.typed +0 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser.egg-info/dependency_links.txt +0 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/junitparser.egg-info/entry_points.txt +0 -0
- {junitparser-3.2.0 → junitparser-4.0.1}/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.1
|
|
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.1"
|
|
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,9 +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
|
+
import io
|
|
11
12
|
import itertools
|
|
12
13
|
from copy import deepcopy
|
|
13
|
-
from typing import List, Union
|
|
14
|
+
from typing import List, Union, Iterator, IO, Optional
|
|
14
15
|
|
|
15
16
|
try:
|
|
16
17
|
from lxml import etree
|
|
@@ -18,12 +19,12 @@ except ImportError:
|
|
|
18
19
|
from xml.etree import ElementTree as etree
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def write_xml(obj,
|
|
22
|
+
def write_xml(obj, file_or_filename: Optional[Union[str, IO]] = None, *, pretty: bool = False):
|
|
22
23
|
tree = etree.ElementTree(obj._elem)
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
if
|
|
26
|
-
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.")
|
|
27
28
|
|
|
28
29
|
if pretty:
|
|
29
30
|
from xml.dom.minidom import parseString
|
|
@@ -31,20 +32,23 @@ def write_xml(obj, filepath=None, pretty=False, to_console=False):
|
|
|
31
32
|
text = etree.tostring(obj._elem)
|
|
32
33
|
xml = parseString(text) # nosec
|
|
33
34
|
content = xml.toprettyxml(encoding="utf-8")
|
|
34
|
-
if
|
|
35
|
-
|
|
36
|
-
else:
|
|
37
|
-
with open(filepath, "wb") as xmlfile:
|
|
35
|
+
if isinstance(file_or_filename, str):
|
|
36
|
+
with open(file_or_filename, mode="wb") as xmlfile:
|
|
38
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)
|
|
39
45
|
else:
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
).decode("utf-8")
|
|
45
|
-
)
|
|
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)
|
|
46
50
|
else:
|
|
47
|
-
tree.write(
|
|
51
|
+
tree.write(file_or_filename, encoding="utf-8", xml_declaration=True)
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class JUnitXmlError(Exception):
|
|
@@ -241,7 +245,13 @@ class Result(Element):
|
|
|
241
245
|
self._elem.text = value
|
|
242
246
|
|
|
243
247
|
|
|
244
|
-
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):
|
|
245
255
|
"""Test result when the case is skipped."""
|
|
246
256
|
|
|
247
257
|
_tag = "skipped"
|
|
@@ -250,7 +260,7 @@ class Skipped(Result):
|
|
|
250
260
|
return super().__eq__(other)
|
|
251
261
|
|
|
252
262
|
|
|
253
|
-
class Failure(
|
|
263
|
+
class Failure(FinalResult):
|
|
254
264
|
"""Test result when the case failed."""
|
|
255
265
|
|
|
256
266
|
_tag = "failure"
|
|
@@ -259,7 +269,7 @@ class Failure(Result):
|
|
|
259
269
|
return super().__eq__(other)
|
|
260
270
|
|
|
261
271
|
|
|
262
|
-
class Error(
|
|
272
|
+
class Error(FinalResult):
|
|
263
273
|
"""Test result when the case has errors during execution."""
|
|
264
274
|
|
|
265
275
|
_tag = "error"
|
|
@@ -268,9 +278,6 @@ class Error(Result):
|
|
|
268
278
|
return super().__eq__(other)
|
|
269
279
|
|
|
270
280
|
|
|
271
|
-
POSSIBLE_RESULTS = {Failure, Error, Skipped}
|
|
272
|
-
|
|
273
|
-
|
|
274
281
|
class System(Element):
|
|
275
282
|
"""Parent class for :class:`SystemOut` and :class:`SystemErr`.
|
|
276
283
|
|
|
@@ -316,6 +323,9 @@ class TestCase(Element):
|
|
|
316
323
|
time = FloatAttr()
|
|
317
324
|
__test__ = False
|
|
318
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
|
+
|
|
319
329
|
def __init__(self, name: str = None, classname: str = None, time: float = None):
|
|
320
330
|
super().__init__(self._tag)
|
|
321
331
|
if name is not None:
|
|
@@ -328,12 +338,10 @@ class TestCase(Element):
|
|
|
328
338
|
def __hash__(self):
|
|
329
339
|
return super().__hash__()
|
|
330
340
|
|
|
331
|
-
def __iter__(self):
|
|
332
|
-
all_types = set.union(POSSIBLE_RESULTS, {SystemOut}, {SystemErr})
|
|
341
|
+
def __iter__(self) -> Iterator[Union[Result, System]]:
|
|
333
342
|
for elem in self._elem.iter():
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
yield entry_type.fromelem(elem)
|
|
343
|
+
if elem.tag in self.ITER_TYPES:
|
|
344
|
+
yield self.ITER_TYPES[elem.tag].fromelem(elem)
|
|
337
345
|
|
|
338
346
|
def __eq__(self, other):
|
|
339
347
|
# TODO: May not work correctly if unreliable hash method is used.
|
|
@@ -344,36 +352,41 @@ class TestCase(Element):
|
|
|
344
352
|
"""Whether this testcase was a success (i.e. if it isn't skipped, failed, or errored)."""
|
|
345
353
|
return not self.result
|
|
346
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
|
+
|
|
347
365
|
@property
|
|
348
366
|
def is_skipped(self):
|
|
349
367
|
"""Whether this testcase was skipped."""
|
|
350
|
-
for r in self.result
|
|
351
|
-
if isinstance(r, Skipped):
|
|
352
|
-
return True
|
|
353
|
-
return False
|
|
368
|
+
return any(isinstance(r, Skipped) for r in self.result)
|
|
354
369
|
|
|
355
370
|
@property
|
|
356
|
-
def result(self):
|
|
371
|
+
def result(self) -> List[FinalResult]:
|
|
357
372
|
"""A list of :class:`Failure`, :class:`Skipped`, or :class:`Error` objects."""
|
|
358
|
-
|
|
359
|
-
for entry in self:
|
|
360
|
-
if isinstance(entry, tuple(POSSIBLE_RESULTS)):
|
|
361
|
-
results.append(entry)
|
|
362
|
-
|
|
363
|
-
return results
|
|
373
|
+
return [entry for entry in self if isinstance(entry, FinalResult)]
|
|
364
374
|
|
|
365
375
|
@result.setter
|
|
366
|
-
def result(self, value: Union[
|
|
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
|
+
|
|
367
382
|
# First remove all existing results
|
|
368
383
|
for entry in self.result:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if isinstance(value, Result):
|
|
384
|
+
self.remove(entry)
|
|
385
|
+
if isinstance(value, FinalResult):
|
|
372
386
|
self.append(value)
|
|
373
|
-
|
|
387
|
+
else:
|
|
374
388
|
for entry in value:
|
|
375
|
-
|
|
376
|
-
self.append(entry)
|
|
389
|
+
self.append(entry)
|
|
377
390
|
|
|
378
391
|
@property
|
|
379
392
|
def system_out(self):
|
|
@@ -454,7 +467,7 @@ class Properties(Element):
|
|
|
454
467
|
def add_property(self, property_: Property):
|
|
455
468
|
self.append(property_)
|
|
456
469
|
|
|
457
|
-
def __iter__(self):
|
|
470
|
+
def __iter__(self) -> Iterator[Property]:
|
|
458
471
|
return super().iterchildren(Property)
|
|
459
472
|
|
|
460
473
|
def __eq__(self, other):
|
|
@@ -495,15 +508,18 @@ class TestSuite(Element):
|
|
|
495
508
|
skipped = IntAttr()
|
|
496
509
|
__test__ = False
|
|
497
510
|
|
|
511
|
+
testcase = TestCase
|
|
512
|
+
|
|
498
513
|
def __init__(self, name=None):
|
|
499
514
|
super().__init__(self._tag)
|
|
500
515
|
self.name = name
|
|
501
516
|
self.filepath = None
|
|
517
|
+
self.root = JUnitXml
|
|
502
518
|
|
|
503
|
-
def __iter__(self):
|
|
519
|
+
def __iter__(self) -> Iterator[TestCase]:
|
|
504
520
|
return itertools.chain(
|
|
505
|
-
super().iterchildren(
|
|
506
|
-
(case for suite in super().iterchildren(
|
|
521
|
+
super().iterchildren(self.testcase),
|
|
522
|
+
(case for suite in super().iterchildren(type(self)) for case in suite),
|
|
507
523
|
)
|
|
508
524
|
|
|
509
525
|
def __len__(self):
|
|
@@ -537,7 +553,7 @@ class TestSuite(Element):
|
|
|
537
553
|
result.update_statistics()
|
|
538
554
|
else:
|
|
539
555
|
# Create a new test result containing two testsuites
|
|
540
|
-
result =
|
|
556
|
+
result = self.root()
|
|
541
557
|
result.add_testsuite(self)
|
|
542
558
|
result.add_testsuite(other)
|
|
543
559
|
return result
|
|
@@ -551,7 +567,7 @@ class TestSuite(Element):
|
|
|
551
567
|
self.update_statistics()
|
|
552
568
|
return self
|
|
553
569
|
|
|
554
|
-
result =
|
|
570
|
+
result = self.root()
|
|
555
571
|
result.filepath = self.filepath
|
|
556
572
|
result.add_testsuite(self)
|
|
557
573
|
result.add_testsuite(other)
|
|
@@ -638,11 +654,10 @@ class TestSuite(Element):
|
|
|
638
654
|
|
|
639
655
|
def testsuites(self):
|
|
640
656
|
"""Iterate through all testsuites."""
|
|
641
|
-
|
|
642
|
-
yield suite
|
|
657
|
+
yield from self.iterchildren(type(self))
|
|
643
658
|
|
|
644
|
-
def write(self,
|
|
645
|
-
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)
|
|
646
661
|
|
|
647
662
|
|
|
648
663
|
class JUnitXml(Element):
|
|
@@ -674,14 +689,14 @@ class JUnitXml(Element):
|
|
|
674
689
|
self.filepath = None
|
|
675
690
|
self.name = name
|
|
676
691
|
|
|
677
|
-
def __iter__(self):
|
|
678
|
-
return super().iterchildren(
|
|
692
|
+
def __iter__(self) -> Iterator[TestSuite]:
|
|
693
|
+
return super().iterchildren(self.testsuite)
|
|
679
694
|
|
|
680
695
|
def __len__(self):
|
|
681
696
|
return len(list(self.__iter__()))
|
|
682
697
|
|
|
683
698
|
def __add__(self, other):
|
|
684
|
-
result =
|
|
699
|
+
result = type(self)()
|
|
685
700
|
for suite in self:
|
|
686
701
|
result.add_testsuite(suite)
|
|
687
702
|
for suite in other:
|
|
@@ -693,7 +708,7 @@ class JUnitXml(Element):
|
|
|
693
708
|
for suite in other:
|
|
694
709
|
self.add_testsuite(suite)
|
|
695
710
|
elif other._elem.tag == "testsuite":
|
|
696
|
-
suite =
|
|
711
|
+
suite = self.testsuite(name=other.name)
|
|
697
712
|
for case in other:
|
|
698
713
|
suite._add_testcase_no_update_stats(case)
|
|
699
714
|
self.add_testsuite(suite)
|
|
@@ -728,39 +743,49 @@ class JUnitXml(Element):
|
|
|
728
743
|
self.time = round(time, 3)
|
|
729
744
|
|
|
730
745
|
@classmethod
|
|
731
|
-
def fromroot(cls, root_elem: Element):
|
|
746
|
+
def fromroot(cls, root_elem: Element) -> "JUnitXml":
|
|
732
747
|
"""Construct JUnit objects from an elementTree root element."""
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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":
|
|
738
754
|
raise JUnitXmlError("Invalid format.")
|
|
739
755
|
instance._elem = root_elem
|
|
740
756
|
return instance
|
|
741
757
|
|
|
742
758
|
@classmethod
|
|
743
|
-
def fromstring(cls, text: str):
|
|
744
|
-
"""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)."""
|
|
745
761
|
root_elem = etree.fromstring(text) # nosec
|
|
746
762
|
return cls.fromroot(root_elem)
|
|
747
763
|
|
|
748
764
|
@classmethod
|
|
749
|
-
def fromfile(cls,
|
|
750
|
-
"""
|
|
751
|
-
|
|
752
|
-
|
|
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)
|
|
753
778
|
else:
|
|
754
|
-
tree = etree.parse(
|
|
779
|
+
tree = etree.parse(file) # nosec
|
|
755
780
|
root_elem = tree.getroot()
|
|
756
781
|
instance = cls.fromroot(root_elem)
|
|
757
|
-
instance.filepath =
|
|
782
|
+
instance.filepath = file if isinstance(file, str) else None
|
|
758
783
|
return instance
|
|
759
784
|
|
|
760
|
-
def write(self,
|
|
785
|
+
def write(self, file_or_filename: Optional[Union[str, IO]] = None, *, pretty: bool = False):
|
|
761
786
|
"""Write the object into a JUnit XML file.
|
|
762
787
|
|
|
763
|
-
If `
|
|
788
|
+
If `file_or_filename` is not specified, it will write to the original filename.
|
|
764
789
|
If `pretty` is True, the result file will be more human friendly.
|
|
765
790
|
"""
|
|
766
|
-
write_xml(self,
|
|
791
|
+
write_xml(self, file_or_filename=file_or_filename, pretty=pretty)
|