Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Edson Tadeu M. Manoel
Eduardo Schettino
Edward Haigh
Eero Vaher
Ege Arhan
Eli Boyarski
Elizaveta Shashkova
Éloi Rivard
Expand Down
1 change: 1 addition & 0 deletions changelog/15329.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix BaseExceptionGroup during teardown skipping remaining finalizers (#15329).
2 changes: 1 addition & 1 deletion src/_pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ def teardown_exact(self, nextitem: Item | None) -> None:
fin = finalizers.pop()
try:
fin()
except TEST_OUTCOME as e:
except (*TEST_OUTCOME, BaseExceptionGroup) as e:
these_exceptions.append(e)

if len(these_exceptions) == 1:
Expand Down
62 changes: 62 additions & 0 deletions testing/test_runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# mypy: allow-untyped-defs
from __future__ import annotations

import asyncio
from functools import partial
import inspect
import os
Expand All @@ -23,6 +24,7 @@


if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup
from exceptiongroup import ExceptionGroup


Expand Down Expand Up @@ -141,6 +143,66 @@ def raiser(exc):
assert isinstance(func.exceptions[0], TypeError)
assert isinstance(func.exceptions[1], ValueError)

def test_teardown_base_exception_group(self, pytester: Pytester) -> None:
"""Ensure BaseExceptionGroup during teardown does not abort remaining finalizers (#15329)."""
orders: list[str] = []

def fin_beg() -> None:
orders.append("beg")
raise BaseExceptionGroup("teardown", [asyncio.CancelledError("cancel")])

def fin_ok() -> None:
orders.append("ok")

item = pytester.getitem("def test_func(): pass")
ss = item.session._setupstate
ss.setup(item)
ss.addfinalizer(fin_ok, item)
ss.addfinalizer(fin_beg, item)

with pytest.raises(BaseExceptionGroup) as exc_info:
ss.teardown_exact(None)

assert orders == ["beg", "ok"]
assert exc_info.value.message == "teardown"

def test_teardown_base_exception_group_cleans_fixture_cache(
self, pytester: Pytester
) -> None:
"""Regression test for #15329: BaseExceptionGroup during teardown does not leak cached fixtures."""
pytester.makepyfile(
"""
import asyncio
import pytest

allocations = 0

@pytest.fixture
def owner(request):
global allocations
allocations += 1
token = allocations
yield token

@pytest.fixture
def resource(owner, request):
yield owner
if request.node.name == "test_first":
raise BaseExceptionGroup(
"cleanup",
[asyncio.CancelledError("cancel marker"), RuntimeError("ordinary marker")],
)

def test_first(resource):
assert resource == 1

def test_next(resource):
assert resource == 2
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=2, errors=1)

def test_cached_exception_doesnt_get_longer(self, pytester: Pytester) -> None:
"""Regression test for #12204 (the "BTW" case)."""
pytester.makepyfile(test="")
Expand Down
Loading