diff --git a/AUTHORS b/AUTHORS index ba5672d4c51..6633356e06d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -311,6 +311,7 @@ Mark Dickinson Mark Vong Marko Pacak Markus Unterwaditzer +Marthala Jagruthi Reddy Martijn Faassen Martin Altmayer Martin K. Scherer diff --git a/changelog/14800.bugfix.rst b/changelog/14800.bugfix.rst new file mode 100644 index 00000000000..6b7bb8aedd6 --- /dev/null +++ b/changelog/14800.bugfix.rst @@ -0,0 +1,2 @@ +Fixes an internal assertion when a parametrized fixture setup fails in a +``pytest_fixture_setup`` hook, allowing subsequent parameters to run normally. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 05537ec01b2..fa2a0eee884 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1205,9 +1205,9 @@ 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. + if self.cached_result is None and not self._finalizers: + # Already finished. If finalizers are present, fixture setup failed + # before the result could be cached, so they still need to run. return exceptions: list[BaseException] = [] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3983bd0007d..6d6626c5d65 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1521,6 +1521,38 @@ def test_foo(async_fixture): ) +def test_sync_tests_with_async_autouse_fixture_do_not_poison_later_tests( + pytester: Pytester, +) -> None: + pytester.makepyfile( + test_sync=""" + import pytest + + @pytest.fixture(autouse=True) + async def async_fixture(): + yield + + def test_first(): + pass + + def test_second(): + pass + + def test_third(): + pass + """ + ) + result = pytester.runpytest() + result.assert_outcomes(errors=3) + result.stdout.fnmatch_lines( + [ + "*'test_first' requested an async fixture 'async_fixture' with autouse=True, *", + "*'test_second' requested an async fixture 'async_fixture' with autouse=True, *", + "*'test_third' requested an async fixture 'async_fixture' with autouse=True, *", + ] + ) + + def test_pdb_can_be_rewritten(pytester: Pytester) -> None: pytester.makepyfile( **{ diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c0b49948152..663eb6a597d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4536,6 +4536,42 @@ def test_second(my_fixture): ) +def test_fixture_setup_hook_exception_does_not_leave_stale_finalizers( + pytester: Pytester, +) -> None: + """A fixture setup hook failure must not poison later parametrized cases (#14800).""" + pytester.makeconftest( + """ + import pytest + + @pytest.hookimpl(tryfirst=True) + def pytest_fixture_setup(fixturedef, request): + param = getattr(request, "param", None) + if isinstance(param, str) and param.startswith("fixture:"): + request.param = request.getfixturevalue(param[len("fixture:"):]) + """ + ) + pytester.makepyfile( + test_fixtures=""" + import pytest + + @pytest.fixture + def skipping_base(): + pytest.skip("backend unavailable") + + @pytest.fixture + def derived(skipping_base): + return "derived" + + @pytest.mark.parametrize("value", ["fixture:derived", "plain-1", "plain-2"]) + def test_value(value): + assert isinstance(value, str) + """ + ) + result = pytester.runpytest("-v") + result.assert_outcomes(passed=2, skipped=1) + + class TestParamValueKey: """Unit tests for the equivalence key used by `reorder_items` (#8914)."""