Repository: python/cpython
Pull request #133533: gh-133532: Run GC fast cycles test in subprocess.
Author: login: nascheme
Labels: name: tests
description: Tests in the Lib/test dir
name: skip news
Created: 2025-05-06T19:26:22Z | Merged: 2025-05-08T04:38:57Z

This makes the test more reliable since there are not extra objects on the heap leftover from other tests.

<!-- gh-issue-number: gh-133532 -->
* Issue: gh-133532
<!-- /gh-issue-number -->



Diff:
diff --git a/Lib/test/_test_gc_fast_cycles.py b/Lib/test/_test_gc_fast_cycles.py
new file mode 100644
index 00000000000000..4e2c7d72a02713
--- /dev/null
+++ b/Lib/test/_test_gc_fast_cycles.py
@@ -0,0 +1,48 @@
+# Run by test_gc.
+from test import support
+import _testinternalcapi
+import gc
+import unittest
+
+class IncrementalGCTests(unittest.TestCase):
+
+    # Use small increments to emulate longer running process in a shorter time
+    @support.gc_threshold(200, 10)
+    def test_incremental_gc_handles_fast_cycle_creation(self):
+
+        class LinkedList:
+
+            #Use slots to reduce number of implicit objects
+            __slots__ = "next", "prev", "surprise"
+
+            def __init__(self, next=None, prev=None):
+                self.next = next
+                if next is not None:
+                    next.prev = self
+                self.prev = prev
+                if prev is not None:
+                    prev.next = self
+
+        def make_ll(depth):
+            head = LinkedList()
+            for i in range(depth):
+                head = LinkedList(head, head.prev)
+            return head
+
+        head = make_ll(1000)
+
+        assert(gc.isenabled())
+        olds = []
+        initial_heap_size = _testinternalcapi.get_tracked_heap_size()
+        for i in range(20_000):
+            newhead = make_ll(20)
+            newhead.surprise = head
+            olds.append(newhead)
+            if len(olds) == 20:
+                new_objects = _testinternalcapi.get_tracked_heap_size() - initial_heap_size
+                self.assertLess(new_objects, 27_000, f"Heap growing. Reached limit after {i} iterations")
+                del olds[:]
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py
index 8fae12c478cb3a..95c98c6ac63f71 100644
--- a/Lib/test/test_gc.py
+++ b/Lib/test/test_gc.py
@@ -7,7 +7,7 @@
                           Py_GIL_DISABLED)
 from test.support.import_helper import import_module
 from test.support.os_helper import temp_dir, TESTFN, unlink
-from test.support.script_helper import assert_python_ok, make_script
+from test.support.script_helper import assert_python_ok, make_script, run_test_script
 from test.support import threading_helper, gc_threshold
 
 import gc
@@ -1127,64 +1127,14 @@ def test_something(self):
 
 
 class IncrementalGCTests(unittest.TestCase):
-
-    def setUp(self):
-        # Reenable GC as it is disabled module-wide
-        gc.enable()
-
-    def tearDown(self):
-        gc.disable()
-
     @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
     @requires_gil_enabled("Free threading does not support incremental GC")
-    # Use small increments to emulate longer running process in a shorter time
-    @gc_threshold(200, 10)
     def test_incremental_gc_handles_fast_cycle_creation(self):
-
-        class LinkedList:
-
-            #Use slots to reduce number of implicit objects
-            __slots__ = "next", "prev", "surprise"
-
-            def __init__(self, next=None, prev=None):
-                self.next = next
-                if next is not None:
-                    next.prev = self
-                self.prev = prev
-                if prev is not None:
-                    prev.next = self
-
-        def make_ll(depth):
-            head = LinkedList()
-            for i in range(depth):
-                head = LinkedList(head, head.prev)
-            return head
-
-        head = make_ll(1000)
-        count = 1000
-
-        # There will be some objects we aren't counting,
-        # e.g. the gc stats dicts. This test checks
-        # that the counts don't grow, so we try to
-        # correct for the uncounted objects
-        # This is just an estimate.
-        CORRECTION = 20
-
-        enabled = gc.isenabled()
-        gc.enable()
-        olds = []
-        initial_heap_size = _testinternalcapi.get_tracked_heap_size()
-        for i in range(20_000):
-            newhead = make_ll(20)
-            count += 20
-            newhead.surprise = head
-            olds.append(newhead)
-            if len(olds) == 20:
-                new_objects = _testinternalcapi.get_tracked_heap_size() - initial_heap_size
-                self.assertLess(new_objects, 27_000, f"Heap growing. Reached limit after {i} iterations")
-                del olds[:]
-        if not enabled:
-            gc.disable()
+        # Run this test in a fresh process.  The number of alive objects (which can
+        # be from unit tests run before this one) can influence how quickly cyclic
+        # garbage is found.
+        script = support.findfile("_test_gc_fast_cycles.py")
+        run_test_script(script)
 
 
 class GCCallbackTests(unittest.TestCase):


Reviews:
user: login: chris-eibl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T20:08:52Z
user: login: nascheme
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T21:03:30Z
user: login: markshannon
body: Looks good.

Maybe we could reduce the 27k limit, but I don't know if it is worth messing with here.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T12:49:39Z

Review comments:
diff_hunk: @@ -0,0 +1,50 @@
+# Run by test_gc.
+from test import support
+import _testinternalcapi
+import gc
+import unittest
+
+class IncrementalGCTests(unittest.TestCase):
+
+    # Use small increments to emulate longer running process in a shorter time
+    @support.gc_threshold(200, 10)
+    def test_incremental_gc_handles_fast_cycle_creation(self):
+
+        class LinkedList:
+
+            #Use slots to reduce number of implicit objects
+            __slots__ = "next", "prev", "surprise"
+
+            def __init__(self, next=None, prev=None):
+                self.next = next
+                if next is not None:
+                    next.prev = self
+                self.prev = prev
+                if prev is not None:
+                    prev.next = self
+
+        def make_ll(depth):
+            head = LinkedList()
+            for i in range(depth):
+                head = LinkedList(head, head.prev)
+            return head
+
+        head = make_ll(1000)
+        count = 1000
path: Lib/test/_test_gc_fast_cycles.py
user: login: chris-eibl
body: Let's get rid of count now?
created_at: 2025-05-06T20:08:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+# Run by test_gc.
+from test import support
+import _testinternalcapi
+import gc
+import unittest
+
+class IncrementalGCTests(unittest.TestCase):
+
+    # Use small increments to emulate longer running process in a shorter time
+    @support.gc_threshold(200, 10)
+    def test_incremental_gc_handles_fast_cycle_creation(self):
+
+        class LinkedList:
+
+            #Use slots to reduce number of implicit objects
+            __slots__ = "next", "prev", "surprise"
+
+            def __init__(self, next=None, prev=None):
+                self.next = next
+                if next is not None:
+                    next.prev = self
+                self.prev = prev
+                if prev is not None:
+                    prev.next = self
+
+        def make_ll(depth):
+            head = LinkedList()
+            for i in range(depth):
+                head = LinkedList(head, head.prev)
+            return head
+
+        head = make_ll(1000)
+        count = 1000
path: Lib/test/_test_gc_fast_cycles.py
user: login: nascheme
body: Sure.
created_at: 2025-05-06T21:03:30Z
author_association: MEMBER

PR comments:
user: login: miss-islington-app[bot]
created_at: 2025-05-08T21:54:36Z
body: Thanks @nascheme for the PR 🌮🎉.. I'm working now to backport this PR to: 3.14.
🐍🍒⛏🤖 I'm not a witch! I'm not a witch!
author_association: NONE
user: login: bedevere-app[bot]
created_at: 2025-05-08T21:54:46Z
body: [GH-133716](https://github.com/python/cpython/pull/133716) is a backport of this pull request to the [3.14 branch](https://github.com/python/cpython/tree/3.14).
author_association: NONE