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 changelog/15067.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug where fixture finalizers were skipped if fixture setup was interrupted before ``cached_result`` was set.
16 changes: 10 additions & 6 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self._finalizers.append(finalizer)

def finish(self, request: SubRequest) -> None:
if self.cached_result is None:
if self.cached_result is None and not self._finalizers:
# Already finished. It is assumed that finalizers cannot be added in
# this state.
return
Expand All @@ -1216,6 +1216,7 @@ def finish(self, request: SubRequest) -> None:
fin()
except BaseException as e:
exceptions.append(e)

node = request.node
# Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods
Expand Down Expand Up @@ -1279,11 +1280,14 @@ def execute(self, request: SubRequest) -> FixtureValue:
# Register the pytest_fixture_post_finalizer as the first finalizer,
# which is executed last.
assert not self._finalizers
self.addfinalizer(
lambda: request.node.ihook.pytest_fixture_post_finalizer(
fixturedef=self, request=request
)
)

def post_finalizer() -> None:
if self.cached_result is not None:
request.node.ihook.pytest_fixture_post_finalizer(
fixturedef=self, request=request
)

self.addfinalizer(post_finalizer)

ihook = request.node.ihook
try:
Expand Down
75 changes: 75 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6278,3 +6278,78 @@ def test_fixt(self, fixt):
)
result.stdout.fnmatch_lines(["*fixture 'fixt' not found*"])
result.assert_outcomes(errors=1)


def test_fixture_finalizer_interrupt(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
from pathlib import Path

MARKER = Path(__file__).with_name("finalizer-ran")

@pytest.fixture
def resource(request):
request.addfinalizer(lambda: MARKER.write_text("ran", encoding="utf-8"))
raise KeyboardInterrupt
"""
)
pytester.makepyfile(
"""
def test_setup(resource):
pass
"""
)
result = pytester.runpytest_subprocess()
assert result.ret == 2
assert (pytester.path / "finalizer-ran").exists()


def test_fixture_post_finalizer_skipped_on_setup_failure(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
from pathlib import Path

@pytest.fixture
def broken_fixture(request):
request.addfinalizer(lambda: print("USER_FINALIZER_RAN"))
raise SystemExit("Setup interrupted!")

def pytest_fixture_post_finalizer(fixturedef, request):
print(f"POST_FINALIZER_RAN:{fixturedef.argname}:{fixturedef.cached_result is not None}")
"""
)
pytester.makepyfile(
"""
def test_setup(broken_fixture):
pass
"""
)
result = pytester.runpytest("-s")
result.stdout.fnmatch_lines(["*USER_FINALIZER_RAN*"])
assert "POST_FINALIZER_RAN:broken_fixture" not in result.stdout.str()
assert "POST_FINALIZER_RAN:broken_fixture:False" not in result.stdout.str()


def test_fixture_post_finalizer_adds_finalizer(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest

@pytest.fixture
def my_fixture(request):
pass

def pytest_fixture_post_finalizer(fixturedef, request):
request.addfinalizer(lambda: print("DYNAMIC_FINALIZER_RAN"))
"""
)
pytester.makepyfile(
"""
def test_dynamic_finalizer(my_fixture):
pass
"""
)
result = pytester.runpytest("-s")
result.stdout.fnmatch_lines(["*DYNAMIC_FINALIZER_RAN*"])
Loading