diff --git a/AUTHORS b/AUTHORS index 303d04133cb..427c39271d1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -240,8 +240,10 @@ Kevin Hierro Carrasco Kevin J. Foley Kian Eliasi Kian-Meng Ang +Kirill Zhdanov Kodi B. Arfer Kojo Idrissa +Konstantin Shkel Kostis Anagnostopoulos Kristoffer Nordström Kyle Altendorf diff --git a/changelog/12163.feature.rst b/changelog/12163.feature.rst new file mode 100644 index 00000000000..1fa09995d1d --- /dev/null +++ b/changelog/12163.feature.rst @@ -0,0 +1 @@ +Teardown fixtures now can access the information about current teardown exceptions in `node.teardown_exceptions`. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 51bc5174628..d5ff3a87edb 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -214,6 +214,10 @@ def __init__( # Deprecated alias. Was never public. Can be removed in a few releases. self._store = self.stash + #: A list of exceptions that happened during teardown. Intended for + #: post-teardown inspection, not required internally. + self.teardown_exceptions: list[BaseException] = [] + @classmethod def from_parent(cls, parent: Node, **kw) -> Self: """Public constructor for Nodes. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 0b60301bf5f..7744c56ff3e 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -539,19 +539,20 @@ def teardown_exact(self, nextitem: Item | None) -> None: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break node, (finalizers, _) = self.stack.popitem() - these_exceptions = [] while finalizers: fin = finalizers.pop() try: fin() except TEST_OUTCOME as e: - these_exceptions.append(e) + node.teardown_exceptions.append(e) - if len(these_exceptions) == 1: - exceptions.extend(these_exceptions) - elif these_exceptions: + if len(node.teardown_exceptions) == 1: + exceptions.extend(node.teardown_exceptions) + elif node.teardown_exceptions: msg = f"errors while tearing down {node!r}" - exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1])) + exceptions.append( + BaseExceptionGroup(msg, node.teardown_exceptions[::-1]) + ) if len(exceptions) == 1: raise exceptions[0] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ba1f86f02d9..ad829889c50 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1584,3 +1584,30 @@ def test_no_terminal_plugin(pytester: Pytester) -> None: pytester.makepyfile("def test(): assert 1 == 2") result = pytester.runpytest("-pno:terminal", "-s") assert result.ret == ExitCode.TESTS_FAILED + + +def test_get_exception_on_teardown_failure(pytester: Pytester) -> None: + """Smoke test to be sure teardown exceptions handled properly via node property""" + pytester.makepyfile( + conftest=""" + import sys + import pytest + def pytest_exception_interact(node, call, report): + sys.stderr.write("teardown_exceptions: `{}`".format(node.teardown_exceptions)) + + @pytest.fixture + def mylist(): + yield + raise AssertionError(111) + """, + test_file=""" + def test_func(mylist): + assert True + """, + ) + result = pytester.runpytest() + assert result.ret == ExitCode.TESTS_FAILED + assert "teardown_exceptions: `[AssertionError(111)]`" in result.stderr.str() + # Related to the #9909 - first the test passes, then the teardown fails, what + # results in a double-reporting. + result.assert_outcomes(passed=1, errors=1)