Skip to content

Commit

Permalink
gh-118846: Fix free-threading test failures when run sequentially (#1…
Browse files Browse the repository at this point in the history
…18864)

The free-threaded build currently immortalizes some objects once the
first thread is started. This can lead to test failures depending on the
order in which tests are run. This PR addresses those failures by
suppressing immortalization or skipping the affected tests.
  • Loading branch information
colesbury committed May 10, 2024
1 parent aa36f83 commit b309c8e
Show file tree
Hide file tree
Showing 8 changed files with 35 additions and 6 deletions.
1 change: 1 addition & 0 deletions Lib/test/seq_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ def test_pickle(self):
self.assertEqual(lst2, lst)
self.assertNotEqual(id(lst2), id(lst))

@support.suppress_immortalization()
def test_free_after_iterating(self):
support.check_free_after_iterating(self, iter, self.type2test)
support.check_free_after_iterating(self, reversed, self.type2test)
5 changes: 4 additions & 1 deletion Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from test.support import threading_helper
from test.support import warnings_helper
from test.support import requires_limited_api
from test.support import requires_gil_enabled, expected_failure_if_gil_disabled
from test.support import suppress_immortalization
from test.support import expected_failure_if_gil_disabled
from test.support import Py_GIL_DISABLED
from test.support.script_helper import assert_python_failure, assert_python_ok, run_python_until_end
try:
Expand Down Expand Up @@ -481,6 +482,7 @@ def test_heap_ctype_doc_and_text_signature(self):
def test_null_type_doc(self):
self.assertEqual(_testcapi.NullTpDocType.__doc__, None)

@suppress_immortalization()
def test_subclass_of_heap_gc_ctype_with_tpdealloc_decrefs_once(self):
class HeapGcCTypeSubclass(_testcapi.HeapGcCType):
def __init__(self):
Expand All @@ -498,6 +500,7 @@ def __init__(self):
del subclass_instance
self.assertEqual(type_refcnt - 1, sys.getrefcount(HeapGcCTypeSubclass))

@suppress_immortalization()
def test_subclass_of_heap_gc_ctype_with_del_modifying_dunder_class_only_decrefs_once(self):
class A(_testcapi.HeapGcCType):
def __init__(self):
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5014,6 +5014,7 @@ def __new__(cls):
cls.lst = [2**i for i in range(10000)]
X.descr

@support.suppress_immortalization()
def test_remove_subclass(self):
# bpo-46417: when the last subclass of a type is deleted,
# remove_subclass() clears the internal dictionary of subclasses:
Expand Down
20 changes: 18 additions & 2 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from test import support
from test.support import (verbose, refcount_test,
cpython_only, requires_subprocess,
requires_gil_enabled)
requires_gil_enabled, suppress_immortalization,
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
Expand Down Expand Up @@ -109,6 +110,7 @@ def test_tuple(self):
del l
self.assertEqual(gc.collect(), 2)

@suppress_immortalization()
def test_class(self):
class A:
pass
Expand All @@ -117,6 +119,7 @@ class A:
del A
self.assertNotEqual(gc.collect(), 0)

@suppress_immortalization()
def test_newstyleclass(self):
class A(object):
pass
Expand All @@ -133,6 +136,7 @@ class A:
del a
self.assertNotEqual(gc.collect(), 0)

@suppress_immortalization()
def test_newinstance(self):
class A(object):
pass
Expand Down Expand Up @@ -219,6 +223,7 @@ class B(object):
self.fail("didn't find obj in garbage (finalizer)")
gc.garbage.remove(obj)

@suppress_immortalization()
def test_function(self):
# Tricky: f -> d -> f, code should call d.clear() after the exec to
# break the cycle.
Expand Down Expand Up @@ -561,6 +566,7 @@ def test_get_referents(self):

self.assertEqual(gc.get_referents(1, 'a', 4j), [])

@suppress_immortalization()
def test_is_tracked(self):
# Atomic built-in types are not tracked, user-defined objects and
# mutable containers are.
Expand Down Expand Up @@ -598,7 +604,9 @@ class UserFloatSlots(float):
class UserIntSlots(int):
__slots__ = ()

self.assertTrue(gc.is_tracked(gc))
if not Py_GIL_DISABLED:
# gh-117783: modules may be immortalized in free-threaded build
self.assertTrue(gc.is_tracked(gc))
self.assertTrue(gc.is_tracked(UserClass))
self.assertTrue(gc.is_tracked(UserClass()))
self.assertTrue(gc.is_tracked(UserInt()))
Expand Down Expand Up @@ -1347,6 +1355,10 @@ def callback(ignored):
junk = []
i = 0
detector = GC_Detector()
if Py_GIL_DISABLED:
# The free-threaded build doesn't have multiple generations, so
# just trigger a GC manually.
gc.collect()
while not detector.gc_happened:
i += 1
if i > 10000:
Expand Down Expand Up @@ -1415,6 +1427,10 @@ def __del__(self):
detector = GC_Detector()
junk = []
i = 0
if Py_GIL_DISABLED:
# The free-threaded build doesn't have multiple generations, so
# just trigger a GC manually.
gc.collect()
while not detector.gc_happened:
i += 1
if i > 10000:
Expand Down
4 changes: 3 additions & 1 deletion Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
except ImportError:
ThreadPoolExecutor = None

from test.support import cpython_only, import_helper
from test.support import cpython_only, import_helper, suppress_immortalization
from test.support import MISSING_C_DOCSTRINGS, ALWAYS_EQ
from test.support.import_helper import DirsOnSysPath, ready_to_import
from test.support.os_helper import TESTFN, temp_cwd
Expand Down Expand Up @@ -768,6 +768,7 @@ def test_getfile_builtin_function_or_method(self):
inspect.getfile(list.append)
self.assertIn('expected, got', str(e_append.exception))

@suppress_immortalization()
def test_getfile_class_without_module(self):
class CM(type):
@property
Expand Down Expand Up @@ -2430,6 +2431,7 @@ def __getattribute__(self, attr):

self.assertFalse(test.called)

@suppress_immortalization()
def test_cache_does_not_cause_classes_to_persist(self):
# regression test for gh-118013:
# check that the internal _shadowed_dict cache does not cause
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_module/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import weakref
from test.support import gc_collect
from test.support import import_helper
from test.support import suppress_immortalization
from test.support.script_helper import assert_python_ok

import sys
Expand Down Expand Up @@ -103,6 +104,7 @@ def f():
gc_collect()
self.assertEqual(f().__dict__["bar"], 4)

@suppress_immortalization()
def test_clear_dict_in_ref_cycle(self):
destroyed = []
m = ModuleType("foo")
Expand All @@ -118,6 +120,7 @@ def __del__(self):
gc_collect()
self.assertEqual(destroyed, [1])

@suppress_immortalization()
def test_weakref(self):
m = ModuleType("foo")
wr = weakref.ref(m)
Expand Down
4 changes: 3 additions & 1 deletion Lib/test/test_trace.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from pickle import dump
import sys
from test.support import captured_stdout, requires_resource
from test.support import captured_stdout, requires_resource, requires_gil_enabled
from test.support.os_helper import (TESTFN, rmtree, unlink)
from test.support.script_helper import assert_python_ok, assert_python_failure
import textwrap
Expand Down Expand Up @@ -301,6 +301,7 @@ def test_loop_caller_importing(self):

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'pre-existing trace function throws off measurements')
@requires_gil_enabled("gh-117783: immortalization of types affects traced method names")
def test_inst_method_calling(self):
obj = TracedClass(20)
self.tracer.runfunc(obj.inst_method_calling, 1)
Expand Down Expand Up @@ -334,6 +335,7 @@ def setUp(self):

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'pre-existing trace function throws off measurements')
@requires_gil_enabled("gh-117783: immortalization of types affects traced method names")
def test_loop_caller_importing(self):
self.tracer.runfunc(traced_func_importing_caller, 1)

Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_zoneinfo/test_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from datetime import date, datetime, time, timedelta, timezone
from functools import cached_property

from test.support import MISSING_C_DOCSTRINGS
from test.support import MISSING_C_DOCSTRINGS, requires_gil_enabled
from test.test_zoneinfo import _support as test_support
from test.test_zoneinfo._support import OS_ENV_LOCK, TZPATH_TEST_LOCK, ZoneInfoTestBase
from test.support.import_helper import import_module, CleanImport
Expand Down Expand Up @@ -1931,6 +1931,7 @@ def test_cache_location(self):
self.assertFalse(hasattr(c_zoneinfo.ZoneInfo, "_weak_cache"))
self.assertTrue(hasattr(py_zoneinfo.ZoneInfo, "_weak_cache"))

@requires_gil_enabled("gh-117783: types may be immortalized")
def test_gc_tracked(self):
import gc

Expand Down

0 comments on commit b309c8e

Please sign in to comment.