Skip to content

Commit a41b231

Browse files
committed
Make it possible for faulthandler to terminate the pytest process on timeout
1 parent 3d58e8d commit a41b231

File tree

2 files changed

+55
-12
lines changed

2 files changed

+55
-12
lines changed

src/_pytest/faulthandler.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616

1717

1818
def pytest_addoption(parser: Parser) -> None:
19-
help = (
19+
help_timeout = (
2020
"Dump the traceback of all threads if a test takes "
2121
"more than TIMEOUT seconds to finish"
2222
)
23-
parser.addini("faulthandler_timeout", help, default=0.0)
23+
help_exit_on_timeout = (
24+
"Exit the test process if a test takes more than "
25+
"faulthandler_timeout seconds to finish"
26+
)
27+
parser.addini("faulthandler_timeout", help_timeout, default=0.0)
28+
parser.addini(
29+
"faulthandler_exit_on_timeout", help_exit_on_timeout, type="bool", default=False
30+
)
2431

2532

2633
def pytest_configure(config: Config) -> None:
@@ -72,14 +79,19 @@ def get_timeout_config_value(config: Config) -> float:
7279
return float(config.getini("faulthandler_timeout") or 0.0)
7380

7481

82+
def get_exit_on_timeout_config_value(config: Config) -> bool:
83+
return bool(config.getini("faulthandler_exit_on_timeout"))
84+
85+
7586
@pytest.hookimpl(wrapper=True, trylast=True)
7687
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
7788
timeout = get_timeout_config_value(item.config)
89+
exit_on_timeout = get_exit_on_timeout_config_value(item.config)
7890
if timeout > 0:
7991
import faulthandler
8092

8193
stderr = item.config.stash[fault_handler_stderr_fd_key]
82-
faulthandler.dump_traceback_later(timeout, file=stderr)
94+
faulthandler.dump_traceback_later(timeout, file=stderr, exit=exit_on_timeout)
8395
try:
8496
return (yield)
8597
finally:

testing/test_faulthandler.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import io
5+
import os
56
import sys
67

78
from _pytest.pytester import Pytester
@@ -71,20 +72,15 @@ def test_disabled():
7172
assert result.ret == 0
7273

7374

74-
@pytest.mark.parametrize(
75-
"enabled",
76-
[
77-
pytest.param(
78-
True, marks=pytest.mark.skip(reason="sometimes crashes on CI (#7022)")
79-
),
80-
False,
81-
],
82-
)
75+
@pytest.mark.parametrize("enabled", [True, False])
8376
def test_timeout(pytester: Pytester, enabled: bool) -> None:
8477
"""Test option to dump tracebacks after a certain timeout.
8578
8679
If faulthandler is disabled, no traceback will be dumped.
8780
"""
81+
if enabled and "CI" in os.environ:
82+
pytest.xfail(reason="sometimes crashes on CI (#7022)")
83+
8884
pytester.makepyfile(
8985
"""
9086
import os, time
@@ -110,6 +106,41 @@ def test_timeout():
110106
assert result.ret == 0
111107

112108

109+
@pytest.mark.parametrize("exit_on_timeout", [True, False])
110+
def test_timeout_and_exit(pytester: Pytester, exit_on_timeout: bool) -> None:
111+
"""Test option to force exit pytest process after a certain timeout."""
112+
if "CI" in os.environ:
113+
pytest.xfail(reason="sometimes crashes on CI (#7022)")
114+
115+
pytester.makepyfile(
116+
"""
117+
import os, time
118+
def test_long_sleep_and_raise():
119+
time.sleep(1 if "CI" in os.environ else 0.1)
120+
raise AssertionError(
121+
"This test should have been interrupted before reaching this point."
122+
)
123+
"""
124+
)
125+
pytester.makeini(
126+
f"""
127+
[pytest]
128+
faulthandler_timeout = 0.01
129+
faulthandler_exit_on_timeout = {"true" if exit_on_timeout else "false"}
130+
"""
131+
)
132+
result = pytester.runpytest_subprocess()
133+
tb_output = "most recent call first"
134+
result.stderr.fnmatch_lines([f"*{tb_output}*"])
135+
if exit_on_timeout:
136+
result.stdout.no_fnmatch_line("*1 failed*")
137+
result.stdout.no_fnmatch_line("*AssertionError*")
138+
else:
139+
result.stdout.fnmatch_lines(["*1 failed*"])
140+
result.stdout.fnmatch_lines(["*AssertionError*"])
141+
assert result.ret == 1
142+
143+
113144
@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
114145
def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None:
115146
"""Make sure that we are cancelling any scheduled traceback dumping due

0 commit comments

Comments
 (0)