unittest: report class cleanup exceptions (#12250)

Fixes #11728

---------

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
This commit is contained in:
Daniel Miller 2024-04-27 08:49:05 -04:00 committed by GitHub
parent d208c1d4a5
commit 7e7503c0b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 110 additions and 0 deletions

View File

@ -101,6 +101,7 @@ Cyrus Maden
Damian Skrzypczak Damian Skrzypczak
Daniel Grana Daniel Grana
Daniel Hahler Daniel Hahler
Daniel Miller
Daniel Nuri Daniel Nuri
Daniel Sánchez Castelló Daniel Sánchez Castelló
Daniel Valenzuela Zenteno Daniel Valenzuela Zenteno

View File

@ -0,0 +1 @@
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 ExceptionGroup
if TYPE_CHECKING: if TYPE_CHECKING:
import unittest import unittest
@ -111,6 +114,20 @@ class UnitTestCase(Class):
return None return None
cleanup = getattr(cls, "doClassCleanups", lambda: None) cleanup = getattr(cls, "doClassCleanups", lambda: None)
def process_teardown_exceptions() -> None:
# tearDown_exceptions is a list set in the class containing exc_infos for errors during
# teardown for the class.
exc_infos = getattr(cls, "tearDown_exceptions", None)
if not exc_infos:
return
exceptions = [exc for (_, exc, _) in exc_infos]
# If a single exception, raise it directly as this provides a more readable
# error (hopefully this will improve in #12255).
if len(exceptions) == 1:
raise exceptions[0]
else:
raise ExceptionGroup("Unittest class cleanup errors", exceptions)
def unittest_setup_class_fixture( def unittest_setup_class_fixture(
request: FixtureRequest, request: FixtureRequest,
) -> Generator[None, None, None]: ) -> Generator[None, None, None]:
@ -125,6 +142,7 @@ class UnitTestCase(Class):
# follow this here. # follow this here.
except Exception: except Exception:
cleanup() cleanup()
process_teardown_exceptions()
raise raise
yield yield
try: try:
@ -132,6 +150,7 @@ class UnitTestCase(Class):
teardown() teardown()
finally: finally:
cleanup() cleanup()
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,6 +1500,95 @@ def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None:
assert passed == 1 assert passed == 1
class TestClassCleanupErrors:
"""
Make sure to show exceptions raised during class cleanup function (those registered
via addClassCleanup()).
See #11728.
"""
def test_class_cleanups_failure_in_setup(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)
raise Exception("fail 0")
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:
"""Regression test for #9610 - doesn't crash during traceback pruning.""" """Regression test for #9610 - doesn't crash during traceback pruning."""
pytester.makepyfile( pytester.makepyfile(