Skip to content

Commit 94f948a

Browse files
okkenflub
authored andcommitted
add --suite-timeout
1 parent 97196bf commit 94f948a

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

pytest_timeout.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import os
1111
import signal
1212
import sys
13+
import time
1314
import threading
1415
import traceback
1516
from collections import namedtuple
@@ -43,6 +44,11 @@
4344
When specified, disables debugger detection. breakpoint(), pdb.set_trace(), etc.
4445
will be interrupted by the timeout.
4546
""".strip()
47+
SUITE_TIMEOUT_DESC = """
48+
Timeout in minutes for entire suite. Default is None which
49+
means no timeout. Timeout is checked between tests, and will not interrupt a test
50+
in progress. Can be specified as a float for partial minutes.
51+
""".strip()
4652

4753
# bdb covers pdb, ipdb, and possibly others
4854
# pydevd covers PyCharm, VSCode, and possibly others
@@ -79,6 +85,15 @@ def pytest_addoption(parser):
7985
action="store_true",
8086
help=DISABLE_DEBUGGER_DETECTION_DESC,
8187
)
88+
group.addoption(
89+
"--suite-timeout",
90+
action="store",
91+
dest="suite_timeout",
92+
default=None,
93+
type=float,
94+
metavar="minutes",
95+
help=SUITE_TIMEOUT_DESC,
96+
)
8297
parser.addini("timeout", TIMEOUT_DESC)
8398
parser.addini("timeout_method", METHOD_DESC)
8499
parser.addini("timeout_func_only", FUNC_ONLY_DESC, type="bool", default=False)
@@ -119,9 +134,13 @@ def pytest_addhooks(pluginmanager):
119134
pluginmanager.add_hookspecs(TimeoutHooks)
120135

121136

137+
_suite_expire_time = 0
138+
139+
122140
@pytest.hookimpl
123141
def pytest_configure(config):
124142
"""Register the marker so it shows up in --markers output."""
143+
global _suite_expire_time, _suite_timeout_minutes
125144
config.addinivalue_line(
126145
"markers",
127146
"timeout(timeout, method=None, func_only=False, "
@@ -143,6 +162,11 @@ def pytest_configure(config):
143162
config._env_timeout_func_only = settings.func_only
144163
config._env_timeout_disable_debugger_detection = settings.disable_debugger_detection
145164

165+
_suite_timeout_minutes = config.getoption("--suite-timeout")
166+
if _suite_timeout_minutes:
167+
_suite_expire_time = time.time() + (_suite_timeout_minutes * 60)
168+
169+
146170

147171
@pytest.hookimpl(hookwrapper=True)
148172
def pytest_runtest_protocol(item):
@@ -507,3 +531,9 @@ def dump_stacks(terminal):
507531
thread_name = "<unknown>"
508532
terminal.sep("~", title="Stack of %s (%s)" % (thread_name, thread_ident))
509533
terminal.write("".join(traceback.format_stack(frame)))
534+
535+
536+
def pytest_runtest_logfinish(nodeid, location):
537+
if _suite_expire_time and _suite_expire_time < time.time():
538+
pytest.exit(f"suite-timeout: {_suite_timeout_minutes} minutes exceeded",
539+
returncode=0)

test_pytest_timeout.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,3 +603,22 @@ def test_foo():
603603
"pytest_timeout_cancel_timer",
604604
]
605605
)
606+
607+
def test_suite_timeout(pytester):
608+
pytester.makepyfile(
609+
"""
610+
import time, pytest
611+
612+
@pytest.mark.parametrize('i', range(10))
613+
def test_foo(i):
614+
time.sleep(0.1)
615+
"""
616+
)
617+
# each parametrization runs for 0.1 sec
618+
# or about 0.00166 seconds each
619+
# so 0.005 min should be about 3 iterations
620+
result = pytester.runpytest_subprocess("--suite-timeout", "0.005")
621+
result.stdout.fnmatch_lines([
622+
"*= 3 passed * =*",
623+
"*!! * suite-timeout: 0.005 minutes exceeded !!!*"
624+
])

0 commit comments

Comments
 (0)