Use exceptiongroup for teardown errors
This commit is contained in:
parent
9e1804a6ee
commit
3a68c08426
|
@ -0,0 +1 @@
|
||||||
|
If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
|
@ -35,6 +35,9 @@ from _pytest.outcomes import OutcomeException
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.outcomes import TEST_OUTCOME
|
from _pytest.outcomes import TEST_OUTCOME
|
||||||
|
|
||||||
|
if sys.version_info[:2] < (3, 11):
|
||||||
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
|
@ -512,22 +515,29 @@ class SetupState:
|
||||||
stack is torn down.
|
stack is torn down.
|
||||||
"""
|
"""
|
||||||
needed_collectors = nextitem and nextitem.listchain() or []
|
needed_collectors = nextitem and nextitem.listchain() or []
|
||||||
exc = None
|
exceptions: List[BaseException] = []
|
||||||
while self.stack:
|
while self.stack:
|
||||||
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
||||||
break
|
break
|
||||||
node, (finalizers, _) = self.stack.popitem()
|
node, (finalizers, _) = self.stack.popitem()
|
||||||
|
these_exceptions = []
|
||||||
while finalizers:
|
while finalizers:
|
||||||
fin = finalizers.pop()
|
fin = finalizers.pop()
|
||||||
try:
|
try:
|
||||||
fin()
|
fin()
|
||||||
except TEST_OUTCOME as e:
|
except TEST_OUTCOME as e:
|
||||||
# XXX Only first exception will be seen by user,
|
these_exceptions.append(e)
|
||||||
# ideally all should be reported.
|
|
||||||
if exc is None:
|
if len(these_exceptions) == 1:
|
||||||
exc = e
|
exceptions.extend(these_exceptions)
|
||||||
if exc:
|
elif these_exceptions:
|
||||||
raise exc
|
msg = f"errors while tearing down {node!r}"
|
||||||
|
exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1]))
|
||||||
|
|
||||||
|
if len(exceptions) == 1:
|
||||||
|
raise exceptions[0]
|
||||||
|
elif exceptions:
|
||||||
|
raise BaseExceptionGroup("errors during test teardown", exceptions[::-1])
|
||||||
if nextitem is None:
|
if nextitem is None:
|
||||||
assert not self.stack
|
assert not self.stack
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -19,6 +20,9 @@ from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.outcomes import OutcomeException
|
from _pytest.outcomes import OutcomeException
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
|
||||||
|
if sys.version_info[:2] < (3, 11):
|
||||||
|
from exceptiongroup import ExceptionGroup
|
||||||
|
|
||||||
|
|
||||||
class TestSetupState:
|
class TestSetupState:
|
||||||
def test_setup(self, pytester: Pytester) -> None:
|
def test_setup(self, pytester: Pytester) -> None:
|
||||||
|
@ -77,8 +81,6 @@ class TestSetupState:
|
||||||
assert r == ["fin3", "fin1"]
|
assert r == ["fin3", "fin1"]
|
||||||
|
|
||||||
def test_teardown_multiple_fail(self, pytester: Pytester) -> None:
|
def test_teardown_multiple_fail(self, pytester: Pytester) -> None:
|
||||||
# Ensure the first exception is the one which is re-raised.
|
|
||||||
# Ideally both would be reported however.
|
|
||||||
def fin1():
|
def fin1():
|
||||||
raise Exception("oops1")
|
raise Exception("oops1")
|
||||||
|
|
||||||
|
@ -90,9 +92,14 @@ class TestSetupState:
|
||||||
ss.setup(item)
|
ss.setup(item)
|
||||||
ss.addfinalizer(fin1, item)
|
ss.addfinalizer(fin1, item)
|
||||||
ss.addfinalizer(fin2, item)
|
ss.addfinalizer(fin2, item)
|
||||||
with pytest.raises(Exception) as err:
|
with pytest.raises(ExceptionGroup) as err:
|
||||||
ss.teardown_exact(None)
|
ss.teardown_exact(None)
|
||||||
assert err.value.args == ("oops2",)
|
|
||||||
|
# Note that finalizers are run LIFO, but because FIFO is more intuitive for
|
||||||
|
# users we reverse the order of messages, and see the error from fin1 first.
|
||||||
|
err1, err2 = err.value.exceptions
|
||||||
|
assert err1.args == ("oops1",)
|
||||||
|
assert err2.args == ("oops2",)
|
||||||
|
|
||||||
def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
|
def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None:
|
||||||
module_teardown = []
|
module_teardown = []
|
||||||
|
@ -113,6 +120,25 @@ class TestSetupState:
|
||||||
ss.teardown_exact(None)
|
ss.teardown_exact(None)
|
||||||
assert module_teardown == ["fin_module"]
|
assert module_teardown == ["fin_module"]
|
||||||
|
|
||||||
|
def test_teardown_multiple_scopes_several_fail(self, pytester) -> None:
|
||||||
|
def raiser(exc):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
item = pytester.getitem("def test_func(): pass")
|
||||||
|
mod = item.listchain()[-2]
|
||||||
|
ss = item.session._setupstate
|
||||||
|
ss.setup(item)
|
||||||
|
ss.addfinalizer(partial(raiser, KeyError("from module scope")), mod)
|
||||||
|
ss.addfinalizer(partial(raiser, TypeError("from function scope 1")), item)
|
||||||
|
ss.addfinalizer(partial(raiser, ValueError("from function scope 2")), item)
|
||||||
|
|
||||||
|
with pytest.raises(ExceptionGroup, match="errors during test teardown") as e:
|
||||||
|
ss.teardown_exact(None)
|
||||||
|
mod, func = e.value.exceptions
|
||||||
|
assert isinstance(mod, KeyError)
|
||||||
|
assert isinstance(func.exceptions[0], TypeError) # type: ignore
|
||||||
|
assert isinstance(func.exceptions[1], ValueError) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class BaseFunctionalTests:
|
class BaseFunctionalTests:
|
||||||
def test_passfunction(self, pytester: Pytester) -> None:
|
def test_passfunction(self, pytester: Pytester) -> None:
|
||||||
|
|
Loading…
Reference in New Issue