Merge pull request #12048 from bluetech/fixture-teardown-excgroup
fixtures: use exception group when multiple finalizers raise in fixture teardown
This commit is contained in:
commit
f4e10251a4
|
@ -0,0 +1,2 @@
|
||||||
|
When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group.
|
||||||
|
Previously, only the first exception was reported.
|
|
@ -7,6 +7,7 @@ import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
@ -67,6 +68,10 @@ from _pytest.scope import HIGH_SCOPES
|
||||||
from _pytest.scope import Scope
|
from _pytest.scope import Scope
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[:2] < (3, 11):
|
||||||
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Deque
|
from typing import Deque
|
||||||
|
|
||||||
|
@ -1017,27 +1022,25 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
self._finalizers.append(finalizer)
|
self._finalizers.append(finalizer)
|
||||||
|
|
||||||
def finish(self, request: SubRequest) -> None:
|
def finish(self, request: SubRequest) -> None:
|
||||||
exc = None
|
exceptions: List[BaseException] = []
|
||||||
try:
|
while self._finalizers:
|
||||||
while self._finalizers:
|
fin = self._finalizers.pop()
|
||||||
try:
|
try:
|
||||||
func = self._finalizers.pop()
|
fin()
|
||||||
func()
|
except BaseException as e:
|
||||||
except BaseException as e:
|
exceptions.append(e)
|
||||||
# XXX Only first exception will be seen by user,
|
node = request.node
|
||||||
# ideally all should be reported.
|
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||||
if exc is None:
|
# Even if finalization fails, we invalidate the cached fixture
|
||||||
exc = e
|
# value and remove all finalizers because they may be bound methods
|
||||||
if exc:
|
# which will keep instances alive.
|
||||||
raise exc
|
self.cached_result = None
|
||||||
finally:
|
self._finalizers.clear()
|
||||||
ihook = request.node.ihook
|
if len(exceptions) == 1:
|
||||||
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
raise exceptions[0]
|
||||||
# Even if finalization fails, we invalidate the cached fixture
|
elif len(exceptions) > 1:
|
||||||
# value and remove all finalizers because they may be bound methods
|
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
|
||||||
# which will keep instances alive.
|
raise BaseExceptionGroup(msg, exceptions[::-1])
|
||||||
self.cached_result = None
|
|
||||||
self._finalizers.clear()
|
|
||||||
|
|
||||||
def execute(self, request: SubRequest) -> FixtureValue:
|
def execute(self, request: SubRequest) -> FixtureValue:
|
||||||
# Get required arguments and register our own finish()
|
# Get required arguments and register our own finish()
|
||||||
|
|
|
@ -932,8 +932,9 @@ class TestRequestBasic:
|
||||||
self, pytester: Pytester
|
self, pytester: Pytester
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure exceptions raised during teardown by a finalizer are suppressed
|
Ensure exceptions raised during teardown by finalizers are suppressed
|
||||||
until all finalizers are called, re-raising the first exception (#2440)
|
until all finalizers are called, then re-reaised together in an
|
||||||
|
exception group (#2440)
|
||||||
"""
|
"""
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -960,8 +961,16 @@ class TestRequestBasic:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
|
result.assert_outcomes(passed=2, errors=1)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*Exception: Error in excepts fixture", "* 2 passed, 1 error in *"]
|
[
|
||||||
|
' | *ExceptionGroup: errors while tearing down fixture "subrequest" of <Function test_first> (2 sub-exceptions)', # noqa: E501
|
||||||
|
" +-+---------------- 1 ----------------",
|
||||||
|
" | Exception: Error in something fixture",
|
||||||
|
" +---------------- 2 ----------------",
|
||||||
|
" | Exception: Error in excepts fixture",
|
||||||
|
" +------------------------------------",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_request_getmodulepath(self, pytester: Pytester) -> None:
|
def test_request_getmodulepath(self, pytester: Pytester) -> None:
|
||||||
|
|
Loading…
Reference in New Issue