Use ExceptionGroup instead of printing, improve changelog

This commit is contained in:
Bruno Oliveira 2024-04-26 20:10:32 -03:00
parent e209a3db15
commit 122fd05e1e
3 changed files with 102 additions and 75 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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: