[3.14] gh-154211: Skip C-stack exhaustion tests when the C stack is huge (GH-154212) (GH-154231)

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/a739f8061b5456643c1a65bab9813555c2612829
commit: a739f8061b5456643c1a65bab9813555c2612829
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-20T09:23:42Z
summary:

[3.14] gh-154211: Skip C-stack exhaustion tests when the C stack is huge (GH-154212) (GH-154231)

(cherry picked from commit 863b31268c3160af1febcee4c79ff6ed380f328f)

Co-authored-by: Claude Fable 5 <[email protected]>

files:
A Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst
M Lib/test/list_tests.py
M Lib/test/mapping_tests.py
M Lib/test/support/__init__.py
M Lib/test/test_ast/test_ast.py
M Lib/test/test_call.py
M Lib/test/test_class.py
M Lib/test/test_compile.py
M Lib/test/test_copy.py
M Lib/test/test_ctypes/test_as_parameter.py
M Lib/test/test_descr.py
M Lib/test/test_dict.py
M Lib/test/test_dictviews.py
M Lib/test/test_exception_group.py
M Lib/test/test_exceptions.py
M Lib/test/test_functools.py
M Lib/test/test_isinstance.py
M Lib/test/test_json/test_recursion.py
M Lib/test/test_pyexpat.py
M Lib/test/test_xml_etree.py

diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py
index e76f79c274e744a..ec2aa59f2cb8728 100644
--- a/Lib/test/list_tests.py
+++ b/Lib/test/list_tests.py
@@ -6,7 +6,7 @@
 from functools import cmp_to_key
 
 from test import seq_tests
-from test.support import ALWAYS_EQ, NEVER_EQ
+from test.support import ALWAYS_EQ, NEVER_EQ, skip_if_huge_c_stack
 from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow
 
 
@@ -60,6 +60,7 @@ def test_repr(self):
         self.assertEqual(str(a2), "[0, 1, 2, [...], 3]")
         self.assertEqual(repr(a2), "[0, 1, 2, [...], 3]")
 
+    @skip_if_huge_c_stack(200_000)
     @skip_wasi_stack_overflow()
     @skip_emscripten_stack_overflow()
     def test_repr_deep(self):
diff --git a/Lib/test/mapping_tests.py b/Lib/test/mapping_tests.py
index 20306e1526d7b84..b95affd9f78479b 100644
--- a/Lib/test/mapping_tests.py
+++ b/Lib/test/mapping_tests.py
@@ -622,6 +622,7 @@ def __repr__(self):
         d = self._full_mapping({1: BadRepr()})
         self.assertRaises(Exc, repr, d)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     @support.skip_if_sanitizer("requires deep stack", ub=True)
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index c401e18247e6017..005ab19a93d941c 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -44,7 +44,7 @@
     "check__all__", "skip_if_buggy_ucrt_strfptime",
     "check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer",
     "requires_limited_api", "requires_specialization", "thread_unsafe",
-    "skip_if_unlimited_stack_size",
+    "skip_if_unlimited_stack_size", "skip_if_huge_c_stack",
     # sys
     "MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi",
     "is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval",
@@ -2743,6 +2743,32 @@ def exceeds_recursion_limit():
     return 150_000
 
 
+def skip_if_huge_c_stack(depth=150_000):
+    """Skip decorator for tests which cannot overflow the C stack.
+
+    Tests exhausting the C stack with *depth* recursive calls cannot
+    trigger the recursion protection if the C stack is too large (e.g.
+    with a large or unlimited RLIMIT_STACK), and either fail, or run
+    for a very long time, or crash, or consume all memory.
+    """
+    try:
+        from _testinternalcapi import get_c_recursion_remaining
+    except ImportError:
+        # Fall back to checking for an unlimited stack size.
+        huge = False
+        if not (is_emscripten or is_wasi) and os.name != "nt":
+            import resource
+            soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
+            huge = soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF)
+    else:
+        remaining = get_c_recursion_remaining()
+        # A negative value means integer overflow in the estimate
+        # (e.g. with an unlimited RLIMIT_STACK).
+        huge = remaining >= depth or remaining < 0
+    return unittest.skipIf(
+        huge, f"the C stack is large enough for {depth} recursive calls")
+
+
 # Windows doesn't have os.uname() but it doesn't support s390x.
 is_s390x = hasattr(os, 'uname') and os.uname().machine == 's390x'
 skip_on_s390x = unittest.skipIf(is_s390x, 'skipped on s390x')
diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index ef12ce0ce37de8e..3f607a00d2508e0 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -24,7 +24,9 @@
 
 from test import support
 from test.support import os_helper
-from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, skip_if_unlimited_stack_size
+from test.support import (skip_emscripten_stack_overflow,
+                          skip_wasi_stack_overflow,
+                          skip_if_unlimited_stack_size, skip_if_huge_c_stack)
 from test.support.ast_helper import ASTTestMixin
 from test.support.import_helper import ensure_lazy_imports
 from test.test_ast.utils import to_tuple
@@ -988,7 +990,7 @@ def next(self):
         enum._test_simple_enum(_Precedence, _ast_unparse._Precedence)
 
     @support.cpython_only
-    @skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 500_000)
     @skip_wasi_stack_overflow()
     @skip_emscripten_stack_overflow()
     def test_ast_recursion_limit(self):
@@ -1960,7 +1962,7 @@ def test_level_as_none(self):
         exec(code, ns)
         self.assertIn('sleep', ns)
 
-    @skip_if_unlimited_stack_size
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     def test_recursion_direct(self):
         e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, operand=ast.Constant(1))
@@ -1969,7 +1971,7 @@ def test_recursion_direct(self):
             with support.infinite_recursion():
                 compile(ast.Expression(e), "<test>", "eval")
 
-    @skip_if_unlimited_stack_size
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     def test_recursion_indirect(self):
         e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, operand=ast.Constant(1))
diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py
index b18f16a6efcf84a..d3af0eed893874e 100644
--- a/Lib/test/test_call.py
+++ b/Lib/test/test_call.py
@@ -3,7 +3,7 @@
                           set_recursion_limit, skip_on_s390x,
                           skip_emscripten_stack_overflow,
                           skip_wasi_stack_overflow, skip_if_sanitizer,
-                          import_helper)
+                          skip_if_huge_c_stack, import_helper)
 try:
     import _testcapi
 except ImportError:
@@ -1062,6 +1062,7 @@ def get_sp():
     @unittest.skipIf(is_wasi and Py_DEBUG, "requires deep stack")
     @skip_if_sanitizer("requires deep stack", thread=True)
     @unittest.skipIf(_testcapi is None, "requires _testcapi")
+    @skip_if_huge_c_stack(90_000)
     @skip_emscripten_stack_overflow()
     @skip_wasi_stack_overflow()
     def test_super_deep(self):
diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py
index 30e959189d68afc..df46c0de00d60aa 100644
--- a/Lib/test/test_class.py
+++ b/Lib/test/test_class.py
@@ -555,6 +555,7 @@ class Custom:
         self.assertFalse(hasattr(o, "__call__"))
         self.assertFalse(hasattr(c, "__call__"))
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def testSFBug532646(self):
diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index a6542b396ccfcfa..db1b4c6aa95186d 100644
--- a/Lib/test/test_compile.py
+++ b/Lib/test/test_compile.py
@@ -724,6 +724,7 @@ def test_yet_more_evil_still_undecodable(self):
 
     @support.cpython_only
     @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
+    @support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 500_000)
     @support.skip_emscripten_stack_overflow()
     def test_compiler_recursion_limit(self):
         # Compiler frames are small
diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py
index cfef24727e8c82e..b452ddbef5ccbee 100644
--- a/Lib/test/test_copy.py
+++ b/Lib/test/test_copy.py
@@ -371,6 +371,7 @@ def test_deepcopy_list(self):
         self.assertIsNot(x, y)
         self.assertIsNot(x[0], y[0])
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_list(self):
@@ -400,6 +401,7 @@ def test_deepcopy_tuple_of_immutables(self):
         y = copy.deepcopy(x)
         self.assertIs(x, y)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_tuple(self):
@@ -419,6 +421,7 @@ def test_deepcopy_dict(self):
         self.assertIsNot(x, y)
         self.assertIsNot(x["foo"], y["foo"])
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_dict(self):
@@ -579,6 +582,7 @@ def __eq__(self, other):
         self.assertIsNot(y, x)
         self.assertIsNot(y.foo, x.foo)
 
+    @support.skip_if_huge_c_stack()
     def test_deepcopy_reflexive_inst(self):
         class C:
             pass
@@ -641,6 +645,7 @@ def __eq__(self, other):
         self.assertEqual(y, x)
         self.assertIsNot(y.foo, x.foo)
 
+    @support.skip_if_huge_c_stack()
     def test_reconstruct_reflexive(self):
         class C(object):
             pass
diff --git a/Lib/test/test_ctypes/test_as_parameter.py b/Lib/test/test_ctypes/test_as_parameter.py
index c9d728e9ae2f9cb..abbfb6efdcea55b 100644
--- a/Lib/test/test_ctypes/test_as_parameter.py
+++ b/Lib/test/test_ctypes/test_as_parameter.py
@@ -5,7 +5,8 @@
                     c_short, c_int, c_long, c_longlong,
                     c_byte, c_wchar, c_float, c_double,
                     ArgumentError)
-from test.support import import_helper, skip_if_sanitizer, skip_emscripten_stack_overflow
+from test.support import (import_helper, skip_if_sanitizer,
+                          skip_emscripten_stack_overflow, skip_if_huge_c_stack)
 _ctypes_test = import_helper.import_module("_ctypes_test")
 
 
@@ -193,6 +194,7 @@ class S8I(Structure):
                              (9*2, 8*3, 7*4, 6*5, 5*6, 4*7, 3*8, 2*9))
 
     @skip_if_sanitizer('requires deep stack', thread=True)
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     def test_recursive_as_param(self):
         class A:
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 626e05f4a085235..00e5386b1d56b98 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -3692,6 +3692,7 @@ def f(a): return a
                            encoding='latin1', errors='replace')
         self.assertEqual(ba, b'abc\xbd?')
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_recursive_call(self):
@@ -4899,6 +4900,7 @@ class Thing:
                 # CALL_METHOD_DESCRIPTOR_O
                 deque.append(thing, thing)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_repr_as_str(self):
diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py
index c1e64a5d99b9e95..7cc6dc7715e2fdf 100644
--- a/Lib/test/test_dict.py
+++ b/Lib/test/test_dict.py
@@ -678,6 +678,7 @@ def __repr__(self):
         d = {1: BadRepr()}
         self.assertRaises(Exc, repr, d)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_repr_deep(self):
diff --git a/Lib/test/test_dictviews.py b/Lib/test/test_dictviews.py
index 691fc5dfb5e8ab9..9816ae6c033ec74 100644
--- a/Lib/test/test_dictviews.py
+++ b/Lib/test/test_dictviews.py
@@ -2,7 +2,9 @@
 import copy
 import pickle
 import unittest
-from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, exceeds_recursion_limit
+from test.support import (skip_emscripten_stack_overflow,
+                          skip_wasi_stack_overflow, skip_if_huge_c_stack,
+                          exceeds_recursion_limit)
 
 class DictSetTest(unittest.TestCase):
 
@@ -277,6 +279,7 @@ def test_recursive_repr(self):
         # Again.
         self.assertIsInstance(r, str)
 
+    @skip_if_huge_c_stack()
     @skip_wasi_stack_overflow()
     @skip_emscripten_stack_overflow()
     def test_deeply_nested_repr(self):
diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py
index db6ff9d33b44aa9..7d690ca59738ebb 100644
--- a/Lib/test/test_exception_group.py
+++ b/Lib/test/test_exception_group.py
@@ -1,7 +1,9 @@
 import collections
 import types
 import unittest
-from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, exceeds_recursion_limit
+from test.support import (skip_emscripten_stack_overflow,
+                          skip_wasi_stack_overflow, skip_if_huge_c_stack,
+                          exceeds_recursion_limit)
 
 class TestExceptionGroupTypeHierarchy(unittest.TestCase):
     def test_exception_group_types(self):
@@ -535,6 +537,7 @@ def make_deep_eg(self):
             e = ExceptionGroup('eg', [e])
         return e
 
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     @skip_wasi_stack_overflow()
     def test_deep_split(self):
@@ -542,6 +545,7 @@ def test_deep_split(self):
         with self.assertRaises(RecursionError):
             e.split(TypeError)
 
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     @skip_wasi_stack_overflow()
     def test_deep_subgroup(self):
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 3a0f94e44f292f1..e79e52911f66aba 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -1509,6 +1509,7 @@ def gen():
     @cpython_only
     @unittest.skipIf(_testcapi is None, "requires _testcapi")
     @force_not_colorized
+    @support.skip_if_huge_c_stack()
     def test_recursion_normalizing_infinite_exception(self):
         # Issue #30697. Test that a RecursionError is raised when
         # maximum recursion depth has been exceeded when creating
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index 16adb8dad7d8bc3..6531bd2bca33c70 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -438,7 +438,7 @@ def test_setstate_subclasses(self):
         self.assertIs(type(r[0]), tuple)
 
     @support.skip_if_sanitizer("thread sanitizer crashes in __tsan::FuncEntry", thread=True)
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     def test_recursive_pickle(self):
         with replaced_module('functools', self.module):
diff --git a/Lib/test/test_isinstance.py b/Lib/test/test_isinstance.py
index d97535ba46e677f..16d879a3831bc65 100644
--- a/Lib/test/test_isinstance.py
+++ b/Lib/test/test_isinstance.py
@@ -263,6 +263,7 @@ def test_subclass_tuple(self):
         self.assertEqual(True, issubclass(int, (int, (float, int))))
         self.assertEqual(True, issubclass(str, (str, (Child, str))))
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_subclass_recursion_limit(self):
@@ -270,6 +271,7 @@ def test_subclass_recursion_limit(self):
         # blown
         self.assertRaises(RecursionError, blowstack, issubclass, str, str)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_isinstance_recursion_limit(self):
@@ -317,7 +319,7 @@ def __bases__(self):
             self.assertRaises(RecursionError, issubclass, int, X())
             self.assertRaises(RecursionError, isinstance, 1, X())
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_infinite_recursion_via_bases_tuple(self):
@@ -329,7 +331,7 @@ def __getattr__(self, attr):
             with self.assertRaises(RecursionError):
                 issubclass(Failure(), int)
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_infinite_cycle_in_bases(self):
diff --git a/Lib/test/test_json/test_recursion.py b/Lib/test/test_json/test_recursion.py
index d732fc80cf1cf30..cbae9fbb4d624ba 100644
--- a/Lib/test/test_json/test_recursion.py
+++ b/Lib/test/test_json/test_recursion.py
@@ -69,7 +69,7 @@ def default(self, o):
 
 
     @support.skip_if_pgo_task  # fails during PGO training w/ some stack sizes
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(500_000)
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_highly_nested_objects_decoding(self):
@@ -102,7 +102,7 @@ def test_highly_nested_objects_encoding(self):
             with support.infinite_recursion(5000):
                 self.dumps(d)
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_endless_recursion(self):
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index 37dbb313b014e85..10e2e5f79908b0b 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -815,7 +815,7 @@ def test_trigger_leak(self):
         parser.ElementDeclHandler = lambda _1, _2: None
         self.assertRaises(TypeError, parser.Parse, data, True)
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(800_000)
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deeply_nested_content_model(self):
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 8c693bfbdb39d92..e268ed7f1ebef3d 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -3123,7 +3123,7 @@ def __deepcopy__(self, memo):
         self.assertEqual([c.tag for c in children[3:]],
                          [a.tag, b.tag, a.tag, b.tag])
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(500_000)
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deeply_nested_deepcopy(self):
diff --git a/Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst b/Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst
new file mode 100644
index 000000000000000..8030db869c7ba6e
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst
@@ -0,0 +1,3 @@
+Add ``test.support.skip_if_huge_c_stack()`` and use it to skip tests
+that exhaust the C stack if the stack limit is very large
+(e.g. on DragonFly BSD or with ``ulimit -s unlimited``).

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.