Skip to content

Commit 0614cb8

Browse files
authored
Add testcase timeout cli option (#1252)
* Add CLI option for default testcase timeout authored-by: butako <[email protected]>
1 parent 4bac3ec commit 0614cb8

File tree

7 files changed

+160
-0
lines changed

7 files changed

+160
-0
lines changed

doc/en/multitest.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,23 @@ Similarly, ``setup`` and ``teardown`` methods in a test suite can be limited to
13431343
13441344
It's useful when ``setup`` has much initialization work that takes long, e.g. connects to a server but has no response and makes program hanging. Note that this ``@timeout`` decorator can also be used for ``pre_testcase`` and ``post_testcase``, but that is not suggested because pre/post testcase methods are called everytime before/after each testcase runs, they should be written as simple as possible.
13451345
1346+
Default Testcase Timeout
1347+
^^^^^^^^^^^^^^^^^^^^^^^^^
1348+
1349+
Set a default timeout for testcases without explicit timeouts:
1350+
1351+
.. code-block:: python
1352+
1353+
# Programmatically
1354+
MultiTest(name="MyTest", suites=[MySuite()], testcase_timeout=300)
1355+
1356+
.. code-block:: bash
1357+
1358+
# Via CLI
1359+
python my_test.py --testcase-timeout 300
1360+
1361+
Explicit ``@testcase(timeout=...)`` values always override the default.
1362+
13461363
Hooks
13471364
-----
13481365
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added ``--testcase-timeout`` CLI option to set a default timeout for testcases without explicit timeout decorators.

testplan/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from testplan.exporters.testing.failed_tests import FailedTestLevel
77

88
TESTPLAN_TIMEOUT = 14400 # 4h
9+
TESTCASE_TIMEOUT = None # No timeout by default
910

1011
SUMMARY_NUM_PASSING = 5
1112
SUMMARY_NUM_FAILING = 5

testplan/parser.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ def generate_parser(self) -> HelpParser:
114114
"processes. Defaults to 14400s (4h). Set to 0 to disable.",
115115
)
116116

117+
general_group.add_argument(
118+
"--testcase-timeout",
119+
metavar="TESTCASE_TIMEOUT",
120+
default=self._default_options.get(
121+
"testcase_timeout", defaults.TESTCASE_TIMEOUT
122+
),
123+
type=int,
124+
help="Default timeout value in seconds for testcases that don't "
125+
"have an explicit timeout set. Set to 0 or omit to disable default timeout.",
126+
)
127+
117128
general_group.add_argument(
118129
"-i",
119130
"--interactive",

testplan/runnable/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ def get_options(cls):
273273
ConfigOption("timeout", default=defaults.TESTPLAN_TIMEOUT): Or(
274274
None, And(int, lambda t: t >= 0)
275275
),
276+
ConfigOption(
277+
"testcase_timeout", default=defaults.TESTCASE_TIMEOUT
278+
): Or(None, And(int, lambda t: t >= 0)),
276279
# active_loop_sleep impacts cpu usage in interactive mode
277280
ConfigOption("active_loop_sleep", default=0.05): float,
278281
ConfigOption(

testplan/testing/multitest/base.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ def get_options(cls):
226226
),
227227
),
228228
config.ConfigOption("testcase_report_target", default=True): bool,
229+
config.ConfigOption("testcase_timeout"): Or(
230+
None, And(int, lambda t: t >= 0)
231+
),
229232
}
230233

231234

@@ -260,6 +263,10 @@ class MultiTest(testing_base.Test):
260263
:param testcase_report_target: Whether to mark testcases as assertions for filepath
261264
and line number information
262265
:type testcase_report_target: ``bool``
266+
:param testcase_timeout: Default timeout value in seconds for testcases that don't
267+
have an explicit timeout set. If not specified, testcases will have no timeout
268+
by default.
269+
:type testcase_timeout: ``int`` or ``NoneType``
263270
264271
Also inherits all
265272
:py:class:`~testplan.testing.base.Test` options.
@@ -294,6 +301,7 @@ def __init__(
294301
tags=None,
295302
result=result.Result,
296303
testcase_report_target=True,
304+
testcase_timeout=None,
297305
**options,
298306
):
299307
self._tags_index = None
@@ -1222,6 +1230,11 @@ def _run_testcase(
12221230
)
12231231

12241232
time_restriction = getattr(testcase, "timeout", None)
1233+
# Use default testcase timeout from config if no explicit timeout is set
1234+
if time_restriction is None and getattr(
1235+
self.cfg, "testcase_timeout", None
1236+
):
1237+
time_restriction = self.cfg.testcase_timeout
12251238
if time_restriction:
12261239
# pylint: disable=unbalanced-tuple-unpacking
12271240
executed, execution_result = timing.timeout(
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Test for default testcase timeout feature (--testcase-timeout CLI option)."""
2+
3+
import time
4+
5+
from testplan.testing.multitest import MultiTest, testsuite, testcase
6+
from testplan.common.utils.testing import check_report
7+
from testplan.report import (
8+
Status,
9+
TestReport,
10+
TestGroupReport,
11+
TestCaseReport,
12+
ReportCategories,
13+
)
14+
15+
16+
@testsuite
17+
class SuiteWithoutTimeout:
18+
"""Testsuite with testcases that don't have explicit timeout."""
19+
20+
@testcase
21+
def test_passes_quickly(self, env, result):
22+
"""This test should pass."""
23+
result.log("Test completes quickly")
24+
25+
@testcase
26+
def test_takes_time(self, env, result):
27+
"""This test takes 3 seconds but should timeout with default of 2s."""
28+
result.log("Test will sleep for 3 seconds")
29+
time.sleep(3)
30+
31+
32+
@testsuite
33+
class SuiteWithExplicitTimeout:
34+
"""Testsuite with testcases that have explicit timeout."""
35+
36+
@testcase(timeout=5)
37+
def test_with_explicit_timeout(self, env, result):
38+
"""This test has explicit timeout that should override default."""
39+
result.log("Test will sleep for 1 second with 5s timeout")
40+
time.sleep(1)
41+
42+
43+
def test_default_testcase_timeout_from_config(mockplan):
44+
"""Test that default testcase timeout from config works."""
45+
# Create MultiTest with explicit testcase_timeout
46+
multitest = MultiTest(
47+
name="TestDefaultTimeout",
48+
suites=[SuiteWithoutTimeout()],
49+
testcase_timeout=2, # 2 seconds default timeout
50+
)
51+
mockplan.add(multitest)
52+
mockplan.run()
53+
54+
# First test should pass (completes quickly)
55+
assert mockplan.report.status == Status.ERROR
56+
test_report = mockplan.report["TestDefaultTimeout"]["SuiteWithoutTimeout"]
57+
assert test_report["test_passes_quickly"].status == Status.PASSED
58+
# Second test should timeout and have ERROR status
59+
assert test_report["test_takes_time"].status == Status.ERROR
60+
61+
62+
def test_explicit_timeout_overrides_default(mockplan):
63+
"""Test that explicit testcase timeout overrides the default."""
64+
multitest = MultiTest(
65+
name="TestExplicitOverride",
66+
suites=[SuiteWithExplicitTimeout()],
67+
testcase_timeout=1, # 1 second default, but test has 5s explicit
68+
)
69+
mockplan.add(multitest)
70+
mockplan.run()
71+
72+
# Test should pass because explicit timeout (5s) overrides default (1s)
73+
assert mockplan.report.status == Status.PASSED
74+
test_report = mockplan.report["TestExplicitOverride"][
75+
"SuiteWithExplicitTimeout"
76+
]
77+
assert test_report["test_with_explicit_timeout"].status == Status.PASSED
78+
79+
80+
def test_no_default_timeout(mockplan):
81+
"""Test that testcases work normally without default timeout."""
82+
multitest = MultiTest(
83+
name="TestNoDefault",
84+
suites=[SuiteWithoutTimeout()],
85+
# No testcase_timeout specified
86+
)
87+
mockplan.add(multitest)
88+
mockplan.run()
89+
90+
# Both tests should pass without any timeout
91+
assert mockplan.report.status == Status.PASSED
92+
test_report = mockplan.report["TestNoDefault"]["SuiteWithoutTimeout"]
93+
assert test_report["test_passes_quickly"].status == Status.PASSED
94+
assert test_report["test_takes_time"].status == Status.PASSED
95+
96+
97+
def test_testcase_timeout_inheritance_from_parent(mockplan):
98+
"""Test that testcase_timeout is inherited from parent config."""
99+
# Set testcase_timeout in parent (mockplan) config
100+
mockplan.cfg.set_local("testcase_timeout", 2)
101+
102+
# Create MultiTest without explicit testcase_timeout
103+
multitest = MultiTest(
104+
name="TestInheritance",
105+
suites=[SuiteWithoutTimeout()],
106+
)
107+
mockplan.add(multitest)
108+
mockplan.run()
109+
110+
# First test should pass, second should timeout
111+
assert mockplan.report.status == Status.ERROR
112+
test_report = mockplan.report["TestInheritance"]["SuiteWithoutTimeout"]
113+
assert test_report["test_passes_quickly"].status == Status.PASSED
114+
assert test_report["test_takes_time"].status == Status.ERROR

0 commit comments

Comments
 (0)