Skip to content

Commit 298c15f

Browse files
author
Anselm Kruis
committed
merge 3.4-slp (Stackless python#117, StacklessTestCase)
2 parents e13d0e6 + e0f01d1 commit 298c15f

File tree

1 file changed

+287
-10
lines changed

1 file changed

+287
-10
lines changed

Stackless/unittests/support.py

Lines changed: 287 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import io
3333
import contextlib
3434
import gc
35+
import os
36+
import functools
3537
from test.support import run_unittest
3638

3739
# emit warnings about uncollectable objects
@@ -65,10 +67,75 @@ def require_one_thread(testcase):
6567
return testcase
6668

6769

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+
"""
69130
# The second argument of get_thread_info() is intentionally undocumented.
70131
# 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)
72139
if isinstance(watchdog_list, list):
73140
return watchdog_list
74141
# The watchdog list has not been created. Force its creation.
@@ -87,11 +154,72 @@ def get_current_watchdog_list():
87154
t.insert()
88155
if scheduled:
89156
assert stackless.current.next == scheduled[0]
90-
watchdog_list = stackless.get_thread_info(-1, 1 << 31)[3]
157+
watchdog_list = get_watchdog_list(-1)
91158
assert isinstance(watchdog_list, list)
92159
return watchdog_list
93160

94161

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+
95223
class StacklessTestCaseMixin(object):
96224
def skipUnlessSoftswitching(self):
97225
if not stackless.enable_softswitch(None):
@@ -190,6 +318,109 @@ def _checkSignature(self, func, nb_mandatory, accept_arbitrary, additionalArg, *
190318
# only required args as kw-args
191319
yield func(**dict((k, kwargs[k]) for k in names[:nb_mandatory]))
192320

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+
193424

194425
# call the class method prepare_test_methods(cls) after creating the
195426
# 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):
251482
for i in range(0, pickle.HIGHEST_PROTOCOL + 1):
252483
for p_letter in ("C", "P"):
253484
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()")
255487
self._pickle_protocol = proto
256488
self._pickle_module = pickle_module
257489
self._unpickle_module = unpickle_module
@@ -284,7 +516,7 @@ def prepare_test_methods(cls):
284516
if inspect.ismethod(m):
285517
m = m.__func__
286518
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
288520
pass
289521

290522
__setup_called = False
@@ -317,20 +549,45 @@ def setUpStacklessTestCase(self):
317549
self.addCleanup(stackless.enable_softswitch, stackless.enable_softswitch(self.__enable_softswitch))
318550

319551
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))
320555
if withThreads and self.__preexisting_threads is None:
321556
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))
322563
return len(self.__preexisting_threads)
323564
return 1
324565

325566
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))
327570
expected_thread_count = self.setUpStacklessTestCase()
328571
if withThreads:
329572
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))
331576

332577
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
334591
# clean them up for the tests. Any tests that expect to exit with no leaked tasklets should do explicit
335592
# assertions to check.
336593
self.assertTrue(self.__setup_called, "Broken test case: it didn't call super(..., self).setUp()")
@@ -341,9 +598,27 @@ def tearDown(self):
341598
next_ = current.next
342599
current.kill()
343600
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()
344609
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))
346615
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))
347622
preexisting_threads = self.__preexisting_threads
348623
self.__preexisting_threads = None # avoid pickling problems, see _addSkip
349624
expected_thread_count = len(preexisting_threads)
@@ -355,7 +630,9 @@ def tearDown(self):
355630
while activeThreads:
356631
activeThreads.pop().join(0.5)
357632
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))
359636
gc.collect() # emits warnings about uncollectable objects after each test
360637

361638
def dumps(self, obj, protocol=None, *, fix_imports=True):

0 commit comments

Comments
 (0)