|
10 | 10 | import os
|
11 | 11 | import signal
|
12 | 12 | import sys
|
| 13 | +import ctypes |
| 14 | +import time |
13 | 15 | import threading
|
14 | 16 | import traceback
|
15 | 17 | from collections import namedtuple
|
16 | 18 | from distutils.version import LooseVersion
|
| 19 | +from threading import current_thread |
| 20 | +from _pytest.config import ExitCode |
| 21 | +from _pytest.outcomes import Failed |
17 | 22 |
|
18 | 23 | import py
|
19 | 24 | import pytest
|
|
23 | 28 | if HAVE_SIGALRM:
|
24 | 29 | DEFAULT_METHOD = "signal"
|
25 | 30 | else:
|
26 |
| - DEFAULT_METHOD = "thread" |
| 31 | + DEFAULT_METHOD = "exception" |
27 | 32 | TIMEOUT_DESC = """
|
28 | 33 | Timeout in seconds before dumping the stacks. Default is 0 which
|
29 | 34 | means no timeout.
|
@@ -56,14 +61,14 @@ def pytest_addoption(parser):
|
56 | 61 | group.addoption(
|
57 | 62 | "--timeout_method",
|
58 | 63 | action="store",
|
59 |
| - choices=["signal", "thread"], |
| 64 | + choices=["signal", "thread", "exception"], |
60 | 65 | help="Deprecated, use --timeout-method",
|
61 | 66 | )
|
62 | 67 | group.addoption(
|
63 | 68 | "--timeout-method",
|
64 | 69 | dest="timeout_method",
|
65 | 70 | action="store",
|
66 |
| - choices=["signal", "thread"], |
| 71 | + choices=["signal", "thread", "exception"], |
67 | 72 | help=METHOD_DESC,
|
68 | 73 | )
|
69 | 74 | parser.addini("timeout", TIMEOUT_DESC)
|
@@ -216,6 +221,16 @@ def cancel():
|
216 | 221 |
|
217 | 222 | item.cancel_timeout = cancel
|
218 | 223 | 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() |
219 | 234 |
|
220 | 235 |
|
221 | 236 | def timeout_teardown(item):
|
@@ -417,6 +432,37 @@ def timeout_timer(item, timeout):
|
417 | 432 | os._exit(1)
|
418 | 433 |
|
419 | 434 |
|
| 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 | + |
420 | 466 | def dump_stacks():
|
421 | 467 | """Dump the stacks of all threads except the current thread."""
|
422 | 468 | current_ident = threading.current_thread().ident
|
|
0 commit comments