Skip to content

Commit 116fb20

Browse files
committed
implement exception timeout
attempt to trigger exception in test first before killing process
1 parent fb28b8b commit 116fb20

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

pytest_timeout.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
import os
1111
import signal
1212
import sys
13+
import ctypes
14+
import time
1315
import threading
1416
import traceback
1517
from collections import namedtuple
1618
from distutils.version import LooseVersion
19+
from threading import current_thread
20+
from _pytest.config import ExitCode
21+
from _pytest.outcomes import Failed
1722

1823
import py
1924
import pytest
@@ -23,7 +28,7 @@
2328
if HAVE_SIGALRM:
2429
DEFAULT_METHOD = "signal"
2530
else:
26-
DEFAULT_METHOD = "thread"
31+
DEFAULT_METHOD = "exception"
2732
TIMEOUT_DESC = """
2833
Timeout in seconds before dumping the stacks. Default is 0 which
2934
means no timeout.
@@ -56,14 +61,14 @@ def pytest_addoption(parser):
5661
group.addoption(
5762
"--timeout_method",
5863
action="store",
59-
choices=["signal", "thread"],
64+
choices=["signal", "thread", "exception"],
6065
help="Deprecated, use --timeout-method",
6166
)
6267
group.addoption(
6368
"--timeout-method",
6469
dest="timeout_method",
6570
action="store",
66-
choices=["signal", "thread"],
71+
choices=["signal", "thread", "exception"],
6772
help=METHOD_DESC,
6873
)
6974
parser.addini("timeout", TIMEOUT_DESC)
@@ -216,6 +221,16 @@ def cancel():
216221

217222
item.cancel_timeout = cancel
218223
timer.start()
224+
elif params.method == "exception":
225+
timer = threading.Timer(params.timeout, timeout_exception, (current_thread(), item, params.timeout))
226+
timer.name = "%s %s" % (__name__, item.nodeid)
227+
228+
def cancel():
229+
timer.cancel()
230+
timer.join()
231+
232+
item.cancel_timeout = cancel
233+
timer.start()
219234

220235

221236
def timeout_teardown(item):
@@ -417,6 +432,37 @@ def timeout_timer(item, timeout):
417432
os._exit(1)
418433

419434

435+
def timeout_exception(thread, item, timeout):
436+
"""Dump stack of threads and call os._exit().
437+
438+
This disables the capturemanager and dumps stdout and stderr.
439+
Then the stacks are dumped and os._exit(1) is called.
440+
"""
441+
if is_debugging():
442+
return
443+
try:
444+
__tracebackhide__ = True
445+
nthreads = len(threading.enumerate())
446+
if nthreads > 1:
447+
write_title("Timeout", sep="+")
448+
dump_stacks()
449+
if nthreads > 1:
450+
write_title("Timeout", sep="+")
451+
except Exception:
452+
traceback.print_exc()
453+
finally:
454+
sys.stdout.flush()
455+
sys.stderr.flush()
456+
457+
for _ in range(10):
458+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
459+
ctypes.c_long(thread.ident), ctypes.py_object(Failed),
460+
)
461+
time.sleep(0.5)
462+
463+
os._exit(1)
464+
465+
420466
def dump_stacks():
421467
"""Dump the stacks of all threads except the current thread."""
422468
current_ident = threading.current_thread().ident

test_pytest_timeout.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ def test_foo():
7272
assert "++ Timeout ++" in result.stderr.lines[-1]
7373

7474

75+
def test_exeption(testdir):
76+
testdir.makepyfile(
77+
"""
78+
import time
79+
80+
def test_foo():
81+
time.sleep(2)
82+
"""
83+
)
84+
result = testdir.runpytest("--timeout=1", "--timeout-method=exception")
85+
result.stderr.fnmatch_lines(
86+
[
87+
"*++ Timeout ++*",
88+
"*~~ Stack of MainThread* ~~*",
89+
"*File *, line *, in *",
90+
"*++ Timeout ++*",
91+
]
92+
)
93+
assert "++ Timeout ++" in result.stderr.lines[-1]
94+
95+
7596
@pytest.mark.skipif(
7697
hasattr(sys, "pypy_version_info"), reason="pypy coverage seems broken currently"
7798
)

0 commit comments

Comments
 (0)