10
10
- However, there effects are isolated since each test is run in a
11
11
separate Python subprocess.
12
12
"""
13
- import contextlib
13
+ import concurrent . futures
14
14
import functools
15
15
import inspect
16
16
import linecache
17
17
import os
18
18
import subprocess
19
19
import sys
20
+ import time
20
21
import tempfile
21
22
import textwrap
23
+ import threading
22
24
import pytest
23
25
from ast import literal_eval
24
26
from io import StringIO
25
27
from types import FrameType
26
- from typing import Any , Callable , Generator , List , Optional , Literal , Union
28
+ from typing import Any , Callable , List , Literal , Union
27
29
from line_profiler import LineProfiler
28
30
29
31
@@ -226,7 +228,7 @@ class MyException(Exception):
226
228
pass
227
229
228
230
229
- # Test: callbacks preserved before/after using the profiler
231
+ # Tests
230
232
231
233
232
234
def _test_helper_callback_preservation (
@@ -235,7 +237,7 @@ def _test_helper_callback_preservation(
235
237
assert sys .gettrace () is callback , f'can\' t set trace to { callback !r} '
236
238
profile = LineProfiler (wrap_trace = False )
237
239
profile .enable_by_count ()
238
- assert sys .gettrace () is profile , f 'can\' t set trace to the profiler'
240
+ assert sys .gettrace () is profile , 'can\' t set trace to the profiler'
239
241
profile .disable_by_count ()
240
242
assert sys .gettrace () is callback , f'trace not restored to { callback !r} '
241
243
sys .settrace (None )
@@ -251,9 +253,6 @@ def test_callback_preservation():
251
253
_test_helper_callback_preservation (lambda frame , event , arg : None )
252
254
253
255
254
- # Test: profiler can wrap around an existing callback and use it
255
-
256
-
257
256
@pytest .mark .parametrize (
258
257
('label' , 'use_profiler' , 'wrap_trace' ),
259
258
[('base case' , False , False ),
@@ -279,9 +278,9 @@ def test_callback_wrapping(
279
278
foo_like = foo
280
279
trace_preserved = True
281
280
if trace_preserved :
282
- exp_output = [f'foo: spam = { spam } ' for spam in range (1 , 6 )]
281
+ exp_logs = [f'foo: spam = { spam } ' for spam in range (1 , 6 )]
283
282
else :
284
- exp_output = []
283
+ exp_logs = []
285
284
286
285
assert sys .gettrace () is my_callback , 'can\' t set custom trace'
287
286
my_callback .emit_debug = True
@@ -293,7 +292,7 @@ def test_callback_wrapping(
293
292
# Check that the existing trace function has been called where
294
293
# appropriate
295
294
print (f'Logs: { logs !r} ' )
296
- assert logs == exp_output , f'expected logs = { exp_output !r} , got { logs !r} '
295
+ assert logs == exp_logs , f'expected logs = { exp_logs !r} , got { logs !r} '
297
296
298
297
# Check that the profiling is as expected: 5 hits on the
299
298
# incrementation line
@@ -308,10 +307,6 @@ def test_callback_wrapping(
308
307
assert nhits == 5 , f'expected 5 profiler hits, got { nhits !r} '
309
308
310
309
311
- # Test: if the wrapped existing callback unsets itself, the profiler's
312
- # callback stops calling it, but profiling continues
313
-
314
-
315
310
@pytest .mark .parametrize (
316
311
('label' , 'use_profiler' , 'enable_count' ),
317
312
[('base case' , False , 0 ),
@@ -396,10 +391,6 @@ def test_wrapping_throwing_callback(
396
391
f'profiler hits, got { nhits !r} ' )
397
392
398
393
399
- # Test: if the wrapped existing callback set `frame.f_trace_lines`, the
400
- # profiler's callback catches and reverts that
401
-
402
-
403
394
@pytest .mark .parametrize (('label' , 'use_profiler' ),
404
395
[('base case' , False ), ('profiled' , True )])
405
396
@isolate_test_in_subproc
@@ -432,8 +423,8 @@ def test_wrapping_line_event_disabling_callback(label: str,
432
423
# Check that the trace function has been called exactly once on the
433
424
# line event, and once on the return event
434
425
print (f'Logs: { logs !r} ' )
435
- exp_output = [f 'foo: spam = 1' , 'Returning from `foo()`' ]
436
- assert logs == exp_output , f'expected logs = { exp_output !r} , got { logs !r} '
426
+ exp_logs = ['foo: spam = 1' , 'Returning from `foo()`' ]
427
+ assert logs == exp_logs , f'expected logs = { exp_logs !r} , got { logs !r} '
437
428
438
429
# Check that the profiling is as expected: 5 hits on the
439
430
# incrementation line
@@ -446,3 +437,82 @@ def test_wrapping_line_event_disabling_callback(label: str,
446
437
line , = (line for line in out .splitlines () if '+=' in line )
447
438
nhits = int (line .split ()[1 ])
448
439
assert nhits == 5 , f'expected 5 profiler hits, got { nhits !r} '
440
+
441
+
442
+ def _test_helper_wrapping_thread_local_callbacks (
443
+ profile : Union [LineProfiler , None ], sleep : float = .0625 ) -> str :
444
+ logs = []
445
+ if threading .current_thread () == threading .main_thread ():
446
+ thread_label = 'main'
447
+ func = foo
448
+ my_callback = get_incr_logger (logs , func )
449
+ exp_logs = [f'foo: spam = { spam } ' for spam in range (1 , 6 )]
450
+ else :
451
+ thread_label = 'side'
452
+ func = bar
453
+ my_callback = get_return_logger (logs )
454
+ exp_logs = ['Returning from `bar()`' ]
455
+ if profile is None :
456
+ func_like = func
457
+ else :
458
+ func_like = profile (func )
459
+ print (f'Thread: { threading .get_ident ()} ({ thread_label } )' )
460
+
461
+ # Check result
462
+ sys .settrace (my_callback )
463
+ assert sys .gettrace () is my_callback , 'can\' t set custom trace'
464
+ my_callback .emit_debug = True
465
+ x = func_like (5 )
466
+ my_callback .emit_debug = False
467
+ assert x == 15 , f'expected `{ func .__name__ } (5) = 15`, got { x !r} '
468
+ assert sys .gettrace () is my_callback , 'trace not restored afterwards'
469
+
470
+ # Check logs
471
+ print (f'Logs ({ thread_label } thread): { logs !r} ' )
472
+ assert logs == exp_logs , f'expected logs = { exp_logs !r} , got { logs !r} '
473
+ time .sleep (sleep )
474
+ return '\n ' .join (logs )
475
+
476
+
477
+ @pytest .mark .parametrize (('label' , 'use_profiler' ),
478
+ [('base case' , False ), ('profiled' , True )])
479
+ @isolate_test_in_subproc
480
+ def test_wrapping_thread_local_callbacks (label : str ,
481
+ use_profiler : bool ) -> None :
482
+ """
483
+ Test in a subprocess that the profiler properly handles thread-local
484
+ `sys` trace callbacks.
485
+ """
486
+ profile = LineProfiler (wrap_trace = True ) if use_profiler else None
487
+ expected_results = {
488
+ # From the main thread
489
+ '\n ' .join (f'foo: spam = { spam } ' for spam in range (1 , 6 )),
490
+ # From the other thread
491
+ 'Returning from `bar()`' ,
492
+ }
493
+
494
+ # Run tasks (and so some basic checks)
495
+ results = set ()
496
+ with concurrent .futures .ThreadPoolExecutor (max_workers = 1 ) as executor :
497
+ tasks = []
498
+ tasks .append (executor .submit ( # This is run on a side thread
499
+ _test_helper_wrapping_thread_local_callbacks , profile ))
500
+ # This is run on the main thread
501
+ results .add (_test_helper_wrapping_thread_local_callbacks (profile ))
502
+ results .update (future .result ()
503
+ for future in concurrent .futures .as_completed (tasks ))
504
+ assert results == expected_results , (f'expected { expected_results !r} , '
505
+ f'got { results !r} ' )
506
+
507
+ # Check profiling
508
+ if profile is None :
509
+ return
510
+ with StringIO () as sio :
511
+ profile .print_stats (stream = sio , summarize = True )
512
+ out = sio .getvalue ()
513
+ print (out )
514
+ for var in 'spam' , 'ham' :
515
+ line , = (line for line in out .splitlines ()
516
+ if line .endswith ('+= ' + var ))
517
+ nhits = int (line .split ()[1 ])
518
+ assert nhits == 5 , f'expected 5 profiler hits, got { nhits !r} '
0 commit comments