Skip to content

Commit 12b9bd5

Browse files
bbrown1867bluetech
andauthored
Fix teardown error reporting when --maxfail=1 (pytest-dev#11721)
Co-authored-by: Ran Benita <[email protected]>
1 parent f017df4 commit 12b9bd5

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
@@ -44,6 +45,7 @@
4445
from _pytest.reports import TestReport
4546
from _pytest.runner import collect_one_node
4647
from _pytest.runner import SetupState
48+
from _pytest.warning_types import PytestWarning
4749

4850

4951
def pytest_addoption(parser: Parser) -> None:
@@ -548,8 +550,8 @@ def __init__(self, config: Config) -> None:
548550
)
549551
self.testsfailed = 0
550552
self.testscollected = 0
551-
self.shouldstop: Union[bool, str] = False
552-
self.shouldfail: Union[bool, str] = False
553+
self._shouldstop: Union[bool, str] = False
554+
self._shouldfail: Union[bool, str] = False
553555
self.trace = config.trace.root.get("collection")
554556
self._initialpaths: FrozenSet[Path] = frozenset()
555557
self._initialpaths_with_parents: FrozenSet[Path] = frozenset()
@@ -576,6 +578,42 @@ def __repr__(self) -> str:
576578
self.testscollected,
577579
)
578580

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