Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make JUnitXml.from* methods return only JUnitXml instances #142

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<TestSuite>`.
A `JUnitXml` instance has already been returned by earlier versions when the root of the file / string / element is a `<TestSuites>`.

If you want to create a `TestSuite` instance from a `<TestSuite>` element, use

TestSuite.fromelem(elem)

## [3.1.2] - 2024-08-31
### Fixed
Expand Down
17 changes: 9 additions & 8 deletions junitparser/junitparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,25 +743,26 @@ 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."""
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

@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.

Expand Down
40 changes: 35 additions & 5 deletions tests/test_fromfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest import skipIf
from junitparser import (
TestCase,
TestSuite,
Skipped,
Failure,
JUnitXmlError,
Expand All @@ -21,13 +22,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
Expand Down Expand Up @@ -98,13 +105,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
Expand All @@ -114,29 +127,43 @@ 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
assert suite2.name == "JUnitXmlReporter.constructor"
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
Expand Down Expand Up @@ -167,6 +194,9 @@ def test_multi_results_in_case():
</testsuite>
</testsuites>"""
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
15 changes: 11 additions & 4 deletions tests/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def test_fromstring(self):
<testcase name="testname2">
</testcase></testsuite></testsuites>"""
result = JUnitXml.fromstring(text)
assert isinstance(result, JUnitXml)
assert len(result) == 2
assert result.time == 0

Expand All @@ -135,6 +136,7 @@ def test_fromstring_no_testsuites(self):
<testcase name="testname1">
</testcase></testsuite>"""
result = JUnitXml.fromstring(text)
assert isinstance(result, JUnitXml)
assert len(result) == 1
assert result.time == 0

Expand Down Expand Up @@ -178,10 +180,14 @@ def test_fromroot_testsuite(self):
</testsuite>"""
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
Expand All @@ -203,6 +209,7 @@ def test_fromroot_testsuites(self):
</testsuites>"""
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]
Expand Down
33 changes: 31 additions & 2 deletions tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
python_minor = int(sys.version.split(".")[1])


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 python_major == 3 and python_minor <= 7 and not has_lxml:
expected_test_suite = '<testsuite errors="0" failures="0" name="suite1" skipped="0" tests="1" time="0">'
else:
Expand All @@ -42,9 +42,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"<?xml version='1.0' encoding='{encoding}'?>\n"
f'{start_test_suites}{expected_test_suite}<testcase name="{test_case_name}"{closing_tag}></testsuite>{end_test_suites}'
f'{start_test_suites}{expected_test_suite}{eol}'
f'{indent}<testcase name="{test_case_name}"{closing_tag}>{eol}'
f'</testsuite>{end_test_suites}'
)


Expand Down Expand Up @@ -174,6 +178,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 version='1.0' encoding='UTF-8'?>
<testsuite name="suite1" tests="1" errors="0" failures="0" skipped="0" time="0">
<testcase name="case1"/>
</testsuite>"""
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 <testsuites> 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"
Expand Down
6 changes: 5 additions & 1 deletion tests/test_xunit2.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,11 @@ def test_suite_fromstring(self):
<testcase name="test name 1"/>
<testcase name="test name 2"/>
</testsuite>"""
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)
Expand Down
Loading