1 | import unittest
|
---|
2 | from test.test_support import verbose, run_unittest
|
---|
3 | import sys
|
---|
4 | import time
|
---|
5 | import gc
|
---|
6 | import weakref
|
---|
7 |
|
---|
8 | try:
|
---|
9 | import threading
|
---|
10 | except ImportError:
|
---|
11 | threading = None
|
---|
12 |
|
---|
13 | ### Support code
|
---|
14 | ###############################################################################
|
---|
15 |
|
---|
16 | # Bug 1055820 has several tests of longstanding bugs involving weakrefs and
|
---|
17 | # cyclic gc.
|
---|
18 |
|
---|
19 | # An instance of C1055820 has a self-loop, so becomes cyclic trash when
|
---|
20 | # unreachable.
|
---|
21 | class C1055820(object):
|
---|
22 | def __init__(self, i):
|
---|
23 | self.i = i
|
---|
24 | self.loop = self
|
---|
25 |
|
---|
26 | class GC_Detector(object):
|
---|
27 | # Create an instance I. Then gc hasn't happened again so long as
|
---|
28 | # I.gc_happened is false.
|
---|
29 |
|
---|
30 | def __init__(self):
|
---|
31 | self.gc_happened = False
|
---|
32 |
|
---|
33 | def it_happened(ignored):
|
---|
34 | self.gc_happened = True
|
---|
35 |
|
---|
36 | # Create a piece of cyclic trash that triggers it_happened when
|
---|
37 | # gc collects it.
|
---|
38 | self.wr = weakref.ref(C1055820(666), it_happened)
|
---|
39 |
|
---|
40 |
|
---|
41 | ### Tests
|
---|
42 | ###############################################################################
|
---|
43 |
|
---|
44 | class GCTests(unittest.TestCase):
|
---|
45 | def test_list(self):
|
---|
46 | l = []
|
---|
47 | l.append(l)
|
---|
48 | gc.collect()
|
---|
49 | del l
|
---|
50 | self.assertEqual(gc.collect(), 1)
|
---|
51 |
|
---|
52 | def test_dict(self):
|
---|
53 | d = {}
|
---|
54 | d[1] = d
|
---|
55 | gc.collect()
|
---|
56 | del d
|
---|
57 | self.assertEqual(gc.collect(), 1)
|
---|
58 |
|
---|
59 | def test_tuple(self):
|
---|
60 | # since tuples are immutable we close the loop with a list
|
---|
61 | l = []
|
---|
62 | t = (l,)
|
---|
63 | l.append(t)
|
---|
64 | gc.collect()
|
---|
65 | del t
|
---|
66 | del l
|
---|
67 | self.assertEqual(gc.collect(), 2)
|
---|
68 |
|
---|
69 | def test_class(self):
|
---|
70 | class A:
|
---|
71 | pass
|
---|
72 | A.a = A
|
---|
73 | gc.collect()
|
---|
74 | del A
|
---|
75 | self.assertNotEqual(gc.collect(), 0)
|
---|
76 |
|
---|
77 | def test_newstyleclass(self):
|
---|
78 | class A(object):
|
---|
79 | pass
|
---|
80 | gc.collect()
|
---|
81 | del A
|
---|
82 | self.assertNotEqual(gc.collect(), 0)
|
---|
83 |
|
---|
84 | def test_instance(self):
|
---|
85 | class A:
|
---|
86 | pass
|
---|
87 | a = A()
|
---|
88 | a.a = a
|
---|
89 | gc.collect()
|
---|
90 | del a
|
---|
91 | self.assertNotEqual(gc.collect(), 0)
|
---|
92 |
|
---|
93 | def test_newinstance(self):
|
---|
94 | class A(object):
|
---|
95 | pass
|
---|
96 | a = A()
|
---|
97 | a.a = a
|
---|
98 | gc.collect()
|
---|
99 | del a
|
---|
100 | self.assertNotEqual(gc.collect(), 0)
|
---|
101 | class B(list):
|
---|
102 | pass
|
---|
103 | class C(B, A):
|
---|
104 | pass
|
---|
105 | a = C()
|
---|
106 | a.a = a
|
---|
107 | gc.collect()
|
---|
108 | del a
|
---|
109 | self.assertNotEqual(gc.collect(), 0)
|
---|
110 | del B, C
|
---|
111 | self.assertNotEqual(gc.collect(), 0)
|
---|
112 | A.a = A()
|
---|
113 | del A
|
---|
114 | self.assertNotEqual(gc.collect(), 0)
|
---|
115 | self.assertEqual(gc.collect(), 0)
|
---|
116 |
|
---|
117 | def test_method(self):
|
---|
118 | # Tricky: self.__init__ is a bound method, it references the instance.
|
---|
119 | class A:
|
---|
120 | def __init__(self):
|
---|
121 | self.init = self.__init__
|
---|
122 | a = A()
|
---|
123 | gc.collect()
|
---|
124 | del a
|
---|
125 | self.assertNotEqual(gc.collect(), 0)
|
---|
126 |
|
---|
127 | def test_finalizer(self):
|
---|
128 | # A() is uncollectable if it is part of a cycle, make sure it shows up
|
---|
129 | # in gc.garbage.
|
---|
130 | class A:
|
---|
131 | def __del__(self): pass
|
---|
132 | class B:
|
---|
133 | pass
|
---|
134 | a = A()
|
---|
135 | a.a = a
|
---|
136 | id_a = id(a)
|
---|
137 | b = B()
|
---|
138 | b.b = b
|
---|
139 | gc.collect()
|
---|
140 | del a
|
---|
141 | del b
|
---|
142 | self.assertNotEqual(gc.collect(), 0)
|
---|
143 | for obj in gc.garbage:
|
---|
144 | if id(obj) == id_a:
|
---|
145 | del obj.a
|
---|
146 | break
|
---|
147 | else:
|
---|
148 | self.fail("didn't find obj in garbage (finalizer)")
|
---|
149 | gc.garbage.remove(obj)
|
---|
150 |
|
---|
151 | def test_finalizer_newclass(self):
|
---|
152 | # A() is uncollectable if it is part of a cycle, make sure it shows up
|
---|
153 | # in gc.garbage.
|
---|
154 | class A(object):
|
---|
155 | def __del__(self): pass
|
---|
156 | class B(object):
|
---|
157 | pass
|
---|
158 | a = A()
|
---|
159 | a.a = a
|
---|
160 | id_a = id(a)
|
---|
161 | b = B()
|
---|
162 | b.b = b
|
---|
163 | gc.collect()
|
---|
164 | del a
|
---|
165 | del b
|
---|
166 | self.assertNotEqual(gc.collect(), 0)
|
---|
167 | for obj in gc.garbage:
|
---|
168 | if id(obj) == id_a:
|
---|
169 | del obj.a
|
---|
170 | break
|
---|
171 | else:
|
---|
172 | self.fail("didn't find obj in garbage (finalizer)")
|
---|
173 | gc.garbage.remove(obj)
|
---|
174 |
|
---|
175 | def test_function(self):
|
---|
176 | # Tricky: f -> d -> f, code should call d.clear() after the exec to
|
---|
177 | # break the cycle.
|
---|
178 | d = {}
|
---|
179 | exec("def f(): pass\n") in d
|
---|
180 | gc.collect()
|
---|
181 | del d
|
---|
182 | self.assertEqual(gc.collect(), 2)
|
---|
183 |
|
---|
184 | def test_frame(self):
|
---|
185 | def f():
|
---|
186 | frame = sys._getframe()
|
---|
187 | gc.collect()
|
---|
188 | f()
|
---|
189 | self.assertEqual(gc.collect(), 1)
|
---|
190 |
|
---|
191 | def test_saveall(self):
|
---|
192 | # Verify that cyclic garbage like lists show up in gc.garbage if the
|
---|
193 | # SAVEALL option is enabled.
|
---|
194 |
|
---|
195 | # First make sure we don't save away other stuff that just happens to
|
---|
196 | # be waiting for collection.
|
---|
197 | gc.collect()
|
---|
198 | # if this fails, someone else created immortal trash
|
---|
199 | self.assertEqual(gc.garbage, [])
|
---|
200 |
|
---|
201 | L = []
|
---|
202 | L.append(L)
|
---|
203 | id_L = id(L)
|
---|
204 |
|
---|
205 | debug = gc.get_debug()
|
---|
206 | gc.set_debug(debug | gc.DEBUG_SAVEALL)
|
---|
207 | del L
|
---|
208 | gc.collect()
|
---|
209 | gc.set_debug(debug)
|
---|
210 |
|
---|
211 | self.assertEqual(len(gc.garbage), 1)
|
---|
212 | obj = gc.garbage.pop()
|
---|
213 | self.assertEqual(id(obj), id_L)
|
---|
214 |
|
---|
215 | def test_del(self):
|
---|
216 | # __del__ methods can trigger collection, make this to happen
|
---|
217 | thresholds = gc.get_threshold()
|
---|
218 | gc.enable()
|
---|
219 | gc.set_threshold(1)
|
---|
220 |
|
---|
221 | class A:
|
---|
222 | def __del__(self):
|
---|
223 | dir(self)
|
---|
224 | a = A()
|
---|
225 | del a
|
---|
226 |
|
---|
227 | gc.disable()
|
---|
228 | gc.set_threshold(*thresholds)
|
---|
229 |
|
---|
230 | def test_del_newclass(self):
|
---|
231 | # __del__ methods can trigger collection, make this to happen
|
---|
232 | thresholds = gc.get_threshold()
|
---|
233 | gc.enable()
|
---|
234 | gc.set_threshold(1)
|
---|
235 |
|
---|
236 | class A(object):
|
---|
237 | def __del__(self):
|
---|
238 | dir(self)
|
---|
239 | a = A()
|
---|
240 | del a
|
---|
241 |
|
---|
242 | gc.disable()
|
---|
243 | gc.set_threshold(*thresholds)
|
---|
244 |
|
---|
245 | # The following two tests are fragile:
|
---|
246 | # They precisely count the number of allocations,
|
---|
247 | # which is highly implementation-dependent.
|
---|
248 | # For example:
|
---|
249 | # - disposed tuples are not freed, but reused
|
---|
250 | # - the call to assertEqual somehow avoids building its args tuple
|
---|
251 | def test_get_count(self):
|
---|
252 | # Avoid future allocation of method object
|
---|
253 | assertEqual = self._baseAssertEqual
|
---|
254 | gc.collect()
|
---|
255 | assertEqual(gc.get_count(), (0, 0, 0))
|
---|
256 | a = dict()
|
---|
257 | # since gc.collect(), we created two objects:
|
---|
258 | # the dict, and the tuple returned by get_count()
|
---|
259 | assertEqual(gc.get_count(), (2, 0, 0))
|
---|
260 |
|
---|
261 | def test_collect_generations(self):
|
---|
262 | # Avoid future allocation of method object
|
---|
263 | assertEqual = self.assertEqual
|
---|
264 | gc.collect()
|
---|
265 | a = dict()
|
---|
266 | gc.collect(0)
|
---|
267 | assertEqual(gc.get_count(), (0, 1, 0))
|
---|
268 | gc.collect(1)
|
---|
269 | assertEqual(gc.get_count(), (0, 0, 1))
|
---|
270 | gc.collect(2)
|
---|
271 | assertEqual(gc.get_count(), (0, 0, 0))
|
---|
272 |
|
---|
273 | def test_trashcan(self):
|
---|
274 | class Ouch:
|
---|
275 | n = 0
|
---|
276 | def __del__(self):
|
---|
277 | Ouch.n = Ouch.n + 1
|
---|
278 | if Ouch.n % 17 == 0:
|
---|
279 | gc.collect()
|
---|
280 |
|
---|
281 | # "trashcan" is a hack to prevent stack overflow when deallocating
|
---|
282 | # very deeply nested tuples etc. It works in part by abusing the
|
---|
283 | # type pointer and refcount fields, and that can yield horrible
|
---|
284 | # problems when gc tries to traverse the structures.
|
---|
285 | # If this test fails (as it does in 2.0, 2.1 and 2.2), it will
|
---|
286 | # most likely die via segfault.
|
---|
287 |
|
---|
288 | # Note: In 2.3 the possibility for compiling without cyclic gc was
|
---|
289 | # removed, and that in turn allows the trashcan mechanism to work
|
---|
290 | # via much simpler means (e.g., it never abuses the type pointer or
|
---|
291 | # refcount fields anymore). Since it's much less likely to cause a
|
---|
292 | # problem now, the various constants in this expensive (we force a lot
|
---|
293 | # of full collections) test are cut back from the 2.2 version.
|
---|
294 | gc.enable()
|
---|
295 | N = 150
|
---|
296 | for count in range(2):
|
---|
297 | t = []
|
---|
298 | for i in range(N):
|
---|
299 | t = [t, Ouch()]
|
---|
300 | u = []
|
---|
301 | for i in range(N):
|
---|
302 | u = [u, Ouch()]
|
---|
303 | v = {}
|
---|
304 | for i in range(N):
|
---|
305 | v = {1: v, 2: Ouch()}
|
---|
306 | gc.disable()
|
---|
307 |
|
---|
308 | @unittest.skipUnless(threading, "test meaningless on builds without threads")
|
---|
309 | def test_trashcan_threads(self):
|
---|
310 | # Issue #13992: trashcan mechanism should be thread-safe
|
---|
311 | NESTING = 60
|
---|
312 | N_THREADS = 2
|
---|
313 |
|
---|
314 | def sleeper_gen():
|
---|
315 | """A generator that releases the GIL when closed or dealloc'ed."""
|
---|
316 | try:
|
---|
317 | yield
|
---|
318 | finally:
|
---|
319 | time.sleep(0.000001)
|
---|
320 |
|
---|
321 | class C(list):
|
---|
322 | # Appending to a list is atomic, which avoids the use of a lock.
|
---|
323 | inits = []
|
---|
324 | dels = []
|
---|
325 | def __init__(self, alist):
|
---|
326 | self[:] = alist
|
---|
327 | C.inits.append(None)
|
---|
328 | def __del__(self):
|
---|
329 | # This __del__ is called by subtype_dealloc().
|
---|
330 | C.dels.append(None)
|
---|
331 | # `g` will release the GIL when garbage-collected. This
|
---|
332 | # helps assert subtype_dealloc's behaviour when threads
|
---|
333 | # switch in the middle of it.
|
---|
334 | g = sleeper_gen()
|
---|
335 | next(g)
|
---|
336 | # Now that __del__ is finished, subtype_dealloc will proceed
|
---|
337 | # to call list_dealloc, which also uses the trashcan mechanism.
|
---|
338 |
|
---|
339 | def make_nested():
|
---|
340 | """Create a sufficiently nested container object so that the
|
---|
341 | trashcan mechanism is invoked when deallocating it."""
|
---|
342 | x = C([])
|
---|
343 | for i in range(NESTING):
|
---|
344 | x = [C([x])]
|
---|
345 | del x
|
---|
346 |
|
---|
347 | def run_thread():
|
---|
348 | """Exercise make_nested() in a loop."""
|
---|
349 | while not exit:
|
---|
350 | make_nested()
|
---|
351 |
|
---|
352 | old_checkinterval = sys.getcheckinterval()
|
---|
353 | sys.setcheckinterval(3)
|
---|
354 | try:
|
---|
355 | exit = False
|
---|
356 | threads = []
|
---|
357 | for i in range(N_THREADS):
|
---|
358 | t = threading.Thread(target=run_thread)
|
---|
359 | threads.append(t)
|
---|
360 | for t in threads:
|
---|
361 | t.start()
|
---|
362 | time.sleep(1.0)
|
---|
363 | exit = True
|
---|
364 | for t in threads:
|
---|
365 | t.join()
|
---|
366 | finally:
|
---|
367 | sys.setcheckinterval(old_checkinterval)
|
---|
368 | gc.collect()
|
---|
369 | self.assertEqual(len(C.inits), len(C.dels))
|
---|
370 |
|
---|
371 | def test_boom(self):
|
---|
372 | class Boom:
|
---|
373 | def __getattr__(self, someattribute):
|
---|
374 | del self.attr
|
---|
375 | raise AttributeError
|
---|
376 |
|
---|
377 | a = Boom()
|
---|
378 | b = Boom()
|
---|
379 | a.attr = b
|
---|
380 | b.attr = a
|
---|
381 |
|
---|
382 | gc.collect()
|
---|
383 | garbagelen = len(gc.garbage)
|
---|
384 | del a, b
|
---|
385 | # a<->b are in a trash cycle now. Collection will invoke
|
---|
386 | # Boom.__getattr__ (to see whether a and b have __del__ methods), and
|
---|
387 | # __getattr__ deletes the internal "attr" attributes as a side effect.
|
---|
388 | # That causes the trash cycle to get reclaimed via refcounts falling to
|
---|
389 | # 0, thus mutating the trash graph as a side effect of merely asking
|
---|
390 | # whether __del__ exists. This used to (before 2.3b1) crash Python.
|
---|
391 | # Now __getattr__ isn't called.
|
---|
392 | self.assertEqual(gc.collect(), 4)
|
---|
393 | self.assertEqual(len(gc.garbage), garbagelen)
|
---|
394 |
|
---|
395 | def test_boom2(self):
|
---|
396 | class Boom2:
|
---|
397 | def __init__(self):
|
---|
398 | self.x = 0
|
---|
399 |
|
---|
400 | def __getattr__(self, someattribute):
|
---|
401 | self.x += 1
|
---|
402 | if self.x > 1:
|
---|
403 | del self.attr
|
---|
404 | raise AttributeError
|
---|
405 |
|
---|
406 | a = Boom2()
|
---|
407 | b = Boom2()
|
---|
408 | a.attr = b
|
---|
409 | b.attr = a
|
---|
410 |
|
---|
411 | gc.collect()
|
---|
412 | garbagelen = len(gc.garbage)
|
---|
413 | del a, b
|
---|
414 | # Much like test_boom(), except that __getattr__ doesn't break the
|
---|
415 | # cycle until the second time gc checks for __del__. As of 2.3b1,
|
---|
416 | # there isn't a second time, so this simply cleans up the trash cycle.
|
---|
417 | # We expect a, b, a.__dict__ and b.__dict__ (4 objects) to get
|
---|
418 | # reclaimed this way.
|
---|
419 | self.assertEqual(gc.collect(), 4)
|
---|
420 | self.assertEqual(len(gc.garbage), garbagelen)
|
---|
421 |
|
---|
422 | def test_boom_new(self):
|
---|
423 | # boom__new and boom2_new are exactly like boom and boom2, except use
|
---|
424 | # new-style classes.
|
---|
425 |
|
---|
426 | class Boom_New(object):
|
---|
427 | def __getattr__(self, someattribute):
|
---|
428 | del self.attr
|
---|
429 | raise AttributeError
|
---|
430 |
|
---|
431 | a = Boom_New()
|
---|
432 | b = Boom_New()
|
---|
433 | a.attr = b
|
---|
434 | b.attr = a
|
---|
435 |
|
---|
436 | gc.collect()
|
---|
437 | garbagelen = len(gc.garbage)
|
---|
438 | del a, b
|
---|
439 | self.assertEqual(gc.collect(), 4)
|
---|
440 | self.assertEqual(len(gc.garbage), garbagelen)
|
---|
441 |
|
---|
442 | def test_boom2_new(self):
|
---|
443 | class Boom2_New(object):
|
---|
444 | def __init__(self):
|
---|
445 | self.x = 0
|
---|
446 |
|
---|
447 | def __getattr__(self, someattribute):
|
---|
448 | self.x += 1
|
---|
449 | if self.x > 1:
|
---|
450 | del self.attr
|
---|
451 | raise AttributeError
|
---|
452 |
|
---|
453 | a = Boom2_New()
|
---|
454 | b = Boom2_New()
|
---|
455 | a.attr = b
|
---|
456 | b.attr = a
|
---|
457 |
|
---|
458 | gc.collect()
|
---|
459 | garbagelen = len(gc.garbage)
|
---|
460 | del a, b
|
---|
461 | self.assertEqual(gc.collect(), 4)
|
---|
462 | self.assertEqual(len(gc.garbage), garbagelen)
|
---|
463 |
|
---|
464 | def test_get_referents(self):
|
---|
465 | alist = [1, 3, 5]
|
---|
466 | got = gc.get_referents(alist)
|
---|
467 | got.sort()
|
---|
468 | self.assertEqual(got, alist)
|
---|
469 |
|
---|
470 | atuple = tuple(alist)
|
---|
471 | got = gc.get_referents(atuple)
|
---|
472 | got.sort()
|
---|
473 | self.assertEqual(got, alist)
|
---|
474 |
|
---|
475 | adict = {1: 3, 5: 7}
|
---|
476 | expected = [1, 3, 5, 7]
|
---|
477 | got = gc.get_referents(adict)
|
---|
478 | got.sort()
|
---|
479 | self.assertEqual(got, expected)
|
---|
480 |
|
---|
481 | got = gc.get_referents([1, 2], {3: 4}, (0, 0, 0))
|
---|
482 | got.sort()
|
---|
483 | self.assertEqual(got, [0, 0] + range(5))
|
---|
484 |
|
---|
485 | self.assertEqual(gc.get_referents(1, 'a', 4j), [])
|
---|
486 |
|
---|
487 | def test_is_tracked(self):
|
---|
488 | # Atomic built-in types are not tracked, user-defined objects and
|
---|
489 | # mutable containers are.
|
---|
490 | # NOTE: types with special optimizations (e.g. tuple) have tests
|
---|
491 | # in their own test files instead.
|
---|
492 | self.assertFalse(gc.is_tracked(None))
|
---|
493 | self.assertFalse(gc.is_tracked(1))
|
---|
494 | self.assertFalse(gc.is_tracked(1.0))
|
---|
495 | self.assertFalse(gc.is_tracked(1.0 + 5.0j))
|
---|
496 | self.assertFalse(gc.is_tracked(True))
|
---|
497 | self.assertFalse(gc.is_tracked(False))
|
---|
498 | self.assertFalse(gc.is_tracked("a"))
|
---|
499 | self.assertFalse(gc.is_tracked(u"a"))
|
---|
500 | self.assertFalse(gc.is_tracked(bytearray("a")))
|
---|
501 | self.assertFalse(gc.is_tracked(type))
|
---|
502 | self.assertFalse(gc.is_tracked(int))
|
---|
503 | self.assertFalse(gc.is_tracked(object))
|
---|
504 | self.assertFalse(gc.is_tracked(object()))
|
---|
505 |
|
---|
506 | class OldStyle:
|
---|
507 | pass
|
---|
508 | class NewStyle(object):
|
---|
509 | pass
|
---|
510 | self.assertTrue(gc.is_tracked(gc))
|
---|
511 | self.assertTrue(gc.is_tracked(OldStyle))
|
---|
512 | self.assertTrue(gc.is_tracked(OldStyle()))
|
---|
513 | self.assertTrue(gc.is_tracked(NewStyle))
|
---|
514 | self.assertTrue(gc.is_tracked(NewStyle()))
|
---|
515 | self.assertTrue(gc.is_tracked([]))
|
---|
516 | self.assertTrue(gc.is_tracked(set()))
|
---|
517 |
|
---|
518 | def test_bug1055820b(self):
|
---|
519 | # Corresponds to temp2b.py in the bug report.
|
---|
520 |
|
---|
521 | ouch = []
|
---|
522 | def callback(ignored):
|
---|
523 | ouch[:] = [wr() for wr in WRs]
|
---|
524 |
|
---|
525 | Cs = [C1055820(i) for i in range(2)]
|
---|
526 | WRs = [weakref.ref(c, callback) for c in Cs]
|
---|
527 | c = None
|
---|
528 |
|
---|
529 | gc.collect()
|
---|
530 | self.assertEqual(len(ouch), 0)
|
---|
531 | # Make the two instances trash, and collect again. The bug was that
|
---|
532 | # the callback materialized a strong reference to an instance, but gc
|
---|
533 | # cleared the instance's dict anyway.
|
---|
534 | Cs = None
|
---|
535 | gc.collect()
|
---|
536 | self.assertEqual(len(ouch), 2) # else the callbacks didn't run
|
---|
537 | for x in ouch:
|
---|
538 | # If the callback resurrected one of these guys, the instance
|
---|
539 | # would be damaged, with an empty __dict__.
|
---|
540 | self.assertEqual(x, None)
|
---|
541 |
|
---|
542 | class GCTogglingTests(unittest.TestCase):
|
---|
543 | def setUp(self):
|
---|
544 | gc.enable()
|
---|
545 |
|
---|
546 | def tearDown(self):
|
---|
547 | gc.disable()
|
---|
548 |
|
---|
549 | def test_bug1055820c(self):
|
---|
550 | # Corresponds to temp2c.py in the bug report. This is pretty
|
---|
551 | # elaborate.
|
---|
552 |
|
---|
553 | c0 = C1055820(0)
|
---|
554 | # Move c0 into generation 2.
|
---|
555 | gc.collect()
|
---|
556 |
|
---|
557 | c1 = C1055820(1)
|
---|
558 | c1.keep_c0_alive = c0
|
---|
559 | del c0.loop # now only c1 keeps c0 alive
|
---|
560 |
|
---|
561 | c2 = C1055820(2)
|
---|
562 | c2wr = weakref.ref(c2) # no callback!
|
---|
563 |
|
---|
564 | ouch = []
|
---|
565 | def callback(ignored):
|
---|
566 | ouch[:] = [c2wr()]
|
---|
567 |
|
---|
568 | # The callback gets associated with a wr on an object in generation 2.
|
---|
569 | c0wr = weakref.ref(c0, callback)
|
---|
570 |
|
---|
571 | c0 = c1 = c2 = None
|
---|
572 |
|
---|
573 | # What we've set up: c0, c1, and c2 are all trash now. c0 is in
|
---|
574 | # generation 2. The only thing keeping it alive is that c1 points to
|
---|
575 | # it. c1 and c2 are in generation 0, and are in self-loops. There's a
|
---|
576 | # global weakref to c2 (c2wr), but that weakref has no callback.
|
---|
577 | # There's also a global weakref to c0 (c0wr), and that does have a
|
---|
578 | # callback, and that callback references c2 via c2wr().
|
---|
579 | #
|
---|
580 | # c0 has a wr with callback, which references c2wr
|
---|
581 | # ^
|
---|
582 | # |
|
---|
583 | # | Generation 2 above dots
|
---|
584 | #. . . . . . . .|. . . . . . . . . . . . . . . . . . . . . . . .
|
---|
585 | # | Generation 0 below dots
|
---|
586 | # |
|
---|
587 | # |
|
---|
588 | # ^->c1 ^->c2 has a wr but no callback
|
---|
589 | # | | | |
|
---|
590 | # <--v <--v
|
---|
591 | #
|
---|
592 | # So this is the nightmare: when generation 0 gets collected, we see
|
---|
593 | # that c2 has a callback-free weakref, and c1 doesn't even have a
|
---|
594 | # weakref. Collecting generation 0 doesn't see c0 at all, and c0 is
|
---|
595 | # the only object that has a weakref with a callback. gc clears c1
|
---|
596 | # and c2. Clearing c1 has the side effect of dropping the refcount on
|
---|
597 | # c0 to 0, so c0 goes away (despite that it's in an older generation)
|
---|
598 | # and c0's wr callback triggers. That in turn materializes a reference
|
---|
599 | # to c2 via c2wr(), but c2 gets cleared anyway by gc.
|
---|
600 |
|
---|
601 | # We want to let gc happen "naturally", to preserve the distinction
|
---|
602 | # between generations.
|
---|
603 | junk = []
|
---|
604 | i = 0
|
---|
605 | detector = GC_Detector()
|
---|
606 | while not detector.gc_happened:
|
---|
607 | i += 1
|
---|
608 | if i > 10000:
|
---|
609 | self.fail("gc didn't happen after 10000 iterations")
|
---|
610 | self.assertEqual(len(ouch), 0)
|
---|
611 | junk.append([]) # this will eventually trigger gc
|
---|
612 |
|
---|
613 | self.assertEqual(len(ouch), 1) # else the callback wasn't invoked
|
---|
614 | for x in ouch:
|
---|
615 | # If the callback resurrected c2, the instance would be damaged,
|
---|
616 | # with an empty __dict__.
|
---|
617 | self.assertEqual(x, None)
|
---|
618 |
|
---|
619 | def test_bug1055820d(self):
|
---|
620 | # Corresponds to temp2d.py in the bug report. This is very much like
|
---|
621 | # test_bug1055820c, but uses a __del__ method instead of a weakref
|
---|
622 | # callback to sneak in a resurrection of cyclic trash.
|
---|
623 |
|
---|
624 | ouch = []
|
---|
625 | class D(C1055820):
|
---|
626 | def __del__(self):
|
---|
627 | ouch[:] = [c2wr()]
|
---|
628 |
|
---|
629 | d0 = D(0)
|
---|
630 | # Move all the above into generation 2.
|
---|
631 | gc.collect()
|
---|
632 |
|
---|
633 | c1 = C1055820(1)
|
---|
634 | c1.keep_d0_alive = d0
|
---|
635 | del d0.loop # now only c1 keeps d0 alive
|
---|
636 |
|
---|
637 | c2 = C1055820(2)
|
---|
638 | c2wr = weakref.ref(c2) # no callback!
|
---|
639 |
|
---|
640 | d0 = c1 = c2 = None
|
---|
641 |
|
---|
642 | # What we've set up: d0, c1, and c2 are all trash now. d0 is in
|
---|
643 | # generation 2. The only thing keeping it alive is that c1 points to
|
---|
644 | # it. c1 and c2 are in generation 0, and are in self-loops. There's
|
---|
645 | # a global weakref to c2 (c2wr), but that weakref has no callback.
|
---|
646 | # There are no other weakrefs.
|
---|
647 | #
|
---|
648 | # d0 has a __del__ method that references c2wr
|
---|
649 | # ^
|
---|
650 | # |
|
---|
651 | # | Generation 2 above dots
|
---|
652 | #. . . . . . . .|. . . . . . . . . . . . . . . . . . . . . . . .
|
---|
653 | # | Generation 0 below dots
|
---|
654 | # |
|
---|
655 | # |
|
---|
656 | # ^->c1 ^->c2 has a wr but no callback
|
---|
657 | # | | | |
|
---|
658 | # <--v <--v
|
---|
659 | #
|
---|
660 | # So this is the nightmare: when generation 0 gets collected, we see
|
---|
661 | # that c2 has a callback-free weakref, and c1 doesn't even have a
|
---|
662 | # weakref. Collecting generation 0 doesn't see d0 at all. gc clears
|
---|
663 | # c1 and c2. Clearing c1 has the side effect of dropping the refcount
|
---|
664 | # on d0 to 0, so d0 goes away (despite that it's in an older
|
---|
665 | # generation) and d0's __del__ triggers. That in turn materializes
|
---|
666 | # a reference to c2 via c2wr(), but c2 gets cleared anyway by gc.
|
---|
667 |
|
---|
668 | # We want to let gc happen "naturally", to preserve the distinction
|
---|
669 | # between generations.
|
---|
670 | detector = GC_Detector()
|
---|
671 | junk = []
|
---|
672 | i = 0
|
---|
673 | while not detector.gc_happened:
|
---|
674 | i += 1
|
---|
675 | if i > 10000:
|
---|
676 | self.fail("gc didn't happen after 10000 iterations")
|
---|
677 | self.assertEqual(len(ouch), 0)
|
---|
678 | junk.append([]) # this will eventually trigger gc
|
---|
679 |
|
---|
680 | self.assertEqual(len(ouch), 1) # else __del__ wasn't invoked
|
---|
681 | for x in ouch:
|
---|
682 | # If __del__ resurrected c2, the instance would be damaged, with an
|
---|
683 | # empty __dict__.
|
---|
684 | self.assertEqual(x, None)
|
---|
685 |
|
---|
686 | def test_main():
|
---|
687 | enabled = gc.isenabled()
|
---|
688 | gc.disable()
|
---|
689 | assert not gc.isenabled()
|
---|
690 | debug = gc.get_debug()
|
---|
691 | gc.set_debug(debug & ~gc.DEBUG_LEAK) # this test is supposed to leak
|
---|
692 |
|
---|
693 | try:
|
---|
694 | gc.collect() # Delete 2nd generation garbage
|
---|
695 | run_unittest(GCTests, GCTogglingTests)
|
---|
696 | finally:
|
---|
697 | gc.set_debug(debug)
|
---|
698 | # test gc.enable() even if GC is disabled by default
|
---|
699 | if verbose:
|
---|
700 | print "restoring automatic collection"
|
---|
701 | # make sure to always test gc.enable()
|
---|
702 | gc.enable()
|
---|
703 | assert gc.isenabled()
|
---|
704 | if not enabled:
|
---|
705 | gc.disable()
|
---|
706 |
|
---|
707 | if __name__ == "__main__":
|
---|
708 | test_main()
|
---|