Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/13650.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed an issue where internal exceptions like those raised from :func:`pytest.exit` would not be correctly handled during test teardown.
22 changes: 19 additions & 3 deletions src/_pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,26 @@ def from_call(
instant = timing.Instant()
try:
result: TResult | None = func()
except BaseException:
except BaseException as caught:
excinfo = ExceptionInfo.from_current()
if reraise is not None and isinstance(excinfo.value, reraise):
raise
val = excinfo.value

if reraise is not None:
reraise_types = (
(reraise,) if not isinstance(reraise, tuple) else reraise
)

# ExceptionGroup-aware path: check if any of the direct children
# is an instance of the `reraise` parameter, and reraise the exception
# accordingly (#13650).
if isinstance(val, BaseExceptionGroup):
for child in val.exceptions:
if isinstance(child, reraise_types):
raise child from caught
# Not an exception group, check if we need to reraise it.
elif isinstance(val, reraise_types):
raise

result = None
duration = instant.elapsed()
return cls(
Expand Down
31 changes: 31 additions & 0 deletions testing/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1264,3 +1264,34 @@ def test_bar(): pass
)
result = pytester.runpytest("--stepwise")
result.assert_outcomes(failed=1, errors=1)


def test_exit_in_teardown_exception_group_stops_session(pytester: Pytester) -> None:
pytester.makepyfile(
test_it="""
import pytest
@pytest.fixture
def failing_teardown():
yield
raise IOError("Exception in teardown")
@pytest.fixture
def exit_session():
yield
pytest.exit("Forced exit")
def test_1(): return
@pytest.mark.usefixtures(
"failing_teardown",
"exit_session"
)
def test_failure(): return
def test_3(): return
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=2)
result.stdout.fnmatch_lines(
[
"!* _pytest.outcomes.Exit: Forced exit !*",
"=* 2 passed in * =*",
]
)