Skip to content

Commit

Permalink
Use ExceptionGroup instead of printing, improve changelog
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Apr 26, 2024
1 parent e209a3d commit 122fd05
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 74 deletions.
2 changes: 1 addition & 1 deletion changelog/11728.improvement.rst
@@ -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.
30 changes: 17 additions & 13 deletions src/_pytest/unittest.py
Expand Up @@ -32,6 +32,9 @@
import pytest


if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup

if TYPE_CHECKING:
import unittest

Expand Down Expand Up @@ -111,18 +114,19 @@ def _register_unittest_setup_class_fixture(self, cls: type) -> None:
return None
cleanup = getattr(cls, "doClassCleanups", lambda: None)

def process_teardown_exceptions(raise_last: bool):
errors = getattr(cls, "tearDown_exceptions", None)
if not errors:
def process_teardown_exceptions() -> None:

Check warning on line 117 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L117

Added line #L117 was not covered by tests
# 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)

Check warning on line 120 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L120

Added line #L120 was not covered by tests
if not exc_infos:
return

Check warning on line 122 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L122

Added line #L122 was not covered by tests
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]
exceptions = [exc for (_, exc, _) in exc_infos]
# If a single exception, raise it directly as this provides a more readable
# error.
if len(exceptions) == 1:
raise exceptions[0]

Check warning on line 127 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L127

Added line #L127 was not covered by tests
else:
raise BaseExceptionGroup("Unittest class cleanup errors", exceptions)

Check warning on line 129 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L129

Added line #L129 was not covered by tests

def unittest_setup_class_fixture(
request: FixtureRequest,
Expand All @@ -138,15 +142,15 @@ def unittest_setup_class_fixture(
# follow this here.
except Exception:
cleanup()
process_teardown_exceptions(raise_last=False)
process_teardown_exceptions()

Check warning on line 145 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L145

Added line #L145 was not covered by tests
raise
yield
try:
if teardown is not None:
teardown()
finally:
cleanup()
process_teardown_exceptions(raise_last=True)
process_teardown_exceptions()

Check warning on line 153 in src/_pytest/unittest.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unittest.py#L153

Added line #L153 was not covered by tests

self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
Expand Down
143 changes: 83 additions & 60 deletions testing/test_unittest.py
Expand Up @@ -1500,70 +1500,93 @@ def test_cleanup_called_the_right_number_of_times():
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
class TestClassCleanupErrors:
"""
)
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",
]
)
Make sure to show exceptions raised during class cleanup function (those registered
via addClassCleanup()).
See #11728.
"""

def test_class_cleanups_failure_in_teardown(pytester: Pytester) -> None:
testpath = pytester.makepyfile(
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
"""
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",
]
)
)
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:
Expand Down

0 comments on commit 122fd05

Please sign in to comment.