From 1a892d0a8b1ef3261ff09f2c7fc78205234a69d0 Mon Sep 17 00:00:00 2001 From: Myp3a Date: Wed, 27 Nov 2024 13:43:18 +0300 Subject: [PATCH 1/7] feat: save teardown exceptions for further teardowns --- AUTHORS | 1 + src/_pytest/nodes.py | 3 +++ src/_pytest/runner.py | 14 ++++++++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index 303d04133cb..b329a6fca93 100644 --- a/AUTHORS +++ b/AUTHORS @@ -242,6 +242,7 @@ Kian Eliasi Kian-Meng Ang Kodi B. Arfer Kojo Idrissa +Konstantin Shkel Kostis Anagnostopoulos Kristoffer Nordström Kyle Altendorf diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 51bc5174628..c920800df1f 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -214,6 +214,9 @@ 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 + 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..339c7b5ef88 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -539,19 +539,21 @@ 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 = [] + node.teardown_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] From 11b5de451ded7cd3e9499068b8ac72df1d8b828f Mon Sep 17 00:00:00 2001 From: Kirill Zhdanov Date: Thu, 28 Nov 2024 12:59:23 +0700 Subject: [PATCH 2/7] test: teardown exception handled using node `teardown_exceptions` property --- AUTHORS | 1 + testing/acceptance_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/AUTHORS b/AUTHORS index b329a6fca93..427c39271d1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -240,6 +240,7 @@ Kevin Hierro Carrasco Kevin J. Foley Kian Eliasi Kian-Meng Ang +Kirill Zhdanov Kodi B. Arfer Kojo Idrissa Konstantin Shkel diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ba1f86f02d9..90689f833a3 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1584,3 +1584,28 @@ 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("{}".format(node.teardown_exceptions)) + + import pytest + @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 "AssertionError(111)" in result.stderr.str() + result.stdout.fnmatch_lines(["*1 error*"]) From 9abac4d77e1072cea1fd8e0c8deb815720907dea Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 08:02:30 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/acceptance_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 90689f833a3..1fb0190bf3b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1585,6 +1585,7 @@ def test_no_terminal_plugin(pytester: Pytester) -> None: 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( From ae683a92c8e7d6c743f02a467059307629b767a7 Mon Sep 17 00:00:00 2001 From: Myp3a Date: Thu, 5 Dec 2024 16:07:28 +0300 Subject: [PATCH 4/7] fix: remove double initialization --- src/_pytest/runner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 339c7b5ef88..7744c56ff3e 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -539,7 +539,6 @@ def teardown_exact(self, nextitem: Item | None) -> None: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break node, (finalizers, _) = self.stack.popitem() - node.teardown_exceptions = [] while finalizers: fin = finalizers.pop() try: From a8e0aba5292e89c30d3b1fa266af69cf193a5723 Mon Sep 17 00:00:00 2001 From: Myp3a Date: Thu, 5 Dec 2024 16:16:30 +0300 Subject: [PATCH 5/7] test: assert fixes --- testing/acceptance_test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 1fb0190bf3b..9f09a6be93a 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1593,9 +1593,8 @@ def test_get_exception_on_teardown_failure(pytester: Pytester) -> None: import sys import pytest def pytest_exception_interact(node, call, report): - sys.stderr.write("{}".format(node.teardown_exceptions)) + sys.stderr.write("teardown_exceptions: `{}`".format(node.teardown_exceptions)) - import pytest @pytest.fixture def mylist(): yield @@ -1608,5 +1607,5 @@ def test_func(mylist): ) result = pytester.runpytest() assert result.ret == ExitCode.TESTS_FAILED - assert "AssertionError(111)" in result.stderr.str() - result.stdout.fnmatch_lines(["*1 error*"]) + assert "teardown_exceptions: `[AssertionError(111)]`" in result.stderr.str() + result.assert_outcomes(passed=1, errors=1) From d5a146f7e8c6ce83ddf05f6e47c1b2904131b4de Mon Sep 17 00:00:00 2001 From: Myp3a Date: Thu, 5 Dec 2024 16:38:31 +0300 Subject: [PATCH 6/7] chore: changelog entry --- changelog/12163.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/12163.feature.rst 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`. From 067d8facd81b49d1f84adf830f4b584371bf1cf4 Mon Sep 17 00:00:00 2001 From: Myp3a Date: Fri, 6 Dec 2024 14:40:24 +0300 Subject: [PATCH 7/7] chore: explanations --- src/_pytest/nodes.py | 3 ++- testing/acceptance_test.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c920800df1f..d5ff3a87edb 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -214,7 +214,8 @@ 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 + #: A list of exceptions that happened during teardown. Intended for + #: post-teardown inspection, not required internally. self.teardown_exceptions: list[BaseException] = [] @classmethod diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 9f09a6be93a..ad829889c50 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1608,4 +1608,6 @@ def test_func(mylist): 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)