From ee24df53993915647e4c586a5327308b7b8222a7 Mon Sep 17 00:00:00 2001 From: Enrico Minack <github@enrico.minack.dev> Date: Mon, 13 Jan 2025 11:24:54 +0100 Subject: [PATCH] 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 --- junitparser/junitparser.py | 31 +++++++------ junitparser/xunit2.py | 56 +++++++++++++++-------- tests/test_xunit2.py | 92 +++++++++++++++++++++++++++++++++++--- 3 files changed, 142 insertions(+), 37 deletions(-) diff --git a/junitparser/junitparser.py b/junitparser/junitparser.py index be6109a..431833a 100644 --- a/junitparser/junitparser.py +++ b/junitparser/junitparser.py @@ -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: @@ -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. @@ -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]]): diff --git a/junitparser/xunit2.py b/junitparser/xunit2.py index 236ede4..500793f 100644 --- a/junitparser/xunit2.py +++ b/junitparser/xunit2.py @@ -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.""" @@ -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.""" diff --git a/tests/test_xunit2.py b/tests/test_xunit2.py index 07ccf71..da50140 100644 --- a/tests/test_xunit2.py +++ b/tests/test_xunit2.py @@ -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"> @@ -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") @@ -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