Skip to content

Commit 83268bf

Browse files
EnricoMiCube707
andcommitted
Add more is_* properties to JUnit and XUnit2 TestCase
- Add is_failure and is_error to JUnit TestCase - Add is_rerun and is_flaky to XUnit2 TestCase - Provide access to XUnit2 interim (rerun & flaky) results Co-authored-by: Jan Wille <mail@janwille.de>
1 parent ce2f1df commit 83268bf

4 files changed

Lines changed: 154 additions & 35 deletions

File tree

junitparser/junitparser.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,9 @@ class TestCase(Element):
321321
time = FloatAttr()
322322
__test__ = False
323323

324+
# JUnit TestCase children are final results, SystemOut and SystemErr
325+
ITER_TYPES = {t._tag: t for t in set.union(FINAL_RESULTS, {SystemOut, SystemErr})}
326+
324327
def __init__(self, name: str = None, classname: str = None, time: float = None):
325328
super().__init__(self._tag)
326329
if name is not None:
@@ -334,11 +337,9 @@ def __hash__(self):
334337
return super().__hash__()
335338

336339
def __iter__(self) -> Iterator[Union[Result, System]]:
337-
all_types = set.union(FINAL_RESULTS, {SystemOut, SystemErr})
338340
for elem in self._elem.iter():
339-
for entry_type in all_types:
340-
if elem.tag == entry_type._tag:
341-
yield entry_type.fromelem(elem)
341+
if elem.tag in self.ITER_TYPES:
342+
yield self.ITER_TYPES[elem.tag].fromelem(elem)
342343

343344
def __eq__(self, other):
344345
# TODO: May not work correctly if unreliable hash method is used.
@@ -349,23 +350,25 @@ def is_passed(self):
349350
"""Whether this testcase was a success (i.e. if it isn't skipped, failed, or errored)."""
350351
return not self.result
351352

353+
@property
354+
def is_failure(self):
355+
"""Whether this testcase failed."""
356+
return any(isinstance(r, Failure) for r in self.result)
357+
358+
@property
359+
def is_error(self):
360+
"""Whether this testcase errored."""
361+
return any(isinstance(r, Error) for r in self.result)
362+
352363
@property
353364
def is_skipped(self):
354365
"""Whether this testcase was skipped."""
355-
for r in self.result:
356-
if isinstance(r, Skipped):
357-
return True
358-
return False
366+
return any(isinstance(r, Skipped) for r in self.result)
359367

360368
@property
361369
def result(self) -> List[FinalResult]:
362370
"""A list of :class:`Failure`, :class:`Skipped`, or :class:`Error` objects."""
363-
results = []
364-
for entry in self:
365-
if isinstance(entry, FinalResult):
366-
results.append(entry)
367-
368-
return results
371+
return [entry for entry in self if isinstance(entry, FinalResult)]
369372

370373
@result.setter
371374
def result(self, value: Union[FinalResult, List[FinalResult]]):

junitparser/xunit2.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,9 @@
1313
"""
1414

1515
import itertools
16-
from typing import List, TypeVar, Iterator
16+
from typing import Iterator, List, Type, TypeVar
1717
from . import junitparser
1818

19-
T = TypeVar("T")
20-
21-
2219
class TestSuite(junitparser.TestSuite):
2320
"""TestSuite for Pytest, with some different attributes."""
2421

@@ -175,31 +172,54 @@ class FlakyError(InterimResult):
175172
_tag = "flakyError"
176173

177174

175+
RERUN_RESULTS = {RerunFailure, RerunError, FlakyFailure, FlakyError}
176+
177+
178+
R = TypeVar("R", bound=InterimResult)
179+
180+
178181
class TestCase(junitparser.TestCase):
179182
group = junitparser.Attr()
180183

181-
def _rerun_results(self, _type: T) -> List[T]:
182-
elems = self.iterchildren(_type)
183-
results = []
184-
for elem in elems:
185-
results.append(_type.fromelem(elem))
186-
return results
184+
# XUnit2 TestCase children are JUnit children and rerun results
185+
ITER_TYPES = {t._tag: t for t in list(junitparser.TestCase.ITER_TYPES.values()) + list(RERUN_RESULTS)}
187186

188-
def rerun_failures(self):
187+
def _interim_results(self, _type: Type[R]) -> List[R]:
188+
return [entry for entry in self if isinstance(entry, _type)]
189+
190+
@property
191+
def interim_result(self) -> List[InterimResult]:
192+
"""
193+
A list of interim results: :class:`RerunFailure`, :class:`RerunError`, :class:`FlakyFailure`, or :class:`FlakyError` objects.
194+
This is complementary to the result property returning final results.
195+
"""
196+
return self._interim_results(InterimResult)
197+
198+
def rerun_failures(self) -> List[RerunFailure]:
189199
"""<rerunFailure>"""
190-
return self._rerun_results(RerunFailure)
200+
return self._interim_results(RerunFailure)
191201

192-
def rerun_errors(self):
202+
def rerun_errors(self) -> List[RerunError]:
193203
"""<rerunError>"""
194-
return self._rerun_results(RerunError)
204+
return self._interim_results(RerunError)
195205

196-
def flaky_failures(self):
206+
def flaky_failures(self) -> List[FlakyFailure]:
197207
"""<flakyFailure>"""
198-
return self._rerun_results(FlakyFailure)
208+
return self._interim_results(FlakyFailure)
199209

200-
def flaky_errors(self):
210+
def flaky_errors(self) -> List[FlakyError]:
201211
"""<flakyError>"""
202-
return self._rerun_results(FlakyError)
212+
return self._interim_results(FlakyError)
213+
214+
@property
215+
def is_rerun(self) -> bool:
216+
"""Whether this testcase is rerun, i.e., there are rerun failures or errors."""
217+
return any(self.rerun_failures()) or any(self.rerun_errors())
218+
219+
@property
220+
def is_flaky(self) -> bool:
221+
"""Whether this testcase is flaky, i.e., there are flaky failures or errors."""
222+
return any(self.flaky_failures()) or any(self.flaky_errors())
203223

204224
def add_interim_result(self, result: InterimResult):
205225
"""Append an interim (rerun or flaky) result to the testcase. A testcase can have multiple interim results."""

tests/test_general.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,18 +685,32 @@ def test_case_is_skipped(self):
685685
case.result = [Skipped()]
686686
assert case.is_skipped
687687
assert not case.is_passed
688+
assert not case.is_failure
689+
assert not case.is_error
688690

689691
def test_case_is_passed(self):
690692
case = TestCase()
691693
case.result = []
692694
assert not case.is_skipped
693695
assert case.is_passed
696+
assert not case.is_failure
697+
assert not case.is_error
694698

695699
def test_case_is_failed(self):
696700
case = TestCase()
697701
case.result = [Failure()]
698702
assert not case.is_skipped
699703
assert not case.is_passed
704+
assert case.is_failure
705+
assert not case.is_error
706+
707+
def test_case_is_error(self):
708+
case = TestCase()
709+
case.result = [Error()]
710+
assert not case.is_skipped
711+
assert not case.is_passed
712+
assert not case.is_failure
713+
assert case.is_error
700714

701715

702716
class Test_Properties:

tests/test_xunit2.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# -*- coding: utf-8 -*-
22

3-
from junitparser.xunit2 import JUnitXml, TestSuite, TestCase, RerunFailure
3+
from junitparser.xunit2 import JUnitXml, TestSuite, TestCase, RerunFailure, RerunError, FlakyFailure, FlakyError
44
from junitparser import Failure
55
from copy import deepcopy
66

77

88
class Test_TestCase:
9-
def test_case_fromstring(self):
9+
def test_case_rerun_fromstring(self):
1010
text = """<testcase name="testname">
1111
<failure message="failure message" type="FailureType"/>
1212
<rerunFailure message="Not found" type="404">
@@ -16,28 +16,110 @@ def test_case_fromstring(self):
1616
<system-err>Error del servidor</system-err>
1717
<stackTrace>Stacktrace</stackTrace>
1818
</rerunFailure>
19+
<rerunError message="Setup error"/>
1920
<system-out>System out</system-out>
2021
<system-err>System err</system-err>
2122
</testcase>"""
2223
case = TestCase.fromstring(text)
2324
assert case.name == "testname"
25+
assert len(case.result) == 1
2426
assert isinstance(case.result[0], Failure)
2527
assert case.system_out == "System out"
2628
assert case.system_err == "System err"
29+
assert case.is_passed == False
30+
assert case.is_failure == True
31+
assert case.is_error == False
32+
assert case.is_skipped == False
33+
assert case.is_rerun == True
34+
assert case.is_flaky == False
35+
36+
interim_results = case.interim_result
37+
assert len(interim_results) == 3
38+
assert isinstance(interim_results[0], RerunFailure)
39+
assert isinstance(interim_results[1], RerunFailure)
40+
assert isinstance(interim_results[2], RerunError)
41+
2742
rerun_failures = case.rerun_failures()
2843
assert len(rerun_failures) == 2
44+
assert isinstance(rerun_failures[0], RerunFailure)
2945
assert rerun_failures[0].message == "Not found"
3046
assert rerun_failures[0].stack_trace is None
3147
assert rerun_failures[0].system_out == "No ha encontrado"
3248
assert rerun_failures[0].system_err is None
49+
assert isinstance(rerun_failures[1], RerunFailure)
3350
assert rerun_failures[1].message == "Server error"
3451
assert rerun_failures[1].stack_trace == "Stacktrace"
3552
assert rerun_failures[1].system_out is None
3653
assert rerun_failures[1].system_err == "Error del servidor"
37-
assert len(case.rerun_errors()) == 0
54+
55+
rerun_errors = case.rerun_errors()
56+
assert len(rerun_errors) == 1
57+
assert isinstance(rerun_errors[0], RerunError)
58+
assert rerun_errors[0].message == "Setup error"
59+
assert rerun_errors[0].stack_trace is None
60+
assert rerun_errors[0].system_out is None
61+
assert rerun_errors[0].system_err is None
62+
3863
assert len(case.flaky_failures()) == 0
64+
3965
assert len(case.flaky_errors()) == 0
4066

67+
def test_case_flaky_fromstring(self):
68+
text = """<testcase name="testname">
69+
<flakyFailure message="Not found" type="404">
70+
<system-out>No ha encontrado</system-out>
71+
</flakyFailure>
72+
<flakyFailure message="Server error" type="500">
73+
<system-err>Error del servidor</system-err>
74+
<stackTrace>Stacktrace</stackTrace>
75+
</flakyFailure>
76+
<flakyError message="Setup error"/>
77+
<system-out>System out</system-out>
78+
<system-err>System err</system-err>
79+
</testcase>"""
80+
case = TestCase.fromstring(text)
81+
assert case.name == "testname"
82+
assert len(case.result) == 0
83+
assert case.system_out == "System out"
84+
assert case.system_err == "System err"
85+
assert case.is_passed == True
86+
assert case.is_failure == False
87+
assert case.is_error == False
88+
assert case.is_skipped == False
89+
assert case.is_rerun == False
90+
assert case.is_flaky == True
91+
92+
interim_results = case.interim_result
93+
assert len(interim_results) == 3
94+
assert isinstance(interim_results[0], FlakyFailure)
95+
assert isinstance(interim_results[1], FlakyFailure)
96+
assert isinstance(interim_results[2], FlakyError)
97+
98+
assert len(case.rerun_failures()) == 0
99+
100+
assert len(case.rerun_errors()) == 0
101+
102+
flaky_failures = case.flaky_failures()
103+
assert len(flaky_failures) == 2
104+
assert isinstance(flaky_failures[0], FlakyFailure)
105+
assert flaky_failures[0].message == "Not found"
106+
assert flaky_failures[0].stack_trace is None
107+
assert flaky_failures[0].system_out == "No ha encontrado"
108+
assert flaky_failures[0].system_err is None
109+
assert isinstance(flaky_failures[1], FlakyFailure)
110+
assert flaky_failures[1].message == "Server error"
111+
assert flaky_failures[1].stack_trace == "Stacktrace"
112+
assert flaky_failures[1].system_out is None
113+
assert flaky_failures[1].system_err == "Error del servidor"
114+
115+
flaky_errors = case.flaky_errors()
116+
assert len(flaky_errors) == 1
117+
assert isinstance(flaky_errors[0], FlakyError)
118+
assert flaky_errors[0].message == "Setup error"
119+
assert flaky_errors[0].stack_trace is None
120+
assert flaky_errors[0].system_out is None
121+
assert flaky_errors[0].system_err is None
122+
41123
def test_case_rerun(self):
42124
case = TestCase("testname")
43125
rerun_failure = RerunFailure("Not found", "404")

0 commit comments

Comments
 (0)