32
32
import io
33
33
import contextlib
34
34
import gc
35
+ import os
36
+ import functools
35
37
from test .support import run_unittest
36
38
37
39
# emit warnings about uncollectable objects
@@ -65,10 +67,75 @@ def require_one_thread(testcase):
65
67
return testcase
66
68
67
69
68
- def get_current_watchdog_list ():
70
+ def testcase_leaks_references (leak_reason , soft_switching = None ):
71
+ """Skip test, which leak references during leak tests.
72
+
73
+ If a test leaks references, which happens if the thread of a tasklet with a C-stack dies,
74
+ you must decorate the test case with this decorator.
75
+
76
+ Note: If you know the leaking object, you can try to use the hackish function
77
+ decref_leaked_object()
78
+ """
79
+ def decorator (testcase ):
80
+ @functools .wraps (testcase )
81
+ def wrapper (self ):
82
+ if soft_switching is not None and stackless .enable_softswitch (None ) != soft_switching :
83
+ # the leak happens only if soft switching is enables/disabled
84
+ return testcase (self )
85
+ for frameinfo in inspect .stack (0 ):
86
+ # print("frameinfo[3]", frameinfo[3], file=sys.stderr)
87
+ if frameinfo [3 ] == "dash_R" :
88
+ # it is a test.regrtest -R: run
89
+ return self .skipTest ("Test leaks references: " + leak_reason )
90
+ return testcase (self )
91
+ return wrapper
92
+ return decorator
93
+
94
+
95
+ # decref_leaked_object() works, but it is not as useful as I expected, because
96
+ # a C-stack usually contains several references to None and other objects. All in all
97
+ # there are more references than we can identify and handle manually.
98
+ #
99
+ # _Py_DecRef = ctypes.pythonapi.Py_DecRef
100
+ # _Py_DecRef.argtypes = [ctypes.py_object]
101
+ # _Py_DecRef.restype = None
102
+ #
103
+ #
104
+ # def decref_leaked_object(obj):
105
+ # """Py_DecRef an object, that would otherwise leak.
106
+ #
107
+ # Used to forget references held by a C-stack, whose thread already died.
108
+ # """
109
+ # # Check, that obj is really leaked
110
+ # def get_rc_delta(obj):
111
+ # gc.collect()
112
+ # rc = sys.getrefcount(obj) - 1
113
+ # n_refferers = len(gc.get_referrers(obj))
114
+ # return rc - n_refferers
115
+ # # Quality check
116
+ # assert get_rc_delta(object()) == 0
117
+ # delta = get_rc_delta(obj)
118
+ # if delta > 0:
119
+ # _Py_DecRef(obj)
120
+ # else:
121
+ # raise RuntimeError("There is no ref leak for obj. Missing referrers %d" % (delta,))
122
+
123
+
124
+ def get_watchdog_list (threadid ):
125
+ """Get the watchdog list of a thread.
126
+
127
+ Contrary to :func:`get_current_watchdog_list` this function does
128
+ not create a watchdog list, if it does not already exist.
129
+ """
69
130
# The second argument of get_thread_info() is intentionally undocumented.
70
131
# See C source.
71
- watchdog_list = stackless .get_thread_info (- 1 , 1 << 31 )[3 ]
132
+ return stackless .get_thread_info (threadid , 1 << 31 )[3 ]
133
+
134
+
135
+ def get_current_watchdog_list ():
136
+ """Get the watchdog list of the current thread
137
+ """
138
+ watchdog_list = get_watchdog_list (- 1 )
72
139
if isinstance (watchdog_list , list ):
73
140
return watchdog_list
74
141
# The watchdog list has not been created. Force its creation.
@@ -87,11 +154,72 @@ def get_current_watchdog_list():
87
154
t .insert ()
88
155
if scheduled :
89
156
assert stackless .current .next == scheduled [0 ]
90
- watchdog_list = stackless . get_thread_info (- 1 , 1 << 31 )[ 3 ]
157
+ watchdog_list = get_watchdog_list (- 1 )
91
158
assert isinstance (watchdog_list , list )
92
159
return watchdog_list
93
160
94
161
162
+ def get_tasklets_with_cstate ():
163
+ """Return a list of all tasklets with a C-stack.
164
+ """
165
+ tlets = []
166
+ current = stackless .current
167
+ if current .nesting_level > 0 :
168
+ tlets .append (current )
169
+ with stackless .atomic ():
170
+ cscurrent = current .cstate
171
+ cs = cscurrent .next
172
+ while cs is not cscurrent :
173
+ t = cs .task
174
+ if (t is not None and
175
+ t .cstate is cs and
176
+ t .alive and
177
+ t .nesting_level > 0 ):
178
+ assert t not in tlets
179
+ tlets .append (t )
180
+ cs = cs .next
181
+ return tlets
182
+
183
+
184
+ class SwitchRecorder (object ):
185
+ def __init__ (self , old_scb ):
186
+ self .ids = {}
187
+ if stackless .main is stackless .current :
188
+ self .ids [id (stackless .main )] = "main/current"
189
+ else :
190
+ self .ids [id (stackless .main )] = "main"
191
+ self .ids [id (stackless .current )] = "current"
192
+ self .switches = []
193
+ self .old_scb = old_scb
194
+
195
+ def __call__ (self , prev_tlet , next_tlet ):
196
+ # print("%s -> %s" % (id(prev_tlet), id(next_tlet)), file=sys.stderr)
197
+ self .switches .append ((None if prev_tlet is None else id (prev_tlet ),
198
+ None if next_tlet is None else id (next_tlet )))
199
+ if self .old_scb is not None :
200
+ return self .old_scb (prev_tlet , next_tlet )
201
+
202
+ def id2str (self , tlet_id ):
203
+ if tlet_id is None :
204
+ return "None"
205
+ try :
206
+ nr = self .ids [tlet_id ]
207
+ except KeyError :
208
+ self .ids [tlet_id ] = nr = "tlet %d" % (len (self .ids ),)
209
+ return nr
210
+
211
+ def __str__ (self ):
212
+ s = ["" , "tasklet switches (%d)" % (len (self .switches ),)]
213
+ for (p , n ) in self .switches :
214
+ s .append ("%s -> %s" % (self .id2str (p ), self .id2str (n )))
215
+ return os .linesep .join (s )
216
+
217
+ def print (self , file = None ):
218
+ if file is None :
219
+ file = sys .stderr
220
+ print (str (self ), file = file )
221
+
222
+
95
223
class StacklessTestCaseMixin (object ):
96
224
def skipUnlessSoftswitching (self ):
97
225
if not stackless .enable_softswitch (None ):
@@ -190,6 +318,109 @@ def _checkSignature(self, func, nb_mandatory, accept_arbitrary, additionalArg, *
190
318
# only required args as kw-args
191
319
yield func (** dict ((k , kwargs [k ]) for k in names [:nb_mandatory ]))
192
320
321
+ def trace_tasklet_switches (self ):
322
+ old_scb = stackless .get_schedule_callback ()
323
+ switch_recorder = SwitchRecorder (old_scb )
324
+ self .addCleanup (stackless .set_schedule_callback , old_scb )
325
+ self .addCleanup (switch_recorder .print )
326
+ stackless .set_schedule_callback (switch_recorder )
327
+
328
+ def register_tasklet_name (self , tlet , name ):
329
+ scb = stackless .get_schedule_callback ()
330
+ if isinstance (scb , SwitchRecorder ):
331
+ if isinstance (tlet , stackless .tasklet ):
332
+ tlet = id (tlet )
333
+ self .assertIsInstance (tlet , int )
334
+ scb .ids [tlet ] = name
335
+
336
+ def refleak_hunting_record_baseline (self ):
337
+ """Record a baseline for hunting reference leaks.
338
+
339
+ This method and the methods refleak_hunting_find_leaks() and
340
+ refleak_hunting_print() can be used to identify reference leaks.
341
+
342
+ The basic idea is simple: For objects which take part in garbage collection,
343
+ the number of referrers matches the reference count. In case of a missing Py_DECREF,
344
+ we can observe a surplus reference count. Unfortunately, in reality the garbage collector
345
+ doesn't care about references from non container objects or local variables of C-functions.
346
+ Therefore, we record a baseline first.
347
+
348
+ Application: Just call bracket the suspicious code like this::
349
+
350
+ self.refleak_hunting_record_baseline()
351
+ # suspicious code goes here
352
+ ...
353
+ self.refleak_hunting_find_leaks()
354
+
355
+ In case you need this functions in a C-Python test suite module, use the following
356
+ code snippet::
357
+
358
+ import os
359
+ import sysconfig
360
+ import sys
361
+ sys.path.append(os.path.join(sysconfig.get_path("data"), "Stackless", "unittests"))
362
+ from support import StacklessTestCaseMixin
363
+
364
+ ...
365
+
366
+ class TestCase(unittest.TestCase, StacklessTestCaseMixin):
367
+ ...
368
+ """
369
+ gc .collect ()
370
+ self .singleton_ref_counts_baseline = [sys .getrefcount (None )]
371
+ oids = set (id (o ) for o in gc .get_objects () if len (gc .get_referrers (o )) <= 2 )
372
+ oids .add (id (sys ._getframe (1 )))
373
+ self .oids_insufficient_referrers = oids
374
+
375
+ def refleak_hunting_find_leaks (self , objects = None ):
376
+ """Detect potential leaked objects.
377
+ """
378
+ gc .collect ()
379
+ if objects is None :
380
+ objects = gc .get_objects ()
381
+ gc .collect ()
382
+ self .singleton_ref_counts = [sys .getrefcount (None )]
383
+ self .assertIsInstance (objects , list )
384
+ oids = self .oids_insufficient_referrers
385
+ # add the watchdog list to the baseline.
386
+ wd_list = get_watchdog_list (- 1 )
387
+ if isinstance (wd_list , list ):
388
+ oids .add (id (wd_list ))
389
+ # add the current frame
390
+ oids .add (id (sys ._getframe ()))
391
+ # add the parent frame
392
+ oids .add (id (sys ._getframe (1 )))
393
+ # compute objects, which are alive, but have to less referrers to
394
+ # justify their live.
395
+ candidates = []
396
+ for i in xrange (len (objects )):
397
+ if id (objects [i ]) in oids :
398
+ continue
399
+ if len (gc .get_referrers (objects [i ])) <= 1 :
400
+ candidates .append (objects [i ])
401
+ del objects [:]
402
+ del self .oids_insufficient_referrers
403
+ self .refleak_candidates = candidates
404
+ self .addCleanup (self .refleak_hunting_print )
405
+ return candidates
406
+
407
+ def refleak_hunting_print (self , file = None ):
408
+ """Print the result.
409
+
410
+ Print the result of the reference leak hunting to sys.stderr.
411
+ """
412
+ if file is None :
413
+ file = sys .stderr
414
+ print ("" , file = file )
415
+ print ("Leak hunting:" , file = file )
416
+ for (i , name ) in enumerate (("None" ,)):
417
+ a = self .singleton_ref_counts_baseline [i ]
418
+ b = self .singleton_ref_counts [i ]
419
+ print ("Ref count of %s: %d - %d = %d" % (name , a , b , a - b ), file = file )
420
+ print ("Number of candidates" , len (self .refleak_candidates ), file = file )
421
+ for (i , o ) in enumerate (self .refleak_candidates ):
422
+ print ("%d: %r %r" % (i , type (o ), o ), file = file )
423
+
193
424
194
425
# call the class method prepare_test_methods(cls) after creating the
195
426
# class. This method can be used to modify the newly created class
@@ -251,7 +482,8 @@ def prepare_pickle_test_method(cls, func, name=None):
251
482
for i in range (0 , pickle .HIGHEST_PROTOCOL + 1 ):
252
483
for p_letter in ("C" , "P" ):
253
484
def test (self , method = func , proto = i , pickle_module = p_letter , unpickle_module = p_letter ):
254
- self .assertTrue (self ._StacklessTestCase__setup_called , "Broken test case: it didn't call super(..., self).setUp()" )
485
+ self .assertTrue (self ._StacklessTestCase__setup_called ,
486
+ "Broken test case: it didn't call super(..., self).setUp()" )
255
487
self ._pickle_protocol = proto
256
488
self ._pickle_module = pickle_module
257
489
self ._unpickle_module = unpickle_module
@@ -284,7 +516,7 @@ def prepare_test_methods(cls):
284
516
if inspect .ismethod (m ):
285
517
m = m .__func__
286
518
prepare = getattr (m , "prepare" , cls .prepare_test_method )
287
- for x in prepare .__func__ (cls , m , n ):
519
+ for x in prepare .__func__ (cls , m , n ): # @UnusedVariable
288
520
pass
289
521
290
522
__setup_called = False
@@ -317,20 +549,45 @@ def setUpStacklessTestCase(self):
317
549
self .addCleanup (stackless .enable_softswitch , stackless .enable_softswitch (self .__enable_softswitch ))
318
550
319
551
self .__active_test_cases [id (self )] = self
552
+ watchdog_list = get_watchdog_list (- 1 )
553
+ if watchdog_list is not None :
554
+ self .assertListEqual (watchdog_list , [None ], "Watchdog list is not empty: " + repr (watchdog_list ))
320
555
if withThreads and self .__preexisting_threads is None :
321
556
self .__preexisting_threads = frozenset (threading .enumerate ())
557
+ for (watchdog_list , tid ) in [(get_watchdog_list (tid ), tid )
558
+ for tid in stackless .threads if tid != stackless .current .thread_id ]:
559
+ if watchdog_list is None :
560
+ continue
561
+ self .assertListEqual (watchdog_list , [None ],
562
+ "Thread %d: watchdog list is not empty: %r" % (tid , watchdog_list ))
322
563
return len (self .__preexisting_threads )
323
564
return 1
324
565
325
566
def setUp (self ):
326
- self .assertEqual (stackless .getruncount (), 1 , "Leakage from other tests, with %d tasklets still in the scheduler" % (stackless .getruncount () - 1 ))
567
+ self .assertEqual (stackless .getruncount (), 1 ,
568
+ "Leakage from other tests, with %d tasklets still in the scheduler" %
569
+ (stackless .getruncount () - 1 ))
327
570
expected_thread_count = self .setUpStacklessTestCase ()
328
571
if withThreads :
329
572
active_count = threading .active_count ()
330
- self .assertEqual (active_count , expected_thread_count , "Leakage from other threads, with %d threads running (%d expected)" % (active_count , expected_thread_count ))
573
+ self .assertEqual (active_count , expected_thread_count ,
574
+ "Leakage from other threads, with %d threads running (%d expected)" %
575
+ (active_count , expected_thread_count ))
331
576
332
577
def tearDown (self ):
333
- # Tasklets created in pickling tests can be left in the scheduler when they finish. We can feel free to
578
+ # Test, that stackless errorhandler is None and reset it
579
+ self .assertIsNone (stackless .set_error_handler (None ))
580
+
581
+ # Test, that switch_trap level is 0 and set the level back to 0
582
+ try :
583
+ # get the level without changing it
584
+ st_level = stackless .switch_trap (0 )
585
+ self .assertEqual (st_level , 0 , "switch_trap is %d" % (st_level ,))
586
+ except AssertionError :
587
+ # change the level so that the result is 0
588
+ stackless .switch_trap (- st_level )
589
+ raise
590
+ # Tasklets created in various tests can be left in the scheduler when they finish. We can feel free to
334
591
# clean them up for the tests. Any tests that expect to exit with no leaked tasklets should do explicit
335
592
# assertions to check.
336
593
self .assertTrue (self .__setup_called , "Broken test case: it didn't call super(..., self).setUp()" )
@@ -341,9 +598,27 @@ def tearDown(self):
341
598
next_ = current .next
342
599
current .kill ()
343
600
current = next_
601
+ # Tasklets with C-stack can create reference leaks, if the C-stack holds a reference,
602
+ # that keeps the tasklet-object alive. A common case is the call of a tasklet or channel method,
603
+ # which causes a tasklet switch. The transient bound-method object keeps the tasklet alive.
604
+ # Here we kill such tasklets.
605
+ for current in get_tasklets_with_cstate ():
606
+ if current .blocked :
607
+ # print("Killing blocked tasklet", current, file=sys.stderr)
608
+ current .kill ()
344
609
run_count = stackless .getruncount ()
345
- self .assertEqual (run_count , 1 , "Leakage from this test, with %d tasklets still in the scheduler" % (run_count - 1 ))
610
+ self .assertEqual (run_count , 1 ,
611
+ "Leakage from this test, with %d tasklets still in the scheduler" % (run_count - 1 ))
612
+ watchdog_list = get_watchdog_list (- 1 )
613
+ if watchdog_list is not None :
614
+ self .assertListEqual (watchdog_list , [None ], "Watchdog list is not empty: " + repr (watchdog_list ))
346
615
if withThreads :
616
+ for (watchdog_list , tid ) in [(get_watchdog_list (tid ), tid )
617
+ for tid in stackless .threads if tid != stackless .current .thread_id ]:
618
+ if watchdog_list is None :
619
+ continue
620
+ self .assertListEqual (watchdog_list , [None ],
621
+ "Thread %d: watchdog list is not empty: %r" % (tid , watchdog_list ))
347
622
preexisting_threads = self .__preexisting_threads
348
623
self .__preexisting_threads = None # avoid pickling problems, see _addSkip
349
624
expected_thread_count = len (preexisting_threads )
@@ -355,7 +630,9 @@ def tearDown(self):
355
630
while activeThreads :
356
631
activeThreads .pop ().join (0.5 )
357
632
active_count = threading .active_count ()
358
- self .assertEqual (active_count , expected_thread_count , "Leakage from other threads, with %d threads running (%d expected)" % (active_count , expected_thread_count ))
633
+ self .assertEqual (active_count , expected_thread_count ,
634
+ "Leakage from other threads, with %d threads running (%d expected)" %
635
+ (active_count , expected_thread_count ))
359
636
gc .collect () # emits warnings about uncollectable objects after each test
360
637
361
638
def dumps (self , obj , protocol = None , * , fix_imports = True ):
0 commit comments