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
Daniel Grana
Daniel Hahler
Daniel Miller
Daniel Nuri
Daniel Sánchez Castelló
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
if sys.version_info[:2] < (3, 11):
from exceptiongroup import ExceptionGroup
if TYPE_CHECKING:
import unittest
@ -111,6 +114,20 @@ class UnitTestCase(Class):
return 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(
request: FixtureRequest,
) -> Generator[None, None, None]:
@ -125,6 +142,7 @@ class UnitTestCase(Class):
# follow this here.
except Exception:
cleanup()
process_teardown_exceptions()
raise
yield
try:
@ -132,6 +150,7 @@ class UnitTestCase(Class):
teardown()
finally:
cleanup()
process_teardown_exceptions()
self.session._fixturemanager._register_fixture(
# 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
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:
"""Regression test for #9610 - doesn't crash during traceback pruning."""
pytester.makepyfile(