Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ def f():
self.assertEqual(gc.collect(), 1)

def test_saveall(self):
self.addCleanup(gc.collect)
# Verify that cyclic garbage like lists show up in gc.garbage if the
# SAVEALL option is enabled.

Expand All @@ -352,6 +353,31 @@ def test_saveall(self):
self.assertEqual(len(gc.garbage), 1)
obj = gc.garbage.pop()
self.assertEqual(id(obj), id_L)
obj.clear()

# A no-op generator finalizer must still set the finalized marker.
gen = (item for item in ())
next(gen, None)
self.assertTrue(gc.is_tracked(gen))
self.assertIsNone(gen.gi_frame)
self.assertFalse(gc.is_finalized(gen))
L = [gen]
L.append(L)
id_L = id(L)
del gen

try:
gc.set_debug(gc.DEBUG_SAVEALL)
del L
gc.collect()
saved = next(obj for obj in gc.garbage if id(obj) == id_L)
self.assertTrue(gc.is_finalized(saved[0]))
finally:
gc.set_debug(debug)
for obj in gc.garbage:
if id(obj) == id_L:
obj.clear()
gc.garbage.clear()

def test_del(self):
# __del__ methods can trigger collection, make this to happen
Expand Down Expand Up @@ -658,6 +684,83 @@ def __del__(self):
lazarus = storage.pop()
self.assertTrue(gc.is_finalized(lazarus))

def test_weakref_callback_installs_finalizer(self):
"""A callback can make a finalizer eligible before the object is cleared."""
self.addCleanup(gc.collect)
events = []

class C:
"""A self-cycle whose weakref callback installs its finalizer."""
__slots__ = ("cycle", "__weakref__")

def finalize(obj):
"""Record whether finalization sees the intact cycle."""
events.append(("finalizer", getattr(obj, "cycle", None) is obj))

def callback(reference):
"""Install the finalizer after observing the cleared weakref."""
events.append(("callback", reference() is None))
C.__del__ = finalize

obj = C()
obj.cycle = obj
reference = weakref.ref(obj, callback)
gc.collect()
del obj

self.assertEqual(gc.collect(), 1)
self.assertEqual(events, [("callback", True), ("finalizer", True)])
self.assertIsNone(reference())

@requires_gil_enabled("tests the generation-list collector")
def test_debug_output_resurrects_without_callbacks(self):
"""Debug output can resurrect garbage without pending callbacks."""
was_enabled = gc.isenabled()
gc.disable()
self.addCleanup(gc.enable if was_enabled else gc.disable)
self.addCleanup(gc.collect)
resurrected = []

class C:
"""An object with a cycle and no finalizer."""
__slots__ = ("cycle", "__weakref__")

class Stderr:
"""Retain the target when collection reports it as collectable."""

def write(self, text):
"""Retain the target on collectable debug output.

text: Debug text written by the collector.
Returns: Number of characters accepted.
"""
if text.startswith("gc: collectable") and not resurrected:
target = reference()
if target is not None:
resurrected.append(target)
return len(text)

writer = Stderr()
gc.collect()
obj = C()
obj.cycle = obj
reference = weakref.ref(obj)
del obj

debug = gc.get_debug()
try:
with unittest.mock.patch.object(sys, "stderr", writer):
gc.set_debug(gc.DEBUG_COLLECTABLE)
self.assertEqual(gc.collect(), 0)
self.assertEqual(len(resurrected), 1)
self.assertIs(reference(), resurrected[0])
self.assertIs(resurrected[0].cycle, resurrected[0])
finally:
gc.set_debug(debug)
for obj in resurrected:
obj.cycle = None
resurrected.clear()

def test_bug1055820b(self):
# Corresponds to temp2b.py in the bug report.

Expand Down Expand Up @@ -1093,6 +1196,36 @@ def getstats():

gc.enable()

@requires_gil_enabled("tests the generation-list collector")
def test_callback_free_weakref_clear_without_pending_callbacks(self):
"""Clear callback-free weakrefs before tp_clear can expose garbage."""
was_enabled = gc.isenabled()
gc.disable()
self.addCleanup(gc.enable if was_enabled else gc.disable)
self.addCleanup(gc.collect)
observed = []
self.addCleanup(observed.clear)

class Observer:
"""Inspect a young target when its clearing releases this object."""

def __del__(self):
"""Record whether the weakref still exposes the target."""
observed.append(reference())

observer = Observer()
gc.collect()
# The older observer does not open the callback path for generation 0.
# Clearing the target releases it, so the weakref must already be dead.
target = C1055820(0)
target.observer = observer
reference = weakref.ref(target)
del observer, target

self.assertEqual(gc.collect(0), 1)
self.assertEqual(observed, [None])
self.assertIsNone(reference())

@unittest.skipIf(ContainerNoGC is None,
'requires ContainerNoGC extension type')
def test_trash_weakref_clear(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Count collectable objects while clearing weak references, avoiding a separate pass during garbage collection.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
With the GIL enabled, garbage collection skips the finalization scan when no
finalizers or weakref callbacks are pending and debug output for collectable
objects is disabled.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Garbage collection in builds with the GIL now reuses the initial garbage count and skips late weakref cleanup when no weakrefs need clearing and the unreachable set remains valid.
Loading
Loading