From 5df76411076db978a7e4e23513e27ad8212dbf28 Mon Sep 17 00:00:00 2001 From: Aleksandr Brodin Date: Tue, 26 Mar 2024 11:17:59 +0700 Subject: [PATCH] add exception spec to pytest_fixture_post_finalizer hook --- src/_pytest/fixtures.py | 18 ++++++++---- src/_pytest/hookspec.py | 6 +++- testing/python/fixtures.py | 57 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5303948c4..03b042127 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1023,17 +1023,23 @@ class FixtureDef(Generic[FixtureValue]): except BaseException as e: exceptions.append(e) node = request.node - node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request) + if len(exceptions) == 1: + final_exception = exceptions[0] + elif len(exceptions) > 1: + msg = f'errors while tearing down fixture "{self.argname}" of {node}' + final_exception = BaseExceptionGroup(msg, exceptions[::-1]) + else: + final_exception = None + node.ihook.pytest_fixture_post_finalizer( + fixturedef=self, request=request, exception=final_exception + ) # Even if finalization fails, we invalidate the cached fixture # value and remove all finalizers because they may be bound methods # which will keep instances alive. self.cached_result = None self._finalizers.clear() - if len(exceptions) == 1: - raise exceptions[0] - elif len(exceptions) > 1: - msg = f'errors while tearing down fixture "{self.argname}" of {node}' - raise BaseExceptionGroup(msg, exceptions[::-1]) + if final_exception: + raise final_exception def execute(self, request: SubRequest) -> FixtureValue: """Return the value of this fixture, executing it if not cached.""" diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index db55bd82d..ad01893bc 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -861,7 +861,9 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: "FixtureDef[Any]", request: "SubRequest" + fixturedef: "FixtureDef[Any]", + request: "SubRequest", + exception: "BaseException | None", ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not @@ -871,6 +873,8 @@ def pytest_fixture_post_finalizer( The fixture definition object. :param request: The fixture request object. + :param exception: + The list of exceptions received at the end of the fixtures. Use in conftest plugins ======================= diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 12ca6e926..2fe35800f 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4014,6 +4014,63 @@ def test_pytest_fixture_setup_and_post_finalizer_hook(pytester: Pytester) -> Non ) +def test_exceptions_in_pytest_fixture_setup_and_post_finalizer_hook( + pytester: Pytester, +) -> None: + pytester.makeconftest( + """ + import pytest + @pytest.hookimpl(hookwrapper=True) + def pytest_fixture_setup(fixturedef): + result = yield + print('SETUP EXCEPTION in {0}: {1}'.format(fixturedef.argname, result.exception)) + def pytest_fixture_post_finalizer(fixturedef, exception): + print('TEARDOWN EXCEPTION in {0}: {1}'.format(fixturedef.argname, exception)) + """ + ) + pytester.makepyfile( + **{ + "tests/test_fixture_exceptions.py": """ + import pytest + + @pytest.fixture(scope='module') + def module_teardown_exeption(): + yield + raise ValueError('exeption in module_teardown_exeption') + + @pytest.fixture() + def func_teardown_exeption(): + yield + raise ValueError('exeption in func_teardown_exeption') + + @pytest.fixture() + def func_setup_exeption(): + raise ValueError('exeption in func_setup_exeption') + + @pytest.mark.usefixtures( + 'module_teardown_exeption', + 'func_teardown_exeption', + 'func_setup_exeption', + ) + def test_func(): + pass + """, + } + ) + result = pytester.runpytest("-s") + assert result.ret == 1 + result.stdout.fnmatch_lines( + [ + "*SETUP EXCEPTION in module_teardown_exeption: None*", + "*SETUP EXCEPTION in func_teardown_exeption: None*", + "*SETUP EXCEPTION in func_setup_exeption: exeption in func_setup_exeption*", + "*TEARDOWN EXCEPTION in func_setup_exeption: None*", + "*TEARDOWN EXCEPTION in func_teardown_exeption: exeption in func_teardown_exeption*", + "*TEARDOWN EXCEPTION in module_teardown_exeption: exeption in module_teardown_exeption*", + ] + ) + + class TestScopeOrdering: """Class of tests that ensure fixtures are ordered based on their scopes (#2405)"""