Skip to content

Commit 6123b24

Browse files
authored
Merge pull request #11764 from pytest-dev/backport-11721-to-8.0.x
[8.0.x] Fix teardown error reporting when `--maxfail=1`
2 parents bb6f5d1 + 620a454 commit 6123b24

File tree

6 files changed

+156
-2
lines changed

6 files changed

+156
-2
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Aviral Verma
5454
Aviv Palivoda
5555
Babak Keyvani
5656
Barney Gale
57+
Ben Brown
5758
Ben Gartner
5859
Ben Webb
5960
Benjamin Peterson

changelog/11706.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.

src/_pytest/main.py

+40-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import importlib
77
import os
88
import sys
9+
import warnings
910
from pathlib import Path
1011
from typing import AbstractSet
1112
from typing import Callable
@@ -45,6 +46,7 @@
4546
from _pytest.reports import TestReport
4647
from _pytest.runner import collect_one_node
4748
from _pytest.runner import SetupState
49+
from _pytest.warning_types import PytestWarning
4850

4951

5052
def pytest_addoption(parser: Parser) -> None:
@@ -550,8 +552,8 @@ def __init__(self, config: Config) -> None:
550552
)
551553
self.testsfailed = 0
552554
self.testscollected = 0
553-
self.shouldstop: Union[bool, str] = False
554-
self.shouldfail: Union[bool, str] = False
555+
self._shouldstop: Union[bool, str] = False
556+
self._shouldfail: Union[bool, str] = False
555557
self.trace = config.trace.root.get("collection")
556558
self._initialpaths: FrozenSet[Path] = frozenset()
557559
self._initialpaths_with_parents: FrozenSet[Path] = frozenset()
@@ -578,6 +580,42 @@ def __repr__(self) -> str:
578580
self.testscollected,
579581
)
580582

583+
@property
584+
def shouldstop(self) -> Union[bool, str]:
585+
return self._shouldstop
586+
587+
@shouldstop.setter
588+
def shouldstop(self, value: Union[bool, str]) -> None:
589+
# The runner checks shouldfail and assumes that if it is set we are
590+
# definitely stopping, so prevent unsetting it.
591+
if value is False and self._shouldstop:
592+
warnings.warn(
593+
PytestWarning(
594+
"session.shouldstop cannot be unset after it has been set; ignoring."
595+
),
596+
stacklevel=2,
597+
)
598+
return
599+
self._shouldstop = value
600+
601+
@property
602+
def shouldfail(self) -> Union[bool, str]:
603+
return self._shouldfail
604+
605+
@shouldfail.setter
606+
def shouldfail(self, value: Union[bool, str]) -> None:
607+
# The runner checks shouldfail and assumes that if it is set we are
608+
# definitely stopping, so prevent unsetting it.
609+
if value is False and self._shouldfail:
610+
warnings.warn(
611+
PytestWarning(
612+
"session.shouldfail cannot be unset after it has been set; ignoring."
613+
),
614+
stacklevel=2,
615+
)
616+
return
617+
self._shouldfail = value
618+
581619
@property
582620
def startpath(self) -> Path:
583621
"""The path from which pytest was invoked.

src/_pytest/runner.py

+4
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ def runtestprotocol(
131131
show_test_item(item)
132132
if not item.config.getoption("setuponly", False):
133133
reports.append(call_and_report(item, "call", log))
134+
# If the session is about to fail or stop, teardown everything - this is
135+
# necessary to correctly report fixture teardown errors (see #11706)
136+
if item.session.shouldfail or item.session.shouldstop:
137+
nextitem = None
134138
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
135139
# After all teardown hooks have been called
136140
# want funcargs and request info to go away.

testing/test_runner.py

+50
Original file line numberDiff line numberDiff line change
@@ -1087,3 +1087,53 @@ def func() -> None:
10871087
with pytest.raises(TypeError) as excinfo:
10881088
OutcomeException(func) # type: ignore
10891089
assert str(excinfo.value) == expected
1090+
1091+
1092+
def test_teardown_session_failed(pytester: Pytester) -> None:
1093+
"""Test that higher-scoped fixture teardowns run in the context of the last
1094+
item after the test session bails early due to --maxfail.
1095+
1096+
Regression test for #11706.
1097+
"""
1098+
pytester.makepyfile(
1099+
"""
1100+
import pytest
1101+
1102+
@pytest.fixture(scope="module")
1103+
def baz():
1104+
yield
1105+
pytest.fail("This is a failing teardown")
1106+
1107+
def test_foo(baz):
1108+
pytest.fail("This is a failing test")
1109+
1110+
def test_bar(): pass
1111+
"""
1112+
)
1113+
result = pytester.runpytest("--maxfail=1")
1114+
result.assert_outcomes(failed=1, errors=1)
1115+
1116+
1117+
def test_teardown_session_stopped(pytester: Pytester) -> None:
1118+
"""Test that higher-scoped fixture teardowns run in the context of the last
1119+
item after the test session bails early due to --stepwise.
1120+
1121+
Regression test for #11706.
1122+
"""
1123+
pytester.makepyfile(
1124+
"""
1125+
import pytest
1126+
1127+
@pytest.fixture(scope="module")
1128+
def baz():
1129+
yield
1130+
pytest.fail("This is a failing teardown")
1131+
1132+
def test_foo(baz):
1133+
pytest.fail("This is a failing test")
1134+
1135+
def test_bar(): pass
1136+
"""
1137+
)
1138+
result = pytester.runpytest("--stepwise")
1139+
result.assert_outcomes(failed=1, errors=1)

testing/test_session.py

+60
Original file line numberDiff line numberDiff line change
@@ -418,3 +418,63 @@ def test_rootdir_wrong_option_arg(pytester: Pytester) -> None:
418418
result.stderr.fnmatch_lines(
419419
["*Directory *wrong_dir* not found. Check your '--rootdir' option.*"]
420420
)
421+
422+
423+
def test_shouldfail_is_sticky(pytester: Pytester) -> None:
424+
"""Test that session.shouldfail cannot be reset to False after being set.
425+
426+
Issue #11706.
427+
"""
428+
pytester.makeconftest(
429+
"""
430+
def pytest_sessionfinish(session):
431+
assert session.shouldfail
432+
session.shouldfail = False
433+
assert session.shouldfail
434+
"""
435+
)
436+
pytester.makepyfile(
437+
"""
438+
import pytest
439+
440+
def test_foo():
441+
pytest.fail("This is a failing test")
442+
443+
def test_bar(): pass
444+
"""
445+
)
446+
447+
result = pytester.runpytest("--maxfail=1", "-Wall")
448+
449+
result.assert_outcomes(failed=1, warnings=1)
450+
result.stdout.fnmatch_lines("*session.shouldfail cannot be unset*")
451+
452+
453+
def test_shouldstop_is_sticky(pytester: Pytester) -> None:
454+
"""Test that session.shouldstop cannot be reset to False after being set.
455+
456+
Issue #11706.
457+
"""
458+
pytester.makeconftest(
459+
"""
460+
def pytest_sessionfinish(session):
461+
assert session.shouldstop
462+
session.shouldstop = False
463+
assert session.shouldstop
464+
"""
465+
)
466+
pytester.makepyfile(
467+
"""
468+
import pytest
469+
470+
def test_foo():
471+
pytest.fail("This is a failing test")
472+
473+
def test_bar(): pass
474+
"""
475+
)
476+
477+
result = pytester.runpytest("--stepwise", "-Wall")
478+
479+
result.assert_outcomes(failed=1, warnings=1)
480+
result.stdout.fnmatch_lines("*session.shouldstop cannot be unset*")

0 commit comments

Comments
 (0)