From e584605b36c546c13d4da8c30377092149d9b894 Mon Sep 17 00:00:00 2001 From: cty-ut Date: Sat, 17 Oct 2026 20:44:15 +0900 Subject: [PATCH 1/2] Fix JUnit XML output for setup errors Co-authored-by: Codex --- AUTHORS | 1 + src/_pytest/junitxml.py | 7 ++++- testing/test_junitxml.py | 56 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index bd1e27cb37d..364a2c05ff5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -118,6 +118,7 @@ Clément M.T. Robert Cornelius Riemenschneider CrazyMerlyn Cristian Vera +cty-ut Cyrus Maden Daara Shaw Damian Skrzypczak diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index d33b81a74ac..e6cc94e5470 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -202,7 +202,12 @@ def _add_simple(self, tag: str, message: str, data: str | None = None) -> None: self.append(node) def write_captured_output(self, report: TestReport) -> None: - if not self.xml.log_passing_tests and report.passed: + # A passing teardown does not make a test with a setup error pass. + if ( + not self.xml.log_passing_tests + and report.passed + and not any(node.tag == "error" for node in self.nodes) + ): return content_out = report.capstdout diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index c607d9f68db..c4e97a88f65 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1894,6 +1894,62 @@ def test_func(): assert len(node.find_by_tag("system-out")) == 0 +@parametrize_families +@pytest.mark.parametrize("log_passing_tests", [True, False]) +@pytest.mark.parametrize("teardown_error", [True, False]) +def test_setup_error_keeps_captured_output( + pytester: Pytester, + run_and_parse: RunAndParse, + xunit_family: _JunitFamily, + log_passing_tests: bool, + teardown_error: bool, +) -> None: + pytester.makepyfile( + f""" + import logging + import sys + import pytest + + def emit(phase): + print(phase + " stdout") + print(phase + " stderr", file=sys.stderr) + logging.warning(phase + " log") + + @pytest.fixture + def broken_setup(request): + def cleanup(): + emit("teardown") + if {teardown_error!r}: + raise RuntimeError("teardown failed") + + request.addfinalizer(cleanup) + emit("setup") + raise RuntimeError("setup failed") + + def test_example(broken_setup): + pass + """ + ) + result, dom = run_and_parse( + "-o", + "junit_logging=all", + "-o", + f"junit_log_passing_tests={log_passing_tests}", + family=xunit_family, + ) + result.assert_outcomes(errors=2 if teardown_error else 1) + testcase = dom.get_first_by_tag("testcase") + # Both phases' output should be retained exactly once, even if cleanup fails. + stdout_nodes = testcase.find_by_tag("system-out") + stderr_nodes = testcase.find_by_tag("system-err") + assert len(stdout_nodes) == 1 + assert len(stderr_nodes) == 1 + for phase in ("setup", "teardown"): + assert stdout_nodes[0].text.count(f"{phase} stdout") == 1 + assert stdout_nodes[0].text.count(f"{phase} log") == 1 + assert stderr_nodes[0].text.count(f"{phase} stderr") == 1 + + def test_no_message_quiet(pytester: Pytester) -> None: """Do not show the summary banner when --quiet is given (#13700).""" pytester.makepyfile("def test(): pass") From 6f53c586a3e4a60c392aeec33171de804c36b73e Mon Sep 17 00:00:00 2001 From: cty-ut Date: Sat, 17 Oct 2026 20:44:53 +0900 Subject: [PATCH 2/2] Add changelog for JUnit XML setup error fix Co-authored-by: Codex --- changelog/15196.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/15196.bugfix.rst diff --git a/changelog/15196.bugfix.rst b/changelog/15196.bugfix.rst new file mode 100644 index 00000000000..bda2c943cd4 --- /dev/null +++ b/changelog/15196.bugfix.rst @@ -0,0 +1 @@ +Fixed missing captured output in JUnit XML reports for setup errors when ``junit_log_passing_tests`` is disabled.