Skip to content
Open
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 changelog/15180.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 0 additions & 9 deletions src/_pytest/faulthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
56 changes: 52 additions & 4 deletions testing/test_faulthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,49 @@ def test_timeout():
assert result.ret == 0


@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
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(
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*", "*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


@pytest.mark.parametrize(
("value", "expected"), [(5, 5), (0.01, 0.01), ('"0.01"', "0.01")]
)
Expand Down Expand Up @@ -172,11 +215,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
Expand Down
Loading