diff --git a/changelog/15067.bugfix.rst b/changelog/15067.bugfix.rst new file mode 100644 index 00000000000..21880ca68c5 --- /dev/null +++ b/changelog/15067.bugfix.rst @@ -0,0 +1,3 @@ +Fixture finalizers registered with ``request.addfinalizer()`` now run even if +fixture setup is interrupted after registration, including by +``KeyboardInterrupt`` or an exception from a ``pytest_fixture_setup`` hook. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7656fca2f5b..c394272a198 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1203,9 +1203,8 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._finalizers.append(finalizer) def finish(self, request: SubRequest) -> None: - if self.cached_result is None: - # Already finished. It is assumed that finalizers cannot be added in - # this state. + # Setup may have failed before caching a result, but still added finalizers. + if self.cached_result is None and not self._finalizers: return exceptions: list[BaseException] = [] diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8e779a93d76..26a84a45c19 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1091,6 +1091,60 @@ def test_finalizer_ran(): reprec = pytester.inline_run("-s") reprec.assertoutcome(failed=1, passed=1) + def test_request_addfinalizer_interrupted_setup(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + from pathlib import Path + import pytest + + @pytest.fixture + def resource(request): + request.addfinalizer( + lambda: Path("finalizer-ran").write_text("ran", encoding="utf-8") + ) + raise KeyboardInterrupt + + def test_setup(resource): + pass + """ + ) + + result = pytester.runpytest_subprocess("-q") + assert result.ret == ExitCode.INTERRUPTED + assert (pytester.path / "finalizer-ran").read_text(encoding="utf-8") == "ran" + + def test_request_addfinalizer_interrupted_setup_hook( + self, pytester: Pytester + ) -> None: + pytester.makeconftest( + """ + from pathlib import Path + + def pytest_fixture_setup(fixturedef, request): + if fixturedef.argname == "resource": + request.addfinalizer( + lambda: Path("finalizer-ran").write_text("ran", encoding="utf-8") + ) + raise KeyboardInterrupt + """ + ) + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def resource(): + pass + + def test_setup(resource): + pass + """ + ) + + result = pytester.runpytest_subprocess("-q") + assert result.ret == ExitCode.INTERRUPTED + assert (pytester.path / "finalizer-ran").read_text(encoding="utf-8") == "ran" + def test_request_addfinalizer_failing_setup_module( self, pytester: Pytester ) -> None: