From d8e5cf9733bc9c1ee00e5ccbac30108787c098e7 Mon Sep 17 00:00:00 2001 From: Mayur Dhavan <118877752+mayur-dhavan@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:12:49 +0530 Subject: [PATCH 1/4] faulthandler: do not cancel the timeout on test failures Removed the pytest_exception_interact hook implementation to prevent traceback dumping on interactive exceptions. --- src/_pytest/faulthandler.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index e92b9893622..4ce04aacd7a 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -108,12 +108,3 @@ def pytest_enter_pdb() -> None: import faulthandler faulthandler.cancel_dump_traceback_later() - - -@pytest.hookimpl(tryfirst=True) -def pytest_exception_interact() -> None: - """Cancel any traceback dumping due to an interactive exception being - raised.""" - import faulthandler - - faulthandler.cancel_dump_traceback_later() From c061eddcc6cf64b411ad0c5c9f04c5ce1bd4c368 Mon Sep 17 00:00:00 2001 From: Mayur Dhavan <118877752+mayur-dhavan@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:13:34 +0530 Subject: [PATCH 2/4] Implement timeout tests and fix hook cancellation Add regression test for timeout behavior after test failure and update hook cancellation logic. --- testing/test_faulthandler.py | 49 +++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 78681d572aa..2e73bb2edec 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -118,6 +118,42 @@ def test_timeout(): assert result.ret == 0 +def test_timeout_still_active_after_test_failure(pytester: Pytester) -> None: + """The timeout must keep covering teardown after the test itself fails. + + Regression test for #15180: pytest_exception_interact used to cancel the + timer on any failure, so a hanging fixture teardown after a failed test + never produced a traceback dump. + """ + pytester.makepyfile( + """ + import time + + import pytest + + + @pytest.fixture + def hang_on_teardown(): + yield + time.sleep(5) + + + def test_fail(hang_on_teardown): + assert False + """ + ) + pytester.makeini( + """ + [pytest] + faulthandler_timeout = 0.5 + """ + ) + result = pytester.runpytest_subprocess() + result.stderr.fnmatch_lines(["*most recent call first*"]) + result.stdout.fnmatch_lines(["*1 failed*"]) + assert result.ret == 1 + + @pytest.mark.parametrize( ("value", "expected"), [(5, 5), (0.01, 0.01), ('"0.01"', "0.01")] ) @@ -172,11 +208,16 @@ def test_long_sleep_and_raise(): assert result.ret == 1 -@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"]) -def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None: +def test_cancel_timeout_on_hook(monkeypatch) -> None: """Make sure that we are cancelling any scheduled traceback dumping due - to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any - other interactive exception (pytest-dev/pytest-faulthandler#14).""" + to timeout before entering pdb (pytest-dev/pytest-faulthandler#12). + + The timeout is intentionally *not* cancelled on pytest_exception_interact + anymore (#15180): that hook fires for every ordinary test failure, which + disabled the timeout for the rest of the item, including teardown. The + pdb case is still covered because entering pdb triggers pytest_enter_pdb. + """ + hook_name = "pytest_enter_pdb" import faulthandler from _pytest import faulthandler as faulthandler_plugin From 869f5bbadca23b377869e2133215b2bf0ceefae6 Mon Sep 17 00:00:00 2001 From: Mayur Dhavan <118877752+mayur-dhavan@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:14:17 +0530 Subject: [PATCH 3/4] Add changelog entry for #15180 Fixes the cancellation of faulthandler_timeout during tests, ensuring it covers the entire test body and teardown process. Now, a hanging teardown after a failed test will dump a traceback and exit as documented. --- changelog/15180.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/15180.bugfix.rst diff --git a/changelog/15180.bugfix.rst b/changelog/15180.bugfix.rst new file mode 100644 index 00000000000..a08a4ce1c38 --- /dev/null +++ b/changelog/15180.bugfix.rst @@ -0,0 +1 @@ +Fixed ``faulthandler_timeout`` being cancelled at the first failure in a test, so the timeout no longer covers the rest of the test body or fixture teardown. A hanging teardown after a failed test now dumps a traceback (and exits with ``faulthandler_exit_on_timeout``) as documented. From c651b7a071e78a87caf229405a7b68337cabe13b Mon Sep 17 00:00:00 2001 From: Mayur Dhavan <118877752+mayur-dhavan@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:00:25 +0530 Subject: [PATCH 4/4] test faulthandler exit-on-timeout after test failure --- testing/test_faulthandler.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 2e73bb2edec..6ff32278bf9 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -118,7 +118,10 @@ def test_timeout(): assert result.ret == 0 -def test_timeout_still_active_after_test_failure(pytester: Pytester) -> None: +@pytest.mark.parametrize("exit_on_timeout", [True, False]) +def test_timeout_still_active_after_test_failure( + pytester: Pytester, exit_on_timeout: bool +) -> None: """The timeout must keep covering teardown after the test itself fails. Regression test for #15180: pytest_exception_interact used to cancel the @@ -143,14 +146,18 @@ def test_fail(hang_on_teardown): """ ) pytester.makeini( - """ + f""" [pytest] faulthandler_timeout = 0.5 + faulthandler_exit_on_timeout = {"true" if exit_on_timeout else "false"} """ ) result = pytester.runpytest_subprocess() - result.stderr.fnmatch_lines(["*most recent call first*"]) - result.stdout.fnmatch_lines(["*1 failed*"]) + result.stderr.fnmatch_lines(["*most recent call first*", "*in hang_on_teardown*"]) + if exit_on_timeout: + result.stdout.no_fnmatch_line("*1 failed*") + else: + result.stdout.fnmatch_lines(["*1 failed*"]) assert result.ret == 1