Skip to content

Commit 83da343

Browse files
ogriselwebknjaz
andauthored
Make it possible for faulthandler to terminate the pytest process on timeout (#13679)
* Make it possible for faulthandler to terminate the pytest process on timeout * Add myself to AUTHORS and add changelog entry. * Update the documentation * Skip flakky CI tests * Revert "Skip flakky CI tests" This reverts commit be4f0ba. * Pass the CI env in tox.ini and use it to conditionally skip tests on CI * Revert "Pass the CI env in tox.ini and use it to conditionally skip tests on CI" This reverts commit 000876e. * Revert "Revert "Skip flakky CI tests"" This reverts commit 5066d98. * Apply suggestions from code review Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]> * Better way to tell mypy that faulthandler_exit_on_timeout is always a bool instance * skip test only on CI * Trigger CI * Skip the new test on ubuntu-py314 * Actually skip the new test on ubuntu-py314 * Trigger CI * Trigger CI --------- Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
1 parent d036b12 commit 83da343

File tree

5 files changed

+80
-3
lines changed

5 files changed

+80
-3
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ Oleg Sushchenko
342342
Oleksandr Zavertniev
343343
Olga Matoula
344344
Oliver Bestwalter
345+
Olivier Grisel
345346
Omar Kohl
346347
Omer Hadari
347348
Ondřej Súkup

changelog/13678.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added a new `faulthandler_exit_on_timeout` ini option set to "false" by default to let `faulthandler` interrupt the `pytest` process after a timeout in case of deadlock -- by :user:`ogrisel`.
2+
3+
Previously, a `faulthandler` timeout would only dump the traceback of all threads to stderr, but would not interrupt the `pytest` process.

doc/en/reference/reference.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,25 @@ passed multiple times. The expected format is ``name=value``. For example::
14841484
14851485
For more information please refer to :ref:`faulthandler`.
14861486

1487+
1488+
.. confval:: faulthandler_exit_on_timeout
1489+
1490+
Exit the pytest process after the per-test timeout is reached by passing
1491+
`exit=True` to the :func:`faulthandler.dump_traceback_later` function. This
1492+
is particularly useful to avoid wasting CI resources for test suites that
1493+
are prone to putting the main Python interpreter into a deadlock state.
1494+
1495+
This option is set to 'false' by default.
1496+
1497+
.. code-block:: ini
1498+
1499+
# content of pytest.ini
1500+
[pytest]
1501+
faulthandler_timeout=5
1502+
faulthandler_exit_on_timeout=true
1503+
1504+
For more information please refer to :ref:`faulthandler`.
1505+
14871506
.. confval:: filterwarnings
14881507

14891508

@@ -2401,6 +2420,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
24012420
faulthandler_timeout (string):
24022421
Dump the traceback of all threads if a test takes
24032422
more than TIMEOUT seconds to finish
2423+
faulthandler_exit_on_timeout (bool):
2424+
Exit the test process if a test takes more than
2425+
faulthandler_timeout seconds to finish
24042426
addopts (args): Extra command line options
24052427
minversion (string): Minimally required pytest version
24062428
pythonpath (paths): Add paths to sys.path

src/_pytest/faulthandler.py

Lines changed: 17 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,21 @@ 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+
exit_on_timeout = config.getini("faulthandler_exit_on_timeout")
84+
assert isinstance(exit_on_timeout, bool)
85+
return exit_on_timeout
86+
87+
7588
@pytest.hookimpl(wrapper=True, trylast=True)
7689
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
7790
timeout = get_timeout_config_value(item.config)
91+
exit_on_timeout = get_exit_on_timeout_config_value(item.config)
7892
if timeout > 0:
7993
import faulthandler
8094

8195
stderr = item.config.stash[fault_handler_stderr_fd_key]
82-
faulthandler.dump_traceback_later(timeout, file=stderr)
96+
faulthandler.dump_traceback_later(timeout, file=stderr, exit=exit_on_timeout)
8397
try:
8498
return (yield)
8599
finally:

testing/test_faulthandler.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,43 @@ def test_timeout():
118118
assert result.ret == 0
119119

120120

121+
@pytest.mark.keep_ci_var
122+
@pytest.mark.skipif(
123+
"CI" in os.environ and sys.platform == "linux" and sys.version_info >= (3, 14),
124+
reason="sometimes crashes on CI because of truncated outputs (#7022)",
125+
)
126+
@pytest.mark.parametrize("exit_on_timeout", [True, False])
127+
def test_timeout_and_exit(pytester: Pytester, exit_on_timeout: bool) -> None:
128+
"""Test option to force exit pytest process after a certain timeout."""
129+
pytester.makepyfile(
130+
"""
131+
import os, time
132+
def test_long_sleep_and_raise():
133+
time.sleep(1 if "CI" in os.environ else 0.1)
134+
raise AssertionError(
135+
"This test should have been interrupted before reaching this point."
136+
)
137+
"""
138+
)
139+
pytester.makeini(
140+
f"""
141+
[pytest]
142+
faulthandler_timeout = 0.01
143+
faulthandler_exit_on_timeout = {"true" if exit_on_timeout else "false"}
144+
"""
145+
)
146+
result = pytester.runpytest_subprocess()
147+
tb_output = "most recent call first"
148+
result.stderr.fnmatch_lines([f"*{tb_output}*"])
149+
if exit_on_timeout:
150+
result.stdout.no_fnmatch_line("*1 failed*")
151+
result.stdout.no_fnmatch_line("*AssertionError*")
152+
else:
153+
result.stdout.fnmatch_lines(["*1 failed*"])
154+
result.stdout.fnmatch_lines(["*AssertionError*"])
155+
assert result.ret == 1
156+
157+
121158
@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
122159
def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None:
123160
"""Make sure that we are cancelling any scheduled traceback dumping due

0 commit comments

Comments
 (0)