18
18
import pytest
19
19
20
20
21
+ __all__ = ("is_debugging" , "Settings" )
22
+
23
+
21
24
HAVE_SIGALRM = hasattr (signal , "SIGALRM" )
22
25
if HAVE_SIGALRM :
23
26
DEFAULT_METHOD = "signal"
@@ -70,6 +73,35 @@ def pytest_addoption(parser):
70
73
parser .addini ("timeout_func_only" , FUNC_ONLY_DESC , type = "bool" )
71
74
72
75
76
+ class TimeoutHooks :
77
+ """Timeout specific hooks."""
78
+
79
+ @pytest .hookspec (firstresult = True )
80
+ def pytest_timeout_set_timer (item , settings ):
81
+ """Called at timeout setup.
82
+
83
+ 'item' is a pytest node to setup timeout for.
84
+
85
+ Can be overridden by plugins for alternative timeout implementation strategies.
86
+
87
+ """
88
+
89
+ @pytest .hookspec (firstresult = True )
90
+ def pytest_timeout_cancel_timer (item ):
91
+ """Called at timeout teardown.
92
+
93
+ 'item' is a pytest node which was used for timeout setup.
94
+
95
+ Can be overridden by plugins for alternative timeout implementation strategies.
96
+
97
+ """
98
+
99
+
100
+ def pytest_addhooks (pluginmanager ):
101
+ """Register timeout-specific hooks."""
102
+ pluginmanager .add_hookspecs (TimeoutHooks )
103
+
104
+
73
105
@pytest .hookimpl
74
106
def pytest_configure (config ):
75
107
"""Register the marker so it shows up in --markers output."""
@@ -98,12 +130,14 @@ def pytest_runtest_protocol(item):
98
130
teardown, then this hook installs the timeout. Otherwise
99
131
pytest_runtest_call is used.
100
132
"""
101
- func_only = get_func_only_setting (item )
102
- if func_only is False :
103
- timeout_setup (item )
133
+ hooks = item .config .pluginmanager .hook
134
+ settings = _get_item_settings (item )
135
+ is_timeout = settings .timeout is not None and settings .timeout > 0
136
+ if is_timeout and settings .func_only is False :
137
+ hooks .pytest_timeout_set_timer (item = item , settings = settings )
104
138
yield
105
- if func_only is False :
106
- timeout_teardown ( item )
139
+ if is_timeout and settings . func_only is False :
140
+ hooks . pytest_timeout_cancel_timer ( item = item )
107
141
108
142
109
143
@pytest .hookimpl (hookwrapper = True )
@@ -113,12 +147,14 @@ def pytest_runtest_call(item):
113
147
If the timeout is set on only the test function this hook installs
114
148
the timeout, otherwise pytest_runtest_protocol is used.
115
149
"""
116
- func_only = get_func_only_setting (item )
117
- if func_only is True :
118
- timeout_setup (item )
150
+ hooks = item .config .pluginmanager .hook
151
+ settings = _get_item_settings (item )
152
+ is_timeout = settings .timeout is not None and settings .timeout > 0
153
+ if is_timeout and settings .func_only is True :
154
+ hooks .pytest_timeout_set_timer (item = item , settings = settings )
119
155
yield
120
- if func_only is True :
121
- timeout_teardown ( item )
156
+ if is_timeout and settings . func_only is True :
157
+ hooks . pytest_timeout_cancel_timer ( item = item )
122
158
123
159
124
160
@pytest .hookimpl (tryfirst = True )
@@ -138,7 +174,8 @@ def pytest_report_header(config):
138
174
@pytest .hookimpl (tryfirst = True )
139
175
def pytest_exception_interact (node ):
140
176
"""Stop the timeout when pytest enters pdb in post-mortem mode."""
141
- timeout_teardown (node )
177
+ hooks = node .config .pluginmanager .hook
178
+ hooks .pytest_timeout_cancel_timer (item = node )
142
179
143
180
144
181
@pytest .hookimpl
@@ -187,13 +224,10 @@ def is_debugging(trace_func=None):
187
224
SUPPRESS_TIMEOUT = False
188
225
189
226
190
- def timeout_setup (item ):
227
+ @pytest .hookimpl (trylast = True )
228
+ def pytest_timeout_set_timer (item , settings ):
191
229
"""Setup up a timeout trigger and handler."""
192
- params = get_params (item )
193
- if params .timeout is None or params .timeout <= 0 :
194
- return
195
-
196
- timeout_method = params .method
230
+ timeout_method = settings .method
197
231
if (
198
232
timeout_method == "signal"
199
233
and threading .current_thread () is not threading .main_thread ()
@@ -204,17 +238,19 @@ def timeout_setup(item):
204
238
205
239
def handler (signum , frame ):
206
240
__tracebackhide__ = True
207
- timeout_sigalrm (item , params .timeout )
241
+ timeout_sigalrm (item , settings .timeout )
208
242
209
243
def cancel ():
210
244
signal .setitimer (signal .ITIMER_REAL , 0 )
211
245
signal .signal (signal .SIGALRM , signal .SIG_DFL )
212
246
213
247
item .cancel_timeout = cancel
214
248
signal .signal (signal .SIGALRM , handler )
215
- signal .setitimer (signal .ITIMER_REAL , params .timeout )
249
+ signal .setitimer (signal .ITIMER_REAL , settings .timeout )
216
250
elif timeout_method == "thread" :
217
- timer = threading .Timer (params .timeout , timeout_timer , (item , params .timeout ))
251
+ timer = threading .Timer (
252
+ settings .timeout , timeout_timer , (item , settings .timeout )
253
+ )
218
254
timer .name = "%s %s" % (__name__ , item .nodeid )
219
255
220
256
def cancel ():
@@ -223,9 +259,11 @@ def cancel():
223
259
224
260
item .cancel_timeout = cancel
225
261
timer .start ()
262
+ return True
226
263
227
264
228
- def timeout_teardown (item ):
265
+ @pytest .hookimpl (trylast = True )
266
+ def pytest_timeout_cancel_timer (item ):
229
267
"""Cancel the timeout trigger if it was set."""
230
268
# When skipping is raised from a pytest_runtest_setup function
231
269
# (as is the case when using the pytest.mark.skipif marker) we
@@ -234,6 +272,7 @@ def timeout_teardown(item):
234
272
cancel = getattr (item , "cancel_timeout" , None )
235
273
if cancel :
236
274
cancel ()
275
+ return True
237
276
238
277
239
278
def get_env_settings (config ):
@@ -268,21 +307,7 @@ def get_env_settings(config):
268
307
return Settings (timeout , method , func_only or False )
269
308
270
309
271
- def get_func_only_setting (item ):
272
- """Return the func_only setting for an item."""
273
- func_only = None
274
- marker = item .get_closest_marker ("timeout" )
275
- if marker :
276
- settings = get_params (item , marker = marker )
277
- func_only = _validate_func_only (settings .func_only , "marker" )
278
- if func_only is None :
279
- func_only = item .config ._env_timeout_func_only
280
- if func_only is None :
281
- func_only = False
282
- return func_only
283
-
284
-
285
- def get_params (item , marker = None ):
310
+ def _get_item_settings (item , marker = None ):
286
311
"""Return (timeout, method) for an item."""
287
312
timeout = method = func_only = None
288
313
if not marker :
@@ -298,6 +323,8 @@ def get_params(item, marker=None):
298
323
method = item .config ._env_timeout_method
299
324
if func_only is None :
300
325
func_only = item .config ._env_timeout_func_only
326
+ if func_only is None :
327
+ func_only = False
301
328
return Settings (timeout , method , func_only )
302
329
303
330
0 commit comments