Use ExceptionGroup instead of printing, improve changelog
This commit is contained in:
parent
e209a3db15
commit
122fd05e1e
|
@ -1 +1 @@
|
||||||
Class cleanup exceptions are now reported.
|
For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup <unittest.TestCase.addClassCleanup>`) are now reported instead of silently failing.
|
||||||
|
|
|
@ -32,6 +32,9 @@ from _pytest.runner import CallInfo
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[:2] < (3, 11):
|
||||||
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
@ -111,18 +114,19 @@ class UnitTestCase(Class):
|
||||||
return None
|
return None
|
||||||
cleanup = getattr(cls, "doClassCleanups", lambda: None)
|
cleanup = getattr(cls, "doClassCleanups", lambda: None)
|
||||||
|
|
||||||
def process_teardown_exceptions(raise_last: bool):
|
def process_teardown_exceptions() -> None:
|
||||||
errors = getattr(cls, "tearDown_exceptions", None)
|
# tearDown_exceptions is a list set in the class containing exc_infos for errors during
|
||||||
if not errors:
|
# teardown for the class.
|
||||||
|
exc_infos = getattr(cls, "tearDown_exceptions", None)
|
||||||
|
if not exc_infos:
|
||||||
return
|
return
|
||||||
others = errors[:-1] if raise_last else errors
|
exceptions = [exc for (_, exc, _) in exc_infos]
|
||||||
if others:
|
# If a single exception, raise it directly as this provides a more readable
|
||||||
num = len(errors)
|
# error.
|
||||||
for n, (exc_type, exc, tb) in enumerate(others, start=1):
|
if len(exceptions) == 1:
|
||||||
print(f"\nclass cleanup error ({n} of {num}):", file=sys.stderr)
|
raise exceptions[0]
|
||||||
traceback.print_exception(exc_type, exc, tb)
|
else:
|
||||||
if raise_last:
|
raise BaseExceptionGroup("Unittest class cleanup errors", exceptions)
|
||||||
raise errors[-1][1]
|
|
||||||
|
|
||||||
def unittest_setup_class_fixture(
|
def unittest_setup_class_fixture(
|
||||||
request: FixtureRequest,
|
request: FixtureRequest,
|
||||||
|
@ -138,7 +142,7 @@ class UnitTestCase(Class):
|
||||||
# follow this here.
|
# follow this here.
|
||||||
except Exception:
|
except Exception:
|
||||||
cleanup()
|
cleanup()
|
||||||
process_teardown_exceptions(raise_last=False)
|
process_teardown_exceptions()
|
||||||
raise
|
raise
|
||||||
yield
|
yield
|
||||||
try:
|
try:
|
||||||
|
@ -146,7 +150,7 @@ class UnitTestCase(Class):
|
||||||
teardown()
|
teardown()
|
||||||
finally:
|
finally:
|
||||||
cleanup()
|
cleanup()
|
||||||
process_teardown_exceptions(raise_last=True)
|
process_teardown_exceptions()
|
||||||
|
|
||||||
self.session._fixturemanager._register_fixture(
|
self.session._fixturemanager._register_fixture(
|
||||||
# Use a unique name to speed up lookup.
|
# Use a unique name to speed up lookup.
|
||||||
|
|
|
@ -1500,70 +1500,93 @@ def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None:
|
||||||
assert passed == 1
|
assert passed == 1
|
||||||
|
|
||||||
|
|
||||||
def test_class_cleanups_failure_in_setup(pytester: Pytester) -> None:
|
class TestClassCleanupErrors:
|
||||||
testpath = pytester.makepyfile(
|
|
||||||
"""
|
|
||||||
import unittest
|
|
||||||
class MyTestCase(unittest.TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
def cleanup(n):
|
|
||||||
raise Exception(f"fail {n}")
|
|
||||||
cls.addClassCleanup(cleanup, 2)
|
|
||||||
cls.addClassCleanup(cleanup, 1)
|
|
||||||
raise Exception("fail 0")
|
|
||||||
def test(self):
|
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
)
|
Make sure to show exceptions raised during class cleanup function (those registered
|
||||||
result = pytester.runpytest("-s", testpath)
|
via addClassCleanup()).
|
||||||
result.assert_outcomes(passed=0, errors=1)
|
|
||||||
result.stderr.fnmatch_lines(
|
|
||||||
[
|
|
||||||
"class cleanup error (1 of 2):",
|
|
||||||
"Exception: fail 1",
|
|
||||||
"class cleanup error (2 of 2):",
|
|
||||||
"Exception: fail 2",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
result.stdout.fnmatch_lines(
|
|
||||||
[
|
|
||||||
"* ERROR at setup of MyTestCase.test *",
|
|
||||||
"E * Exception: fail 0",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
See #11728.
|
||||||
def test_class_cleanups_failure_in_teardown(pytester: Pytester) -> None:
|
|
||||||
testpath = pytester.makepyfile(
|
|
||||||
"""
|
|
||||||
import unittest
|
|
||||||
class MyTestCase(unittest.TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
def cleanup(n):
|
|
||||||
raise Exception(f"fail {n}")
|
|
||||||
cls.addClassCleanup(cleanup, 2)
|
|
||||||
cls.addClassCleanup(cleanup, 1)
|
|
||||||
def test(self):
|
|
||||||
pass
|
|
||||||
"""
|
"""
|
||||||
)
|
|
||||||
result = pytester.runpytest("-s", testpath)
|
def test_class_cleanups_failure_in_setup(self, pytester: Pytester) -> None:
|
||||||
result.assert_outcomes(passed=1, errors=1)
|
testpath = pytester.makepyfile(
|
||||||
result.stderr.fnmatch_lines(
|
"""
|
||||||
[
|
import unittest
|
||||||
"class cleanup error (1 of 2):",
|
class MyTestCase(unittest.TestCase):
|
||||||
"Traceback *",
|
@classmethod
|
||||||
"Exception: fail 1",
|
def setUpClass(cls):
|
||||||
]
|
def cleanup(n):
|
||||||
)
|
raise Exception(f"fail {n}")
|
||||||
result.stdout.fnmatch_lines(
|
cls.addClassCleanup(cleanup, 2)
|
||||||
[
|
cls.addClassCleanup(cleanup, 1)
|
||||||
"* ERROR at teardown of MyTestCase.test *",
|
raise Exception("fail 0")
|
||||||
"E * Exception: fail 2",
|
def test(self):
|
||||||
]
|
pass
|
||||||
)
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("-s", testpath)
|
||||||
|
result.assert_outcomes(passed=0, errors=1)
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*Unittest class cleanup errors *2 sub-exceptions*",
|
||||||
|
"*Exception: fail 1",
|
||||||
|
"*Exception: fail 2",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"* ERROR at setup of MyTestCase.test *",
|
||||||
|
"E * Exception: fail 0",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_class_cleanups_failure_in_teardown(self, pytester: Pytester) -> None:
|
||||||
|
testpath = pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
def cleanup(n):
|
||||||
|
raise Exception(f"fail {n}")
|
||||||
|
cls.addClassCleanup(cleanup, 2)
|
||||||
|
cls.addClassCleanup(cleanup, 1)
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("-s", testpath)
|
||||||
|
result.assert_outcomes(passed=1, errors=1)
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*Unittest class cleanup errors *2 sub-exceptions*",
|
||||||
|
"*Exception: fail 1",
|
||||||
|
"*Exception: fail 2",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_class_cleanup_1_failure_in_teardown(self, pytester: Pytester) -> None:
|
||||||
|
testpath = pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
def cleanup(n):
|
||||||
|
raise Exception(f"fail {n}")
|
||||||
|
cls.addClassCleanup(cleanup, 1)
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("-s", testpath)
|
||||||
|
result.assert_outcomes(passed=1, errors=1)
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
[
|
||||||
|
"*ERROR at teardown of MyTestCase.test*",
|
||||||
|
"*Exception: fail 1",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_traceback_pruning(pytester: Pytester) -> None:
|
def test_traceback_pruning(pytester: Pytester) -> None:
|
||||||
|
|
Loading…
Reference in New Issue