This commit is contained in:
Aleksandr Brodin 2024-06-20 10:47:51 +02:00 committed by GitHub
commit c3b2ec7221
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 19 deletions

View File

@ -785,6 +785,8 @@ Session related reporting hooks:
.. autofunction:: pytest_terminal_summary
.. hook:: pytest_fixture_setup
.. autofunction:: pytest_fixture_setup
.. hook:: pytest_fixture_teardown
.. autofunction:: pytest_fixture_teardown
.. hook:: pytest_fixture_post_finalizer
.. autofunction:: pytest_fixture_post_finalizer
.. hook:: pytest_warning_recorded

View File

@ -1015,25 +1015,8 @@ class FixtureDef(Generic[FixtureValue]):
self._finalizers.append(finalizer)
def finish(self, request: SubRequest) -> None:
exceptions: List[BaseException] = []
while self._finalizers:
fin = self._finalizers.pop()
try:
fin()
except BaseException as e:
exceptions.append(e)
node = request.node
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
# 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])
node.ihook.pytest_fixture_teardown(fixturedef=self, request=request)
def execute(self, request: SubRequest) -> FixtureValue:
"""Return the value of this fixture, executing it if not cached."""
@ -1145,6 +1128,30 @@ def pytest_fixture_setup(
return result
def pytest_fixture_teardown(
fixturedef: FixtureDef[FixtureValue], request: SubRequest
) -> None:
exceptions: List[BaseException] = []
while fixturedef._finalizers:
fin = fixturedef._finalizers.pop()
try:
fin()
except BaseException as e:
exceptions.append(e)
node = request.node
node.ihook.pytest_fixture_post_finalizer(fixturedef=fixturedef, request=request)
# 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.
fixturedef.cached_result = None
fixturedef._finalizers.clear()
if len(exceptions) == 1:
raise exceptions[0]
elif len(exceptions) > 1:
msg = f'errors while tearing down fixture "{fixturedef.argname}" of {node}'
raise BaseExceptionGroup(msg, exceptions[::-1])
def wrap_function_to_error_out_if_called_directly(
function: FixtureFunction,
fixture_marker: "FixtureFunctionMarker",

View File

@ -889,8 +889,28 @@ def pytest_fixture_setup(
"""
def pytest_fixture_post_finalizer(
def pytest_fixture_teardown(
fixturedef: "FixtureDef[Any]", request: "SubRequest"
) -> None:
"""Perform fixture teardown execution.
:param fixturdef:
The fixture definition object.
:param request:
The fixture request object.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
"""
def pytest_fixture_post_finalizer(
fixturedef: "FixtureDef[Any]",
request: "SubRequest",
) -> None:
"""Called after fixture teardown, but before the cache is cleared, so
the fixture result ``fixturedef.cached_result`` is still available (not

View File

@ -4054,6 +4054,65 @@ def test_pytest_fixture_setup_and_post_finalizer_hook(pytester: Pytester) -> Non
)
def test_exceptions_in_pytest_fixture_setup_and_pytest_fixture_teardown(
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))
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_teardown(fixturedef):
result = yield
print('TEARDOWN EXCEPTION in {0}: {1}'.format(fixturedef.argname, result.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)"""