Skip to content

Commit

Permalink
Fix result handling for class methods
Browse files Browse the repository at this point in the history
Fix result handling for class methods

Resolve the issue where the pytest plugin failed to update the original
XML file for flaky tests when the test function was inside a class. Now
it properly compares and updates test results for both standalone
functions and class methods.
Refactor uid handling for conciseness and clarity.

ROCKY-18669
  • Loading branch information
LucasRochaAbraao authored Apr 20, 2023
1 parent 27c6921 commit 946645d
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 58 deletions.
39 changes: 36 additions & 3 deletions src/pytest_update_test_results/update_test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Dict

from _pytest.reports import TestReport
from dataclasses import dataclass


def modify_xml(original_xml: Path, retest_results: Dict[str, TestReport], new_xml: Path) -> None:
Expand All @@ -23,13 +24,14 @@ def modify_xml(original_xml: Path, retest_results: Dict[str, TestReport], new_xm
original_failure_count = retest_failure_count = int(testsuite.attrib["failures"])
original_error_count = retest_error_count = int(testsuite.attrib["errors"])

# test_uid = nodeid => classname + name do xml
succeed_in_retest = {
result.location[2] for result in retest_results.values() if result.outcome == "passed"
TestUid.from_nodeid(report.nodeid) for report in retest_results.values() if report.outcome == "passed"
}

for testcase in testsuite:
testcase_name = testcase.attrib["name"]
if testcase_name in succeed_in_retest:
testcase_identifier = TestUid(testcase.attrib["classname"], testcase.attrib["name"])
if testcase_identifier in succeed_in_retest:
failure = testcase.find("failure")
if failure is not None:
retest_failure_count -= 1
Expand All @@ -47,3 +49,34 @@ def modify_xml(original_xml: Path, retest_results: Dict[str, TestReport], new_xm
tree.write(new_xml, encoding="utf-8", xml_declaration=True)
elif original_xml != new_xml:
shutil.copy(original_xml, new_xml)


@dataclass(frozen=True)
class TestUid:
classname: str
testname: str

@classmethod
def from_nodeid(cls, test_nodeid) -> "TestUid":
"""
Converts a TestReport nodeid to a unique identifier for a test case, and returns a tuple containing
the UID and the test name. This is required because there is no direct way to map a `TestReport`
object to a jUnit XML entry.
:param nodeid: A TestReport nodeid string, typically in the format: "path/to/test_file.py::TestClass::test_name".
:return: A tuple containing the UID and the test name. The UID will be in the format:
"path.to.test_file.TestClass" if there's a class, or "path.to.test_file" if not.
"""
test_filepath_uid, test_name = test_nodeid.rsplit("::", 1)

if "::" in test_filepath_uid:
test_filepath_uid, class_name = test_filepath_uid.split("::")
else:
class_name = ""

test_filepath_without_ext = Path(test_filepath_uid).with_suffix('')
test_name_with_dots = ".".join(test_filepath_without_ext.parts)

test_uid = f"{test_name_with_dots}.{class_name}" if class_name else test_name_with_dots

return TestUid(test_uid, test_name)
116 changes: 76 additions & 40 deletions test/test_pytest_update_test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

def test_one_failure(datadir: Path) -> None:
retest_results = {
"test_xml_three_failed_zero_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_three_failed_zero_passed",
"test_one_failure": TestReport(
nodeid="test/test_flaky.py::test_one_failure_passed",
outcome="passed",
location=("test/test_pytest_merge_xml.py", 88, "test_xml_three_failed_zero_passed"),
keywords="",
location=("test/test_pytest_merge_xml.py", 16, "test_one_failure_passed"),
keywords={},
longrepr=None,
when="call",
)
Expand All @@ -31,31 +31,31 @@ def test_one_failure(datadir: Path) -> None:
def test_two_failures(datadir: Path) -> None:
retest_results_passed = {
"test_xml_one_failed_two_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_one_failed_two_passed",
nodeid="test/test_flaky.py::first_test_passed",
outcome="passed",
location=("test/test_pytest_merge_xml.py", 9, "test_xml_one_failed_two_passed"),
location=("test/test_flaky.py", 8, "first_test_passed"),
keywords={},
longrepr=None,
when="call",
),
"test_xml_two_failed_one_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_two_failed_one_passed",
"second_test_passed": TestReport(
nodeid="test/test_flaky.py::second_test_passed",
outcome="passed",
location=("test/test_pytest_merge_xml.py", 49, "test_xml_two_failed_one_passed"),
keywords="",
location=("test/test_flaky.py", 16, "second_test_passed"),
keywords={},
longrepr=None,
when="call",
),
"test_xml_three_failed_zero_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_three_failed_zero_passed",
"third_test_failed": TestReport(
nodeid="test/test_flaky.py::third_test_failed",
outcome="failed",
location=("test/test_pytest_merge_xml.py", 88, "test_xml_three_failed_zero_passed"),
keywords="",
location=("test/test_flaky.py", 48, "third_test_failed"),
keywords={},
longrepr=None,
when="call",
),
}
modified_file = datadir / "failure_tmp.xml"
modified_file = datadir / "two_failures.retest.xml"
modify_xml(Path(datadir / "two_failures.xml"), retest_results_passed, modified_file)

root_el = ET.parse(modified_file).getroot()
Expand All @@ -68,26 +68,26 @@ def test_two_failures(datadir: Path) -> None:


def test_one_failure_one_error(datadir: Path) -> None:
"""Tests modify_xml() with that change a XML with one failure and one error"""
"""Tests modify_xml() with that change an XML with one failure and one error"""
retest_results_passed = {
"test_xml_one_failed_two_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_one_failed_two_passed",
"test_failure_passed": TestReport(
nodeid="test/test_flaky.py::test_failure_passed",
outcome="passed",
location=("test/test_pytest_merge_xml.py", 9, "test_xml_one_failed_two_passed"),
location=("test/test_flaky.py", 8, "test_failure_passed"),
keywords={},
longrepr=None,
when="call",
),
"test_xml_three_failed_zero_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_three_failed_zero_passed",
"test_error_passed": TestReport(
nodeid="test/test_flaky.py::test_error_passed",
outcome="passed",
location=("test/test_pytest_merge_xml.py", 88, "test_xml_three_failed_zero_passed"),
keywords="",
location=("test/test_flaky.py", 16, "test_error_passed"),
keywords={},
longrepr=None,
when="call",
),
}
modified_file = datadir / "failure.retest.xml"
modified_file = datadir / "one_failure_one_error.retest.xml"
modify_xml(
Path(datadir / "one_failure_one_error.xml"),
retest_results_passed,
Expand All @@ -106,31 +106,23 @@ def test_succeed_to_failure_on_retest_not_supported(datadir: Path) -> None:
"""
For now, pytest-update-test-results is supposed to always run with `--last-failed` flag.
It convert failed outcomes into passed, but DOES NOT convert passed test cases into
It converts failed outcomes into passed, but DOES NOT convert passed test cases into
failed ones (that could happen if `--update-xml` runs without `--last-failed`).
"""
retest_results_mix = {
"test_xml_one_failed_two_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_one_failed_two_passed",
"test_xml_first_retest_failed": TestReport(
nodeid="test/test_flaky.py::test_xml_first_retest_failed",
outcome="failed",
location=("test/test_pytest_merge_xml.py", 9, "test_xml_one_failed_two_passed"),
location=("test/test_flaky.py", 8, "test_xml_first_retest_failed"),
keywords={},
longrepr=None,
when="call",
),
"test_xml_two_failed_one_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_two_failed_one_passed",
"test_xml_second_retest_failed": TestReport(
nodeid="test/test_flaky.py::test_xml_second_retest_failed",
outcome="failed",
location=("test/test_pytest_merge_xml.py", 49, "test_xml_two_failed_one_passed"),
keywords="",
longrepr=None,
when="call",
),
"test_xml_three_failed_zero_passed": TestReport(
nodeid="test/test_pytest_merge_xml.py::test_xml_three_failed_zero_passed",
outcome="failed",
location=("test/test_pytest_merge_xml.py", 88, "test_xml_three_failed_zero_passed"),
keywords="",
location=("test/test_flaky.py", 16, "test_xml_second_retest_failed"),
keywords={},
longrepr=None,
when="call",
),
Expand All @@ -142,3 +134,47 @@ def test_succeed_to_failure_on_retest_not_supported(datadir: Path) -> None:
testsuite_el = root_el.find("testsuite")

assert testsuite_el.attrib["failures"] == "0"


def test_failure_using_unittest_style(datadir: Path) -> None:
retest_results = {
"test_xml_failure_unittest_style": TestReport(
nodeid="test/test_flaky.py::Test::test_xml_failure_unittest_style",
outcome="passed",
location=("test/test_flaky.py", 8, "Test.test_xml_failure_unittest_style"),
keywords={},
longrepr=None,
when="call",
)
}

modified_file = datadir / "one_failure_unittest_style.retest.xml"
modify_xml(Path(datadir / "one_failure_unittest_style.xml"), retest_results, modified_file)

et = ET.parse(modified_file)
root_el = et.getroot()
testsuite_el = root_el.find("testsuite")
assert testsuite_el.attrib["failures"] == "0"
assert len(list(testsuite_el.iter("failure"))) == 0


def test_duplicated_test_names(datadir: Path) -> None:
retest_results = {
"test_duplicated_test_names": TestReport(
nodeid="test/test_flaky.py::Test::test_duplicated_test_names",
outcome="passed",
location=("test/test_flaky.py", 8, "Test.test_duplicated_test_names"),
keywords={},
longrepr=None,
when="call",
)
}

modified_file = datadir / "one_failure_duplicated_names.retest.xml"
modify_xml(Path(datadir / "one_failure_duplicated_names.xml"), retest_results, modified_file)

et = ET.parse(modified_file)
root_el = et.getroot()
testsuite_el = root_el.find("testsuite")
assert testsuite_el.attrib["failures"] == "0"
assert len(list(testsuite_el.iter("failure"))) == 0
5 changes: 2 additions & 3 deletions test/test_pytest_update_test_results/no_failures.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<testsuites>
<testsuite errors="0" failures="0" hostname="BRWS009" name="pytest" skipped="0" tests="3" time="0.035" timestamp="2022-10-04T15:10:10.669419">
<testcase classname="src.test_flaky" name="test_xml_one_failed_two_passed" time="0.000" />
<testcase classname="src.test_flaky" name="test_xml_two_failed_one_passed" time="0.000" />
<testcase classname="src.test_flaky" name="test_xml_three_failed_zero_passed" time="0.000" />
<testcase classname="test.test_flaky" name="test_xml_first_retest_failed" time="0.000" />
<testcase classname="test.test_flaky" name="test_xml_second_retest_failed" time="0.000" />
</testsuite>
</testsuites>
9 changes: 4 additions & 5 deletions test/test_pytest_update_test_results/one_failure.xml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<testsuites>
<testsuite errors="0" failures="1" hostname="BRWS009" name="pytest" skipped="0" tests="3" time="0.035" timestamp="2022-10-04T15:10:10.669419">
<testcase classname="src.test_flaky" name="test_xml_one_failed_two_passed" time="0.000" />
<testcase classname="src.test_flaky" name="test_xml_two_failed_one_passed" time="0.000" />
<testcase classname="src.test_flaky" name="test_xml_three_failed_zero_passed" time="0.000">
<failure message="assert False">def test_xml_three_failed_zero_passed():
<testcase classname="test.test_flaky" name="test_passed" time="0.000" />
<testcase classname="test.test_flaky" name="test_one_failure_passed" time="0.000">
<failure message="assert False">def test_one_failure_passed():
&gt; assert False
E assert False

src/test_flaky.py:16: AssertionError</failure>
test/test_flaky.py:16: AssertionError</failure>
</testcase>
</testsuite>
</testsuites>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<testsuites>
<testsuite errors="0" failures="1" hostname="BRWS009" name="pytest" skipped="0" tests="3" time="0.035" timestamp="2022-10-04T15:10:10.669419">
<testcase classname="test.test_flaky" name="test_name" time="0.000" />
<testcase classname="test.test_flaky.OtherClassName" name="test_duplicated_test_names" time="0.000" />
<testcase classname="test.test_flaky.Test" name="test_duplicated_test_names" time="0.000">
<failure message="assert False">def test_duplicated_test_names():
&gt; assert False
E assert False

test/test_flaky.py:8: AssertionError</failure>
</testcase>
</testsuite>
</testsuites>
10 changes: 5 additions & 5 deletions test/test_pytest_update_test_results/one_failure_one_error.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<testsuites>
<testsuite errors="1" failures="1" hostname="BRWS009" name="pytest" skipped="0" tests="3" time="0.035" timestamp="2022-10-04T15:10:10.669419">
<testcase classname="src.test_flaky" name="test_xml_one_failed_two_passed" time="0.000">
<failure message="assert False">def test_xml_one_failed_two_passed():
<testcase classname="test.test_flaky" name="test_failure_passed" time="0.000">
<failure message="assert False">def test_failure_passed():
&gt; assert False
E assert False

src/test_flaky.py:8: AssertionError</failure>
</testcase><testcase classname="src.test_flaky" name="test_xml_two_failed_one_passed" time="0.000" />
<testcase classname="src.test_flaky" name="test_xml_three_failed_zero_passed" time="0.000">
<error message="failed on setup with &quot;worker 'gw5' crashed while running 'test_xml_three_failed_zero_passed'&quot;">worker 'gw5' crashed while running 'build/bare_tests/source/python/_python_tests/rocky30/core/simulator/_tests/test_rocky_solver_process.py::testRockyFluentTwoWaySolverMisconfiguration'</error>
</testcase><testcase classname="src.test_flaky" name="test_error_passed" time="0.000" />
<testcase classname="test.test_flaky" name="test_error_passed" time="0.000">
<error message="failed on setup with &quot;worker 'gw5' crashed while running 'test_error_passed'&quot;">worker 'gw5' crashed while running 'build/bare_tests/source/python/_python_tests/rocky30/core/simulator/_tests/test_rocky_solver_process.py::testRockyFluentTwoWaySolverMisconfiguration'</error>
</testcase>
</testsuite>
</testsuites>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<testsuites>
<testsuite errors="0" failures="1" hostname="BRWS009" name="pytest" skipped="0" tests="3" time="0.035" timestamp="2022-10-04T15:10:10.669419">
<testcase classname="test.test_flaky" name="test_name" time="0.000" />
<testcase classname="test.test_flaky.Test" name="test_xml_failure_unittest_style" time="0.000">
<failure message="assert False">def test_xml_failure_unittest_style():
&gt; assert False
E assert False

test/test_flaky.py:8: AssertionError</failure>
</testcase>
</testsuite>
</testsuites>
4 changes: 2 additions & 2 deletions test/test_pytest_update_test_results/two_failures.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<testsuites><testsuite errors="0" failures="2" hostname="BRWS009" name="pytest" skipped="0" tests="3" time="0.035" timestamp="2022-10-04T15:10:10.669419"><testcase classname="src.test_flaky" name="test_xml_one_failed_two_passed" time="0.000"><failure message="assert False">def test_xml_one_failed_two_passed():
<testsuites><testsuite errors="0" failures="2" hostname="BRWS009" name="pytest" skipped="0" tests="3" time="0.035" timestamp="2022-10-04T15:10:10.669419"><testcase classname="test.test_flaky" name="first_test_passed" time="0.000"><failure message="assert False">def first_test_passed():
&gt; assert False
E assert False

src/test_flaky.py:8: AssertionError</failure></testcase><testcase classname="src.test_flaky" name="test_xml_two_failed_one_passed" time="0.000" /><testcase classname="src.test_flaky" name="test_xml_three_failed_zero_passed" time="0.000"><failure message="assert False">def test_xml_three_failed_zero_passed():
src/test_flaky.py:8: AssertionError</failure></testcase><testcase classname="test.test_flaky" name="second_test_passed" time="0.000" /><testcase classname="test.test_flaky" name="third_test_failed" time="0.000"><failure message="assert False">def third_test_failed():
&gt; assert False
E assert False

Expand Down

0 comments on commit 946645d

Please sign in to comment.