From 4e13a5fe3f43f4b40f91257d1d4e68c10ab8e610 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Fri, 26 Apr 2024 10:24:58 -0400 Subject: [PATCH] Report class cleanup exceptions --- AUTHORS | 1 + changelog/11728.improvement.rst | 1 + src/_pytest/unittest.py | 15 +++++++++ testing/test_unittest.py | 58 +++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 changelog/11728.improvement.rst diff --git a/AUTHORS b/AUTHORS index d7148acfc..4f61c0591 100644 --- a/AUTHORS +++ b/AUTHORS @@ -101,6 +101,7 @@ Cyrus Maden Damian Skrzypczak Daniel Grana Daniel Hahler +Daniel Miller Daniel Nuri Daniel Sánchez Castelló Daniel Valenzuela Zenteno diff --git a/changelog/11728.improvement.rst b/changelog/11728.improvement.rst new file mode 100644 index 000000000..3464bd90b --- /dev/null +++ b/changelog/11728.improvement.rst @@ -0,0 +1 @@ +Class cleanup exceptions are now reported. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 5099904fd..10cd7212e 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -111,6 +111,19 @@ class UnitTestCase(Class): return None cleanup = getattr(cls, "doClassCleanups", lambda: None) + def process_teardown_exceptions(raise_last: bool): + errors = getattr(cls, "tearDown_exceptions", None) + if not errors: + return + others = errors[:-1] if raise_last else errors + if others: + num = len(errors) + for n, (exc_type, exc, tb) in enumerate(others, start=1): + print(f"\nclass cleanup error ({n} of {num}):", file=sys.stderr) + traceback.print_exception(exc_type, exc, tb) + if raise_last: + raise errors[-1][1] + def unittest_setup_class_fixture( request: FixtureRequest, ) -> Generator[None, None, None]: @@ -125,6 +138,7 @@ class UnitTestCase(Class): # follow this here. except Exception: cleanup() + process_teardown_exceptions(raise_last=False) raise yield try: @@ -132,6 +146,7 @@ class UnitTestCase(Class): teardown() finally: cleanup() + process_teardown_exceptions(raise_last=True) self.session._fixturemanager._register_fixture( # Use a unique name to speed up lookup. diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 9ecb548ee..f9e7741db 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1500,6 +1500,64 @@ def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None: assert passed == 1 +def test_class_cleanups_failure_in_setup(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.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', + ]) + + +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) + result.assert_outcomes(passed=1, errors=1) + result.stderr.fnmatch_lines([ + 'class cleanup error (1 of 2):', + 'Traceback *', + 'Exception: fail 1', + ]) + result.stdout.fnmatch_lines([ + '* ERROR at teardown of MyTestCase.test *', + 'E * Exception: fail 2', + ]) + + def test_traceback_pruning(pytester: Pytester) -> None: """Regression test for #9610 - doesn't crash during traceback pruning.""" pytester.makepyfile(