From 9e99b225d46d96799bbcaf6ae7ff3270d20d0b18 Mon Sep 17 00:00:00 2001 From: Enrico Minack Date: Fri, 20 Dec 2024 18:03:23 +0100 Subject: [PATCH 1/3] JUnitXml.from methods always return JUnitXml instance --- CHANGELOG.md | 7 +++++++ junitparser/junitparser.py | 11 ++++++----- tests/test_fromfile.py | 39 +++++++++++++++++++++++++++++++++----- tests/test_general.py | 13 +++++++++---- tests/test_xunit2.py | 6 +++++- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ed0c6..ac63ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ - Setter method `TestCase.result` used to ignore values of invalid types. This method now throws a `ValueError` instead. - Method `xunit2.TestCase.add_rerun_result` has been renamed to `add_interim_result` result to better reflect class hierarchy of interim (rerun and flaky) results. +- Methods `JUnitXml.fromfile`, `JUnitXml.fromstring`, `JUnitXml.fromroot` always return a `JUnitXml` instance. + Earlier versions return a `TestSuite` instance when the root of the file / string / element is a ``. + A `JUnitXml` instance has already been returned by earlier versions when the root of the file / string / element is a ``. + + If you want to create a `TestSuite` instance from a `` element, use + + TestSuite.fromelem(elem) ## [3.1.2] - 2024-08-31 ### Fixed diff --git a/junitparser/junitparser.py b/junitparser/junitparser.py index 5e70e66..704427b 100644 --- a/junitparser/junitparser.py +++ b/junitparser/junitparser.py @@ -731,11 +731,12 @@ def update_statistics(self): @classmethod def fromroot(cls, root_elem: Element): """Construct JUnit objects from an elementTree root element.""" - if root_elem.tag == "testsuites": - instance = cls() - elif root_elem.tag == "testsuite": - instance = cls.testsuite() - else: + instance = cls() + if root_elem.tag == "testsuite": + testsuite_element = root_elem + root_elem = testsuite_element.makeelement("testsuites", {}) + root_elem.append(testsuite_element) + if not root_elem.tag == "testsuites": raise JUnitXmlError("Invalid format.") instance._elem = root_elem return instance diff --git a/tests/test_fromfile.py b/tests/test_fromfile.py index 31bcbee..55e41a0 100644 --- a/tests/test_fromfile.py +++ b/tests/test_fromfile.py @@ -21,13 +21,19 @@ def do_test_fromfile(fromfile_arg): xml = JUnitXml.fromfile(fromfile_arg) + assert isinstance(xml, JUnitXml) suite1, suite2 = list(iter(xml)) + assert isinstance(suite1, TestSuite) + assert isinstance(suite2, TestSuite) assert len(list(suite1.properties())) == 0 assert len(list(suite2.properties())) == 3 assert len(suite2) == 3 assert suite2.name == "JUnitXmlReporter.constructor" assert suite2.tests == 3 cases = list(suite2.iterchildren(TestCase)) + assert isinstance(cases[0], TestCase) + assert isinstance(cases[1], TestCase) + assert isinstance(cases[2], TestCase) assert isinstance(cases[0].result[0], Failure) assert isinstance(cases[1].result[0], Skipped) assert len(cases[2].result) == 0 @@ -98,13 +104,19 @@ def parse_func(file_path): os.path.join(os.path.dirname(__file__), "data/normal.xml"), parse_func=parse_func, ) + assert isinstance(xml, JUnitXml) suite1, suite2 = list(iter(xml)) + assert isinstance(suite1, TestSuite) + assert isinstance(suite2, TestSuite) assert len(list(suite1.properties())) == 0 assert len(list(suite2.properties())) == 3 assert len(suite2) == 3 assert suite2.name == "JUnitXmlReporter.constructor" assert suite2.tests == 3 cases = list(suite2.iterchildren(TestCase)) + assert isinstance(cases[0], TestCase) + assert isinstance(cases[1], TestCase) + assert isinstance(cases[2], TestCase) assert isinstance(cases[0].result[0], Failure) assert isinstance(cases[1].result[0], Skipped) assert len(cases[2].result) == 0 @@ -114,20 +126,31 @@ def test_fromfile_without_testsuites_tag(): xml = JUnitXml.fromfile( os.path.join(os.path.dirname(__file__), "data/no_suites_tag.xml") ) - cases = list(iter(xml)) - properties = list(iter(xml.properties())) - assert len(properties) == 3 + assert isinstance(xml, JUnitXml) + suites = list(iter(xml)) + assert len(suites) == 1 + suite = suites[0] + assert isinstance(suite, TestSuite) + assert suite.name == "JUnitXmlReporter.constructor" + assert suite.tests == 3 + cases = list(iter(suite)) assert len(cases) == 3 - assert xml.name == "JUnitXmlReporter.constructor" - assert xml.tests == 3 + assert isinstance(cases[0], TestCase) + assert isinstance(cases[1], TestCase) + assert isinstance(cases[2], TestCase) assert isinstance(cases[0].result[0], Failure) assert isinstance(cases[1].result[0], Skipped) assert len(cases[2].result) == 0 + properties = list(iter(suite.properties())) + assert len(properties) == 3 def test_fromfile_with_testsuite_in_testsuite(): xml = JUnitXml.fromfile(os.path.join(os.path.dirname(__file__), "data/jenkins.xml")) + assert isinstance(xml, JUnitXml) suite1, suite2 = list(iter(xml)) + assert isinstance(suite1, TestSuite) + assert isinstance(suite2, TestSuite) assert len(list(suite1.properties())) == 0 assert len(list(suite2.properties())) == 3 assert len(suite2) == 3 @@ -135,8 +158,11 @@ def test_fromfile_with_testsuite_in_testsuite(): assert suite2.tests == 3 direct_cases = list(suite2.iterchildren(TestCase)) assert len(direct_cases) == 1 + assert isinstance(direct_cases[0], TestCase) assert isinstance(direct_cases[0].result[0], Failure) all_cases = list(suite2) + assert isinstance(all_cases[0], TestCase) + assert isinstance(all_cases[1], TestCase) assert isinstance(all_cases[0].result[0], Failure) assert isinstance(all_cases[1].result[0], Skipped) assert len(all_cases[2].result) == 0 @@ -167,6 +193,9 @@ def test_multi_results_in_case(): """ xml = JUnitXml.fromstring(text) + assert isinstance(xml, JUnitXml) suite = next(iter(xml)) + assert isinstance(suite, TestSuite) case = next(iter(suite)) + assert isinstance(case, TestCase) assert len(case.result) == 2 diff --git a/tests/test_general.py b/tests/test_general.py index e2b7613..950d1be 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -175,10 +175,14 @@ def test_fromroot_testsuite(self): """ root_elemt = etree.fromstring(text) result = JUnitXml.fromroot(root_elemt) - assert isinstance(result, TestSuite) - assert result.errors == 1 - assert result.skipped == 1 - cases = list(iter(result)) + assert isinstance(result, JUnitXml) + suites = list(iter(result)) + assert len(suites) == 1 + suite = suites[0] + assert isinstance(suite, TestSuite) + assert suite.errors == 1 + assert suite.skipped == 1 + cases = list(iter(suite)) assert len(cases[0].result) == 0 assert len(cases[1].result) == 2 text = cases[1].result[1].text @@ -200,6 +204,7 @@ def test_fromroot_testsuites(self): """ root_elemt = etree.fromstring(text) result = JUnitXml.fromroot(root_elemt) + assert isinstance(result, JUnitXml) assert result.errors == 1 assert result.skipped == 1 suite = list(iter(result))[0] diff --git a/tests/test_xunit2.py b/tests/test_xunit2.py index 8cd406f..8f9de07 100644 --- a/tests/test_xunit2.py +++ b/tests/test_xunit2.py @@ -129,7 +129,11 @@ def test_suite_fromstring(self): """ - suite = JUnitXml.fromstring(text) + xml = JUnitXml.fromstring(text) + assert isinstance(xml, JUnitXml) + suites = list(xml) + assert len(suites) == 1 + suite = suites[0] assert isinstance(suite, TestSuite) assert suite.name == "suite name" cases = list(suite) From 966f8a60ec429b9049d3b0b229b8061ddf972ad1 Mon Sep 17 00:00:00 2001 From: Enrico Minack Date: Fri, 20 Dec 2024 18:00:25 +0100 Subject: [PATCH 2/3] Add type annotation to JUnitXml.from methods --- junitparser/junitparser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junitparser/junitparser.py b/junitparser/junitparser.py index 704427b..6353f3e 100644 --- a/junitparser/junitparser.py +++ b/junitparser/junitparser.py @@ -729,7 +729,7 @@ def update_statistics(self): self.time = round(time, 3) @classmethod - def fromroot(cls, root_elem: Element): + def fromroot(cls, root_elem: Element) -> "JUnitXml": """Construct JUnit objects from an elementTree root element.""" instance = cls() if root_elem.tag == "testsuite": @@ -742,13 +742,13 @@ def fromroot(cls, root_elem: Element): return instance @classmethod - def fromstring(cls, text: Union[str, bytes]): + def fromstring(cls, text: Union[str, bytes]) -> "JUnitXml": """Construct JUnit objects from an XML string (str or bytes).""" root_elem = etree.fromstring(text) # nosec return cls.fromroot(root_elem) @classmethod - def fromfile(cls, file: Union[str, IO], parse_func=None): + def fromfile(cls, file: Union[str, IO], parse_func=None) -> "JUnitXml": """ Construct JUnit objects from an XML file. From 5cbf10d8aeb00b5cb96c8cad6a437e967b79b528 Mon Sep 17 00:00:00 2001 From: Enrico Minack Date: Tue, 14 Jan 2025 10:11:54 +0100 Subject: [PATCH 3/3] More tests on writing --- tests/test_fromfile.py | 1 + tests/test_general.py | 2 ++ tests/test_write.py | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/test_fromfile.py b/tests/test_fromfile.py index 55e41a0..605c39a 100644 --- a/tests/test_fromfile.py +++ b/tests/test_fromfile.py @@ -5,6 +5,7 @@ from unittest import skipIf from junitparser import ( TestCase, + TestSuite, Skipped, Failure, JUnitXmlError, diff --git a/tests/test_general.py b/tests/test_general.py index 950d1be..d1f795f 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -124,6 +124,7 @@ def test_fromstring(self): """ result = JUnitXml.fromstring(text) + assert isinstance(result, JUnitXml) assert len(result) == 2 assert result.time == 0 @@ -132,6 +133,7 @@ def test_fromstring_no_testsuites(self): """ result = JUnitXml.fromstring(text) + assert isinstance(result, JUnitXml) assert len(result) == 1 assert result.time == 0 diff --git a/tests/test_write.py b/tests/test_write.py index 444b4e1..6e8a837 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -17,7 +17,7 @@ has_lxml = False -def get_expected_xml(test_case_name: str, test_suites: bool = True): +def get_expected_xml(test_case_name: str, test_suites: bool = True, newlines: bool = False): if sys.version.startswith("3.6.") and not has_lxml: expected_test_suite = '' else: @@ -37,9 +37,13 @@ def get_expected_xml(test_case_name: str, test_suites: bool = True): start_test_suites = "" end_test_suites = "" + eol = "\n" if newlines else "" + indent = " " if newlines else "" return ( f"\n" - f'{start_test_suites}{expected_test_suite}{end_test_suites}' + f'{start_test_suites}{expected_test_suite}{eol}' + f'{indent}{eol}' + f'{end_test_suites}' ) @@ -129,6 +133,31 @@ def test_write_nonascii(): assert xmlfile.getvalue().decode("utf-8") == get_expected_xml("用例1") +def test_write_no_testsuites(): + # Has to be a binary string to include xml declarations. + text = b""" + + +""" + xml = JUnitXml.fromstring(text) + assert isinstance(xml, JUnitXml) + suite = next(iter(xml)) + assert isinstance(suite, TestSuite) + case = next(iter(suite)) + assert isinstance(case, TestCase) + assert len(case.result) == 0 + + # writing this JUnitXml object contains a root element + xmlfile = BytesIO() + xml.write(xmlfile) + assert xmlfile.getvalue().decode("utf-8") == get_expected_xml("case1", test_suites=True, newlines=True) + + # writing the inner testsuite reproduces the input string + xmlfile = BytesIO() + suite.write(xmlfile) + assert xmlfile.getvalue().decode("utf-8") == get_expected_xml("case1", test_suites=False, newlines=True) + + def test_read_written_xml(): suite1 = TestSuite() suite1.name = "suite1"