From: Serhiy Storchaka Date: Mon, 20 Jul 2026 05:58:25 +0000 (+0300) Subject: gh-154211: Skip C-stack exhaustion tests when the C stack is huge (GH-154212) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=863b31268c3160af1febcee4c79ff6ed380f328f;p=thirdparty%2FPython%2Fcpython.git gh-154211: Skip C-stack exhaustion tests when the C stack is huge (GH-154212) Co-authored-by: Claude Fable 5 --- diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py index e76f79c274e7..ec2aa59f2cb8 100644 --- a/Lib/test/list_tests.py +++ b/Lib/test/list_tests.py @@ -6,7 +6,7 @@ import sys 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 @@ class CommonTest(seq_tests.CommonTest): 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 9624072e69ad..ae2fb3f5f448 100644 --- a/Lib/test/mapping_tests.py +++ b/Lib/test/mapping_tests.py @@ -629,6 +629,7 @@ class TestHashMappingProtocol(TestMappingProtocol): 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 e5bc2fd9b3d0..f4c4b4c1acfc 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -45,7 +45,7 @@ __all__ = [ "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 31e218df127a..87d63fcd8529 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -26,7 +26,9 @@ except ImportError: 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 @@ class AST_Tests(unittest.TestCase): 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 @@ Module( 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 @@ Module( with support.infinite_recursion(): compile(ast.Expression(e), "", "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 f42526aee194..1e42356be21d 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -3,7 +3,7 @@ from test.support import (cpython_only, is_wasi, requires_limited_api, Py_DEBUG, 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 @@ class TestRecursion(unittest.TestCase): @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 e401941c6d69..62d8806b75d9 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -555,6 +555,7 @@ class ClassTests(unittest.TestCase): 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 ecc69b5383e3..2c7b1181817c 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -724,6 +724,7 @@ class TestSpecifics(unittest.TestCase): @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 98f56b5ae87f..9455c9e00514 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -377,6 +377,7 @@ class TestCopy(unittest.TestCase): 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 @@ class TestCopy(unittest.TestCase): 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 @@ class TestCopy(unittest.TestCase): 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 @@ class TestCopy(unittest.TestCase): 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 @@ class TestCopy(unittest.TestCase): 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 c9d728e9ae2f..abbfb6efdcea 100644 --- a/Lib/test/test_ctypes/test_as_parameter.py +++ b/Lib/test/test_ctypes/test_as_parameter.py @@ -5,7 +5,8 @@ from ctypes import (Structure, CDLL, CFUNCTYPE, 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 BasicWrapTestCase(unittest.TestCase): (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 ba504119d294..7ac22455541f 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -3774,6 +3774,7 @@ class ClassPropertiesAndMethods(unittest.TestCase): 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 ClassPropertiesAndMethods(unittest.TestCase): # 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 6d61c71e162f..dc31d403b837 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -678,6 +678,7 @@ class DictTest(unittest.TestCase): 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 691fc5dfb5e8..9816ae6c033e 100644 --- a/Lib/test/test_dictviews.py +++ b/Lib/test/test_dictviews.py @@ -2,7 +2,9 @@ import collections.abc 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 @@ class DictSetTest(unittest.TestCase): # 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 35ffc9a0a4cf..325b1c91fa5a 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 @@ class DeepRecursionInSplitAndSubgroup(unittest.TestCase): 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 @@ class DeepRecursionInSplitAndSubgroup(unittest.TestCase): 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 6e458017298d..ee4888c18598 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1523,6 +1523,7 @@ class ExceptionTests(unittest.TestCase): @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 c30386afe418..941dd7249a48 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -438,7 +438,7 @@ class TestPartial: 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 d97535ba46e6..16d879a3831b 100644 --- a/Lib/test/test_isinstance.py +++ b/Lib/test/test_isinstance.py @@ -263,6 +263,7 @@ class TestIsInstanceIsSubclass(unittest.TestCase): 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 @@ class TestIsInstanceIsSubclass(unittest.TestCase): # 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 @@ class TestIsInstanceIsSubclass(unittest.TestCase): 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 @@ class TestIsInstanceIsSubclass(unittest.TestCase): 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 d732fc80cf1c..cbae9fbb4d62 100644 --- a/Lib/test/test_json/test_recursion.py +++ b/Lib/test/test_json/test_recursion.py @@ -69,7 +69,7 @@ class TestRecursion: @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 @@ class TestRecursion: 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 98be98a25278..a2ef67baadd4 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -904,7 +904,7 @@ class ElementDeclHandlerTest(unittest.TestCase): 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 acec4ec2ca25..bc7f1ac12b42 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -3246,7 +3246,7 @@ class BadElementTest(ElementTestCase, unittest.TestCase): 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 000000000000..8030db869c7b --- /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``).