Skip to content

Commit

Permalink
Add more is_* properties to JUnit and XUnit2 TestCase
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
EnricoMi committed Jan 13, 2025
1 parent 2d6dd39 commit ee24df5
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 37 deletions.
31 changes: 17 additions & 14 deletions junitparser/junitparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ class TestCase(Element):
time = FloatAttr()
__test__ = False

# JUnit TestCase children are final results, SystemOut and SystemErr
ITER_TYPES = {t._tag: t for t in set.union(FINAL_RESULTS, {SystemOut, SystemErr})}

def __init__(self, name: str = None, classname: str = None, time: float = None):
super().__init__(self._tag)
if name is not None:
Expand All @@ -334,11 +337,9 @@ def __hash__(self):
return super().__hash__()

def __iter__(self):
all_types = set.union(FINAL_RESULTS, {SystemOut}, {SystemErr})
for elem in self._elem.iter():
for entry_type in all_types:
if elem.tag == entry_type._tag:
yield entry_type.fromelem(elem)
if elem.tag in self.ITER_TYPES:
yield self.ITER_TYPES[elem.tag].fromelem(elem)

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

@property
def is_failure(self):
"""Whether this testcase failed."""
return any(isinstance(r, Failure) for r in self.result)

@property
def is_error(self):
"""Whether this testcase errored."""
return any(isinstance(r, Error) for r in self.result)

@property
def is_skipped(self):
"""Whether this testcase was skipped."""
for r in self.result:
if isinstance(r, Skipped):
return True
return False
return any(isinstance(r, Skipped) for r in self.result)

@property
def result(self) -> List[FinalResult]:
"""A list of :class:`Failure`, :class:`Skipped`, or :class:`Error` objects."""
results = []
for entry in self:
if isinstance(entry, FinalResult):
results.append(entry)

return results
return [entry for entry in self if isinstance(entry, FinalResult)]

@result.setter
def result(self, value: Union[FinalResult, List[FinalResult]]):
Expand Down
56 changes: 38 additions & 18 deletions junitparser/xunit2.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
"""

import itertools
from typing import List, TypeVar
from typing import List, Type, TypeVar
from . import junitparser

T = TypeVar("T")


class TestSuite(junitparser.TestSuite):
"""TestSuite for Pytest, with some different attributes."""

Expand Down Expand Up @@ -175,31 +172,54 @@ class FlakyError(InterimResult):
_tag = "flakyError"


RERUN_RESULTS = {RerunFailure, RerunError, FlakyFailure, FlakyError}


R = TypeVar("R", bound=InterimResult)


class TestCase(junitparser.TestCase):
group = junitparser.Attr()

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

def rerun_failures(self):
def _interim_results(self, _type: Type[R]) -> List[R]:
return [entry for entry in self if isinstance(entry, _type)]

@property
def interim_result(self) -> List[InterimResult]:
"""
A list of interim results: :class:`RerunFailure`, :class:`RerunError`, :class:`FlakyFailure`, or :class:`FlakyError` objects.
This is complementary to the result property returning final results.
"""
return self._interim_results(InterimResult)

def rerun_failures(self) -> List[RerunFailure]:
"""<rerunFailure>"""
return self._rerun_results(RerunFailure)
return self._interim_results(RerunFailure)

def rerun_errors(self):
def rerun_errors(self) -> List[RerunError]:
"""<rerunError>"""
return self._rerun_results(RerunError)
return self._interim_results(RerunError)

def flaky_failures(self):
def flaky_failures(self) -> List[FlakyFailure]:
"""<flakyFailure>"""
return self._rerun_results(FlakyFailure)
return self._interim_results(FlakyFailure)

def flaky_errors(self):
def flaky_errors(self) -> List[FlakyError]:
"""<flakyError>"""
return self._rerun_results(FlakyError)
return self._interim_results(FlakyError)

@property
def is_rerun(self) -> bool:
"""Whether this testcase is rerun, i.e., there are rerun failures or errors."""
return any(self.rerun_failures()) or any(self.rerun_errors())

@property
def is_flaky(self) -> bool:
"""Whether this testcase is flaky, i.e., there are flaky failures or errors."""
return any(self.flaky_failures()) or any(self.flaky_errors())

def add_interim_result(self, result: InterimResult):
"""Append an interim (rerun or flaky) result to the testcase. A testcase can have multiple interim results."""
Expand Down
92 changes: 87 additions & 5 deletions tests/test_xunit2.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-

from junitparser.xunit2 import JUnitXml, TestSuite, TestCase, RerunFailure
from junitparser.xunit2 import JUnitXml, TestSuite, TestCase, RerunFailure, RerunError, FlakyFailure, FlakyError
from junitparser import Failure
from copy import deepcopy


class Test_TestCase:
def test_case_fromstring(self):
def test_case_rerun_fromstring(self):
text = """<testcase name="testname">
<failure message="failure message" type="FailureType"/>
<rerunFailure message="Not found" type="404">
Expand All @@ -16,28 +16,110 @@ def test_case_fromstring(self):
<system-err>Error del servidor</system-err>
<stackTrace>Stacktrace</stackTrace>
</rerunFailure>
<rerunError message="Setup error"/>
<system-out>System out</system-out>
<system-err>System err</system-err>
</testcase>"""
case = TestCase.fromstring(text)
assert case.name == "testname"
assert len(case.result) == 1
assert isinstance(case.result[0], Failure)
assert case.system_out == "System out"
assert case.system_err == "System err"
assert case.is_passed == False
assert case.is_failure == True
assert case.is_error == False
assert case.is_skipped == False
assert case.is_rerun == True
assert case.is_flaky == False

interim_results = case.interim_result
assert len(interim_results) == 3
assert isinstance(interim_results[0], RerunFailure)
assert isinstance(interim_results[1], RerunFailure)
assert isinstance(interim_results[2], RerunError)

rerun_failures = case.rerun_failures()
assert len(rerun_failures) == 2
assert isinstance(rerun_failures[0], RerunFailure)
assert rerun_failures[0].message == "Not found"
assert rerun_failures[0].stack_trace is None
assert rerun_failures[0].system_out == "No ha encontrado"
assert rerun_failures[0].system_err is None
assert isinstance(rerun_failures[1], RerunFailure)
assert rerun_failures[1].message == "Server error"
assert rerun_failures[1].stack_trace == "Stacktrace"
assert rerun_failures[1].system_out is None
assert rerun_failures[1].system_err == "Error del servidor"
assert len(case.rerun_errors()) == 0

rerun_errors = case.rerun_errors()
assert len(rerun_errors) == 1
assert isinstance(rerun_errors[0], RerunError)
assert rerun_errors[0].message == "Setup error"
assert rerun_errors[0].stack_trace is None
assert rerun_errors[0].system_out is None
assert rerun_errors[0].system_err is None

assert len(case.flaky_failures()) == 0

assert len(case.flaky_errors()) == 0

def test_case_flaky_fromstring(self):
text = """<testcase name="testname">
<flakyFailure message="Not found" type="404">
<system-out>No ha encontrado</system-out>
</flakyFailure>
<flakyFailure message="Server error" type="500">
<system-err>Error del servidor</system-err>
<stackTrace>Stacktrace</stackTrace>
</flakyFailure>
<flakyError message="Setup error"/>
<system-out>System out</system-out>
<system-err>System err</system-err>
</testcase>"""
case = TestCase.fromstring(text)
assert case.name == "testname"
assert len(case.result) == 0
assert case.system_out == "System out"
assert case.system_err == "System err"
assert case.is_passed == True
assert case.is_failure == False
assert case.is_error == False
assert case.is_skipped == False
assert case.is_rerun == False
assert case.is_flaky == True

interim_results = case.interim_result
assert len(interim_results) == 3
assert isinstance(interim_results[0], FlakyFailure)
assert isinstance(interim_results[1], FlakyFailure)
assert isinstance(interim_results[2], FlakyError)

assert len(case.rerun_failures()) == 0

assert len(case.rerun_errors()) == 0

flaky_failures = case.flaky_failures()
assert len(flaky_failures) == 2
assert isinstance(flaky_failures[0], FlakyFailure)
assert flaky_failures[0].message == "Not found"
assert flaky_failures[0].stack_trace is None
assert flaky_failures[0].system_out == "No ha encontrado"
assert flaky_failures[0].system_err is None
assert isinstance(flaky_failures[1], FlakyFailure)
assert flaky_failures[1].message == "Server error"
assert flaky_failures[1].stack_trace == "Stacktrace"
assert flaky_failures[1].system_out is None
assert flaky_failures[1].system_err == "Error del servidor"

flaky_errors = case.flaky_errors()
assert len(flaky_errors) == 1
assert isinstance(flaky_errors[0], FlakyError)
assert flaky_errors[0].message == "Setup error"
assert flaky_errors[0].stack_trace is None
assert flaky_errors[0].system_out is None
assert flaky_errors[0].system_err is None

def test_case_rerun(self):
case = TestCase("testname")
rerun_failure = RerunFailure("Not found", "404")
Expand All @@ -46,14 +128,14 @@ def test_case_rerun(self):
rerun_failure.stack_trace = "Stack"
rerun_failure.system_err = "E404"
rerun_failure.system_out = "NOT FOUND"
case.add_rerun_result(rerun_failure)
case.add_interim_result(rerun_failure)
assert len(case.rerun_failures()) == 1
# Interesting, same object is only added once by xml libs
failure2 = deepcopy(rerun_failure)
failure2.stack_trace = "Stack2"
failure2.system_err = "E401"
failure2.system_out = "401 Error"
case.add_rerun_result(failure2)
case.add_interim_result(failure2)
assert len(case.rerun_failures()) == 2


Expand Down

0 comments on commit ee24df5

Please sign in to comment.