Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report class cleanup exceptions #12250

Merged
merged 4 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Cyrus Maden
Damian Skrzypczak
Daniel Grana
Daniel Hahler
Daniel Miller
Daniel Nuri
Daniel Sánchez Castelló
Daniel Valenzuela Zenteno
Expand Down
1 change: 1 addition & 0 deletions changelog/11728.improvement.rst
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions src/_pytest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
import pytest


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

if TYPE_CHECKING:
import unittest

Expand Down Expand Up @@ -111,6 +114,20 @@
return None
cleanup = getattr(cls, "doClassCleanups", lambda: None)

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
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]

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 ExceptionGroup("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,
) -> Generator[None, None, None]:
Expand All @@ -125,13 +142,15 @@
# follow this here.
except Exception:
cleanup()
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()

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
89 changes: 89 additions & 0 deletions testing/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,95 @@ def test_cleanup_called_the_right_number_of_times():
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(
Expand Down
Loading