Skip to content

Commit 5a88b61

Browse files
committed
Add junitxml test for nodeid collision safety
The test forces interlaced reports with a shared nodeid and asserts captured output is suppressed rather than mis-attributed. Refs #14078
1 parent 9361b9b commit 5a88b61

File tree

1 file changed

+85
-0
lines changed

1 file changed

+85
-0
lines changed

testing/test_junitxml.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,6 +1865,91 @@ def test_two(setup_output):
18651865
assert (expected in system_err_text) is expect_err
18661866

18671867

1868+
def test_interlaced_reports_nodeid_collision(pytester: Pytester) -> None:
1869+
pytester.makeconftest(
1870+
"""
1871+
import pytest
1872+
from _pytest.runner import call_and_report
1873+
1874+
_reports = []
1875+
1876+
@pytest.hookimpl(tryfirst=True)
1877+
def pytest_runtest_protocol(item, nextitem):
1878+
item.ihook.pytest_runtest_logstart(
1879+
nodeid=item.nodeid, location=item.location
1880+
)
1881+
reports = [call_and_report(item, "setup", log=False)]
1882+
if reports[0].passed:
1883+
reports.append(call_and_report(item, "call", log=False))
1884+
reports.append(
1885+
call_and_report(item, "teardown", log=False, nextitem=nextitem)
1886+
)
1887+
item.ihook.pytest_runtest_logfinish(
1888+
nodeid=item.nodeid, location=item.location
1889+
)
1890+
1891+
_reports.append(reports)
1892+
if nextitem is not None:
1893+
return True
1894+
1895+
ihook = item.ihook
1896+
for reports in _reports:
1897+
ihook.pytest_runtest_logreport(report=reports[0])
1898+
for reports in _reports:
1899+
if len(reports) == 3:
1900+
ihook.pytest_runtest_logreport(report=reports[1])
1901+
for reports in reversed(_reports):
1902+
ihook.pytest_runtest_logreport(report=reports[-1])
1903+
return True
1904+
1905+
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
1906+
def pytest_runtest_logreport(report):
1907+
if report.when in ("setup", "call", "teardown"):
1908+
report.nodeid = "collided::nodeid"
1909+
sections = []
1910+
for name, content in report.sections:
1911+
if name.startswith("Captured "):
1912+
if name.endswith(f" {report.when}"):
1913+
sections.append((name, content))
1914+
else:
1915+
sections.append((name, content))
1916+
report.sections = sections
1917+
yield
1918+
"""
1919+
)
1920+
pytester.makepyfile(
1921+
"""
1922+
import sys
1923+
import pytest
1924+
1925+
@pytest.fixture
1926+
def setup_output(request):
1927+
print(f"SETUP_STDOUT_{request.node.name}")
1928+
sys.stderr.write(f"SETUP_STDERR_{request.node.name}\\n")
1929+
1930+
def test_one(setup_output):
1931+
print("CALL_STDOUT_test_one")
1932+
sys.stderr.write("CALL_STDERR_test_one\\n")
1933+
1934+
def test_two(setup_output):
1935+
print("CALL_STDOUT_test_two")
1936+
sys.stderr.write("CALL_STDERR_test_two\\n")
1937+
"""
1938+
)
1939+
1940+
xml_path = pytester.path.joinpath("junit.xml")
1941+
result = pytester.runpytest(
1942+
f"--junitxml={xml_path}",
1943+
"--override-ini=junit_family=xunit1",
1944+
"--override-ini=junit_logging=all",
1945+
)
1946+
assert result.ret == 0
1947+
1948+
root = ET.parse(xml_path).getroot()
1949+
assert not list(root.iter("system-out"))
1950+
assert not list(root.iter("system-err"))
1951+
1952+
18681953
@parametrize_families
18691954
def test_logging_passing_tests_disabled_does_not_log_test_output(
18701955
pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str

0 commit comments

Comments
 (0)