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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/863b31268c3160af1febcee4c79ff6ed380f328f
commit: 863b31268c3160af1febcee4c79ff6ed380f328f
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-20T08:58:25+03:00
summary:

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

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 9624072e69adfcc..ae2fb3f5f448e25 100644
--- a/Lib/test/mapping_tests.py
+++ b/Lib/test/mapping_tests.py
@@ -629,6 +629,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 e5bc2fd9b3d0954..f4c4b4c1acfc182 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -45,7 +45,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",
@@ -2839,6 +2839,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 31e218df127ae6b..87d63fcd8529336 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -26,7 +26,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
@@ -1023,7 +1025,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):
@@ -2096,7 +2098,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))
@@ -2105,7 +2107,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 f42526aee194174..1e42356be21ddd6 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 e401941c6d69700..62d8806b75d9db0 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 ecc69b5383e3f21..2c7b1181817cf55 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 98f56b5ae87f964..9455c9e00514ca4 100644
--- a/Lib/test/test_copy.py
+++ b/Lib/test/test_copy.py
@@ -377,6 +377,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):
@@ -406,6 +407,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):
@@ -449,6 +451,7 @@ def test_deepcopy_frozendict(self):
         self.assertIsNot(x[0], y[0])
         self.assertIs(y[0]['foo'], y)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_dict(self):
@@ -609,6 +612,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
@@ -671,6 +675,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 ba504119d294fd3..7ac22455541fe2c 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -3774,6 +3774,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):
@@ -5122,6 +5123,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 6d61c71e162f8be..dc31d403b837adb 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 35ffc9a0a4cf30a..325b1c91fa5aeef 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):
@@ -547,6 +549,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):
@@ -554,6 +557,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 6e458017298dffe..ee4888c18598f3d 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -1523,6 +1523,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 c30386afe41849b..941dd7249a48d91 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 98be98a25278024..a2ef67baadd4afb 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -904,7 +904,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 acec4ec2ca257c4..bc7f1ac12b42874 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -3246,7 +3246,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.