Skip to content

Commit a255b4f

Browse files
author
Fazeel Usmani
committed
Fix session fixture teardown exceptions being reported as duplicate XFAILs
When a session-scoped autouse fixture raises an exception during teardown, and the last test in the suite is marked @pytest.mark.xfail, pytest was incorrectly showing an extra XFAIL line (duplicated) instead of reporting the teardown failure as an ERROR. The root cause was that the xfail handling in pytest_runtest_makereport was being applied to all phases (setup, call, teardown), converting any exception into an xfail result if the test was marked with xfail. This meant that session fixture teardown exceptions were being misreported as expected failures. The fix restricts xfail handling to only apply during the "call" phase. Setup and teardown failures are now properly reported as errors, regardless of xfail markers on the test. This aligns with the principle that xfail should only apply to test execution, not to fixture setup/teardown failures. Fixes #8375
1 parent 5ae9e47 commit a255b4f

File tree

3 files changed

+77
-21
lines changed

3 files changed

+77
-21
lines changed

src/_pytest/skipping.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -287,21 +287,27 @@ def pytest_runtest_makereport(
287287
rep.outcome = "skipped"
288288
elif not rep.skipped and xfailed:
289289
if call.excinfo:
290-
raises = xfailed.raises
291-
if raises is None or (
292-
(
293-
isinstance(raises, type | tuple)
294-
and isinstance(call.excinfo.value, raises)
295-
)
296-
or (
297-
isinstance(raises, AbstractRaises)
298-
and raises.matches(call.excinfo.value)
299-
)
300-
):
301-
rep.outcome = "skipped"
302-
rep.wasxfail = xfailed.reason
303-
else:
304-
rep.outcome = "failed"
290+
# Only apply xfail handling to the "call" phase.
291+
# Setup and teardown failures should be reported as errors,
292+
# not as expected failures, even if the test is marked xfail.
293+
# This ensures that fixture teardown exceptions (e.g., from
294+
# session-scoped fixtures) are properly reported as errors.
295+
if call.when == "call":
296+
raises = xfailed.raises
297+
if raises is None or (
298+
(
299+
isinstance(raises, type | tuple)
300+
and isinstance(call.excinfo.value, raises)
301+
)
302+
or (
303+
isinstance(raises, AbstractRaises)
304+
and raises.matches(call.excinfo.value)
305+
)
306+
):
307+
rep.outcome = "skipped"
308+
rep.wasxfail = xfailed.reason
309+
else:
310+
rep.outcome = "failed"
305311
elif call.when == "call":
306312
if xfailed.strict:
307313
rep.outcome = "failed"

testing/python/fixtures.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4914,7 +4914,8 @@ def test_crash_expected_setup_and_teardown() -> None:
49144914
"""
49154915
)
49164916
result = pytester.runpytest()
4917-
assert result.ret == 0
4917+
# Fixture setup failures are reported as errors, not xfails
4918+
assert result.ret == 1 # Errors from fixture setup failures
49184919

49194920

49204921
def test_scoped_fixture_teardown_order(pytester: Pytester) -> None:

testing/test_skipping.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,11 @@ def test_2():
737737

738738
class TestXFailwithSetupTeardown:
739739
def test_failing_setup_issue9(self, pytester: Pytester) -> None:
740+
"""Setup failures should be reported as errors, not xfails.
741+
742+
Even if a test is marked xfail, if the setup fails, that's an
743+
infrastructure error, not an expected test failure.
744+
"""
740745
pytester.makepyfile(
741746
"""
742747
import pytest
@@ -749,9 +754,14 @@ def test_func():
749754
"""
750755
)
751756
result = pytester.runpytest()
752-
result.stdout.fnmatch_lines(["*1 xfail*"])
757+
result.stdout.fnmatch_lines(["*1 error*"])
753758

754759
def test_failing_teardown_issue9(self, pytester: Pytester) -> None:
760+
"""Teardown failures should be reported as errors, not xfails.
761+
762+
Even if a test is marked xfail, if the teardown fails, that's an
763+
infrastructure error, not an expected test failure.
764+
"""
755765
pytester.makepyfile(
756766
"""
757767
import pytest
@@ -764,7 +774,7 @@ def test_func():
764774
"""
765775
)
766776
result = pytester.runpytest()
767-
result.stdout.fnmatch_lines(["*1 xfail*"])
777+
result.stdout.fnmatch_lines(["*1 error*"])
768778

769779

770780
class TestSkip:
@@ -1185,6 +1195,11 @@ def test_default_markers(pytester: Pytester) -> None:
11851195

11861196

11871197
def test_xfail_test_setup_exception(pytester: Pytester) -> None:
1198+
"""Setup exceptions should be reported as errors, not xfails.
1199+
1200+
Even if a test is marked xfail, if setup fails (via pytest_runtest_setup hook),
1201+
that's an infrastructure error, not an expected test failure.
1202+
"""
11881203
pytester.makeconftest(
11891204
"""
11901205
def pytest_runtest_setup():
@@ -1200,9 +1215,9 @@ def test_func():
12001215
"""
12011216
)
12021217
result = pytester.runpytest(p)
1203-
assert result.ret == 0
1204-
assert "xfailed" in result.stdout.str()
1205-
result.stdout.no_fnmatch_line("*xpassed*")
1218+
assert result.ret == 1 # Should fail due to error
1219+
assert "error" in result.stdout.str()
1220+
result.stdout.no_fnmatch_line("*xfailed*")
12061221

12071222

12081223
def test_imperativeskip_on_xfail_test(pytester: Pytester) -> None:
@@ -1489,3 +1504,37 @@ def test_exit_reason_only():
14891504
)
14901505
result = pytester.runpytest(p)
14911506
result.stdout.fnmatch_lines("*_pytest.outcomes.Exit: foo*")
1507+
1508+
1509+
def test_session_fixture_teardown_exception_with_xfail(pytester: Pytester) -> None:
1510+
"""Test that session fixture teardown exceptions are reported as errors,
1511+
not as duplicate xfails, even when the last test is marked xfail.
1512+
1513+
Regression test for issue #8375.
1514+
"""
1515+
pytester.makepyfile(
1516+
"""
1517+
import pytest
1518+
1519+
@pytest.fixture(autouse=True, scope='session')
1520+
def failme():
1521+
yield
1522+
raise RuntimeError('cleanup fails for some reason')
1523+
1524+
def test_ok():
1525+
assert True
1526+
1527+
@pytest.mark.xfail()
1528+
def test_expected_failure():
1529+
assert False
1530+
"""
1531+
)
1532+
result = pytester.runpytest("-q")
1533+
result.stdout.fnmatch_lines(
1534+
[
1535+
"*1 passed, 1 xfailed, 1 error*",
1536+
]
1537+
)
1538+
# Make sure we don't have duplicate xfails (would be "2 xfailed" before the fix)
1539+
assert "2 xfailed" not in result.stdout.str()
1540+
assert "1 xfailed" in result.stdout.str()

0 commit comments

Comments
 (0)