40
40
function body, ignoring the time it takes when evaluating any fixtures
41
41
used in the test.
42
42
""" .strip ()
43
+ DISABLE_DEBUGGER_DETECTION_DESC = """
44
+ When specified, disables debugger detection. breakpoint(), pdb.set_trace(), etc.
45
+ will be interrupted.
46
+ """ .strip ()
43
47
44
48
# bdb covers pdb, ipdb, and possibly others
45
49
# pydevd covers PyCharm, VSCode, and possibly others
46
50
KNOWN_DEBUGGING_MODULES = {"pydevd" , "bdb" , "pydevd_frame_evaluator" }
47
- Settings = namedtuple ("Settings" , ["timeout" , "method" , "func_only" ])
51
+ Settings = namedtuple (
52
+ "Settings" , ["timeout" , "method" , "func_only" , "disable_debugger_detection" ]
53
+ )
48
54
49
55
50
56
@pytest .hookimpl
@@ -68,9 +74,21 @@ def pytest_addoption(parser):
68
74
choices = ["signal" , "thread" ],
69
75
help = METHOD_DESC ,
70
76
)
77
+ group .addoption (
78
+ "--disable-debugger-detection" ,
79
+ dest = "timeout_disable_debugger_detection" ,
80
+ action = "store_true" ,
81
+ help = DISABLE_DEBUGGER_DETECTION_DESC ,
82
+ )
71
83
parser .addini ("timeout" , TIMEOUT_DESC )
72
84
parser .addini ("timeout_method" , METHOD_DESC )
73
85
parser .addini ("timeout_func_only" , FUNC_ONLY_DESC , type = "bool" , default = False )
86
+ parser .addini (
87
+ "timeout_disable_debugger_detection" ,
88
+ DISABLE_DEBUGGER_DETECTION_DESC ,
89
+ type = "bool" ,
90
+ default = False ,
91
+ )
74
92
75
93
76
94
class TimeoutHooks :
@@ -107,19 +125,24 @@ def pytest_configure(config):
107
125
"""Register the marker so it shows up in --markers output."""
108
126
config .addinivalue_line (
109
127
"markers" ,
110
- "timeout(timeout, method=None, func_only=False): Set a timeout, timeout "
128
+ "timeout(timeout, method=None, func_only=False, "
129
+ "disable_debugger_detection=False): Set a timeout, timeout "
111
130
"method and func_only evaluation on just one test item. The first "
112
131
"argument, *timeout*, is the timeout in seconds while the keyword, "
113
- "*method*, takes the same values as the --timeout_method option. The "
132
+ "*method*, takes the same values as the --timeout-method option. The "
114
133
"*func_only* keyword, when set to True, defers the timeout evaluation "
115
134
"to only the test function body, ignoring the time it takes when "
116
- "evaluating any fixtures used in the test." ,
135
+ "evaluating any fixtures used in the test. The "
136
+ "*disable_debugger_detection* keyword, when set to True, disables "
137
+ "debugger detection, allowing breakpoint(), pdb.set_trace(), etc. "
138
+ "to be interrupted" ,
117
139
)
118
140
119
141
settings = get_env_settings (config )
120
142
config ._env_timeout = settings .timeout
121
143
config ._env_timeout_method = settings .method
122
144
config ._env_timeout_func_only = settings .func_only
145
+ config ._env_timeout_disable_debugger_detection = settings .disable_debugger_detection
123
146
124
147
125
148
@pytest .hookimpl (hookwrapper = True )
@@ -238,7 +261,7 @@ def pytest_timeout_set_timer(item, settings):
238
261
239
262
def handler (signum , frame ):
240
263
__tracebackhide__ = True
241
- timeout_sigalrm (item , settings . timeout )
264
+ timeout_sigalrm (item , settings )
242
265
243
266
def cancel ():
244
267
signal .setitimer (signal .ITIMER_REAL , 0 )
@@ -248,9 +271,7 @@ def cancel():
248
271
signal .signal (signal .SIGALRM , handler )
249
272
signal .setitimer (signal .ITIMER_REAL , settings .timeout )
250
273
elif timeout_method == "thread" :
251
- timer = threading .Timer (
252
- settings .timeout , timeout_timer , (item , settings .timeout )
253
- )
274
+ timer = threading .Timer (settings .timeout , timeout_timer , (item , settings ))
254
275
timer .name = "%s %s" % (__name__ , item .nodeid )
255
276
256
277
def cancel ():
@@ -299,26 +320,40 @@ def get_env_settings(config):
299
320
method = DEFAULT_METHOD
300
321
301
322
func_only = config .getini ("timeout_func_only" )
302
- return Settings (timeout , method , func_only )
323
+
324
+ disable_debugger_detection = config .getvalue ("timeout_disable_debugger_detection" )
325
+ if disable_debugger_detection is None :
326
+ ini = config .getini ("timeout_disable_debugger_detection" )
327
+ if ini :
328
+ disable_debugger_detection = _validate_disable_debugger_detection (
329
+ ini , "config file"
330
+ )
331
+
332
+ return Settings (timeout , method , func_only , disable_debugger_detection )
303
333
304
334
305
335
def _get_item_settings (item , marker = None ):
306
336
"""Return (timeout, method) for an item."""
307
- timeout = method = func_only = None
337
+ timeout = method = func_only = disable_debugger_detection = None
308
338
if not marker :
309
339
marker = item .get_closest_marker ("timeout" )
310
340
if marker is not None :
311
341
settings = _parse_marker (item .get_closest_marker (name = "timeout" ))
312
342
timeout = _validate_timeout (settings .timeout , "marker" )
313
343
method = _validate_method (settings .method , "marker" )
314
344
func_only = _validate_func_only (settings .func_only , "marker" )
345
+ disable_debugger_detection = _validate_disable_debugger_detection (
346
+ settings .disable_debugger_detection , "marker"
347
+ )
315
348
if timeout is None :
316
349
timeout = item .config ._env_timeout
317
350
if method is None :
318
351
method = item .config ._env_timeout_method
319
352
if func_only is None :
320
353
func_only = item .config ._env_timeout_func_only
321
- return Settings (timeout , method , func_only )
354
+ if disable_debugger_detection is None :
355
+ disable_debugger_detection = item .config ._env_timeout_disable_debugger_detection
356
+ return Settings (timeout , method , func_only , disable_debugger_detection )
322
357
323
358
324
359
def _parse_marker (marker ):
@@ -329,14 +364,16 @@ def _parse_marker(marker):
329
364
"""
330
365
if not marker .args and not marker .kwargs :
331
366
raise TypeError ("Timeout marker must have at least one argument" )
332
- timeout = method = func_only = NOTSET = object ()
367
+ timeout = method = func_only = disable_debugger_detection = NOTSET = object ()
333
368
for kw , val in marker .kwargs .items ():
334
369
if kw == "timeout" :
335
370
timeout = val
336
371
elif kw == "method" :
337
372
method = val
338
373
elif kw == "func_only" :
339
374
func_only = val
375
+ elif kw == "disable_debugger_detection" :
376
+ disable_debugger_detection = val
340
377
else :
341
378
raise TypeError ("Invalid keyword argument for timeout marker: %s" % kw )
342
379
if len (marker .args ) >= 1 and timeout is not NOTSET :
@@ -347,15 +384,23 @@ def _parse_marker(marker):
347
384
raise TypeError ("Multiple values for method argument of timeout marker" )
348
385
elif len (marker .args ) >= 2 :
349
386
method = marker .args [1 ]
350
- if len (marker .args ) > 2 :
387
+ if len (marker .args ) >= 3 and disable_debugger_detection is not NOTSET :
388
+ raise TypeError (
389
+ "Multiple values for disable_debugger_detection argument of timeout marker"
390
+ )
391
+ elif len (marker .args ) >= 3 :
392
+ disable_debugger_detection = marker .args [2 ]
393
+ if len (marker .args ) > 3 :
351
394
raise TypeError ("Too many arguments for timeout marker" )
352
395
if timeout is NOTSET :
353
396
timeout = None
354
397
if method is NOTSET :
355
398
method = None
356
399
if func_only is NOTSET :
357
400
func_only = None
358
- return Settings (timeout , method , func_only )
401
+ if disable_debugger_detection is NOTSET :
402
+ disable_debugger_detection = None
403
+ return Settings (timeout , method , func_only , disable_debugger_detection )
359
404
360
405
361
406
def _validate_timeout (timeout , where ):
@@ -383,14 +428,25 @@ def _validate_func_only(func_only, where):
383
428
return func_only
384
429
385
430
386
- def timeout_sigalrm (item , timeout ):
431
+ def _validate_disable_debugger_detection (disable_debugger_detection , where ):
432
+ if disable_debugger_detection is None :
433
+ return None
434
+ if not isinstance (disable_debugger_detection , bool ):
435
+ raise ValueError (
436
+ "Invalid disable_debugger_detection value %s from %s"
437
+ % (disable_debugger_detection , where )
438
+ )
439
+ return disable_debugger_detection
440
+
441
+
442
+ def timeout_sigalrm (item , settings ):
387
443
"""Dump stack of threads and raise an exception.
388
444
389
445
This will output the stacks of any threads other then the
390
446
current to stderr and then raise an AssertionError, thus
391
447
terminating the test.
392
448
"""
393
- if is_debugging ():
449
+ if not settings . disable_debugger_detection and is_debugging ():
394
450
return
395
451
__tracebackhide__ = True
396
452
nthreads = len (threading .enumerate ())
@@ -399,16 +455,16 @@ def timeout_sigalrm(item, timeout):
399
455
dump_stacks ()
400
456
if nthreads > 1 :
401
457
write_title ("Timeout" , sep = "+" )
402
- pytest .fail ("Timeout >%ss" % timeout )
458
+ pytest .fail ("Timeout >%ss" % settings . timeout )
403
459
404
460
405
- def timeout_timer (item , timeout ):
461
+ def timeout_timer (item , settings ):
406
462
"""Dump stack of threads and call os._exit().
407
463
408
464
This disables the capturemanager and dumps stdout and stderr.
409
465
Then the stacks are dumped and os._exit(1) is called.
410
466
"""
411
- if is_debugging ():
467
+ if not settings . disable_debugger_detection and is_debugging ():
412
468
return
413
469
try :
414
470
capman = item .config .pluginmanager .getplugin ("capturemanager" )
0 commit comments