Skip to content
Draft
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 @@ -3,6 +3,7 @@ merlinux GmbH, Germany, office at merlinux eu

Contributors include::

0718lol
Aaron Coleman
Abdeali JK
Abdelrahman Elbehery
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 @@
Remaining finalizers now run when a finalizer raises a :class:`BaseExceptionGroup`, preventing function-scoped fixtures from retaining cached values across tests.
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 (OutcomeException, Exception, BaseExceptionGroup) as e:
these_exceptions.append(e)

if len(these_exceptions) == 1:
Expand Down
77 changes: 77 additions & 0 deletions testing/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@


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


Expand Down Expand Up @@ -103,6 +104,82 @@ def fin2():
assert err1.args == ("oops1",)
assert err2.args == ("oops2",)

@pytest.mark.parametrize("exception_type", [GeneratorExit, KeyboardInterrupt])
def test_teardown_base_exception_group_runs_remaining_finalizers(
self, pytester: Pytester, exception_type: type[BaseException]
) -> None:
item = pytester.getitem("def test_func(): pass")
mod = item.listchain()[-2]
ss = item.session._setupstate
ss.setup(item)
finalized = []
error = BaseExceptionGroup(
"cleanup", [exception_type("base marker"), RuntimeError("ordinary marker")]
)

def fail():
raise error

ss.addfinalizer(lambda: finalized.append("module"), mod)
ss.addfinalizer(lambda: finalized.append("function"), item)
ss.addfinalizer(fail, item)
with pytest.raises(BaseExceptionGroup) as exc_info:
ss.teardown_exact(None)
assert exc_info.value is error
assert finalized == ["function", "module"]
assert not ss.stack

@pytest.mark.parametrize("exception_type", ["CancelledError", "GeneratorExit"])
def test_teardown_base_exception_group_does_not_reuse_fixture(
self, pytester: Pytester, exception_type: str
) -> None:
pytester.makepyfile(
f"""
from asyncio import CancelledError
import sys
import pytest

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

events = []
allocations = 0

@pytest.fixture
def owner():
global allocations
allocations += 1
token = allocations
events.append(("allocate", token))
yield token
events.append(("release", token))

@pytest.fixture
def resource(owner, request):
yield owner
if request.node.name == "test_first":
raise BaseExceptionGroup(
"cleanup",
[{exception_type}("base marker"), RuntimeError("ordinary marker")],
)

def test_first(resource):
assert resource == 1

def test_next(resource):
assert resource == 2

def test_releases():
assert events == [
("allocate", 1), ("release", 1),
("allocate", 2), ("release", 2),
]
"""
)
result = pytester.runpytest_subprocess("-q")
result.assert_outcomes(passed=3, errors=1)
result.stdout.fnmatch_lines(["*base marker*", "*ordinary marker*"])

def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
module_teardown = []

Expand Down
Loading