Skip to content

Commit b669be3

Browse files
[3.14] gh-155109: Run tests exhausting the C stack with a limited C stack (GH-155120) (GH-155165)
Add the @support.run_with_limited_c_stack() decorator which runs the test in a thread with a 8 MiB C stack, so that the outcome does not depend on RLIMIT_STACK. Use it in tests which recurse to a fixed depth -- @support.skip_if_huge_c_stack() failed to skip them with a 16 MiB stack. (cherry picked from commit ce5ae29) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 46cdf53 commit b669be3

13 files changed

Lines changed: 102 additions & 32 deletions

Lib/test/list_tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from functools import cmp_to_key
77

88
from test import seq_tests
9-
from test.support import ALWAYS_EQ, NEVER_EQ, skip_if_huge_c_stack
9+
from test.support import ALWAYS_EQ, NEVER_EQ, run_with_limited_c_stack
1010
from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow
1111

1212

@@ -60,7 +60,7 @@ def test_repr(self):
6060
self.assertEqual(str(a2), "[0, 1, 2, [...], 3]")
6161
self.assertEqual(repr(a2), "[0, 1, 2, [...], 3]")
6262

63-
@skip_if_huge_c_stack(200_000)
63+
@run_with_limited_c_stack(200_000)
6464
@skip_wasi_stack_overflow()
6565
@skip_emscripten_stack_overflow()
6666
def test_repr_deep(self):

Lib/test/mapping_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ def __repr__(self):
622622
d = self._full_mapping({1: BadRepr()})
623623
self.assertRaises(Exc, repr, d)
624624

625-
@support.skip_if_huge_c_stack()
625+
@support.run_with_limited_c_stack()
626626
@support.skip_wasi_stack_overflow()
627627
@support.skip_emscripten_stack_overflow()
628628
@support.skip_if_sanitizer("requires deep stack", ub=True)

Lib/test/support/__init__.py

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer",
4646
"requires_limited_api", "requires_specialization", "thread_unsafe",
4747
"skip_if_unlimited_stack_size", "skip_if_huge_c_stack",
48+
"run_with_limited_c_stack",
4849
# sys
4950
"MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi",
5051
"is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval",
@@ -2743,30 +2744,94 @@ def exceeds_recursion_limit():
27432744
return 150_000
27442745

27452746

2747+
def _has_huge_c_stack(depth):
2748+
"""Check that *depth* recursive calls cannot exhaust the C stack."""
2749+
try:
2750+
from _testinternalcapi import get_c_recursion_remaining
2751+
except ImportError:
2752+
# Fall back to checking for an unlimited stack size.
2753+
if is_emscripten or is_wasi or os.name == "nt":
2754+
return False
2755+
import resource
2756+
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
2757+
return soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF)
2758+
else:
2759+
remaining = get_c_recursion_remaining()
2760+
# A negative value means integer overflow in the estimate
2761+
# (e.g. with an unlimited RLIMIT_STACK). The estimate is based on
2762+
# the size of the interpreter loop frame, so it is only a lower
2763+
# bound for recursion with smaller C frames.
2764+
return remaining >= depth or remaining < 0
2765+
2766+
27462767
def skip_if_huge_c_stack(depth=150_000):
27472768
"""Skip decorator for tests which cannot overflow the C stack.
27482769
27492770
Tests exhausting the C stack with *depth* recursive calls cannot
27502771
trigger the recursion protection if the C stack is too large (e.g.
27512772
with a large or unlimited RLIMIT_STACK), and either fail, or run
27522773
for a very long time, or crash, or consume all memory.
2774+
2775+
Prefer run_with_limited_c_stack() for tests recursing to a fixed depth.
27532776
"""
2754-
try:
2755-
from _testinternalcapi import get_c_recursion_remaining
2756-
except ImportError:
2757-
# Fall back to checking for an unlimited stack size.
2758-
huge = False
2759-
if not (is_emscripten or is_wasi) and os.name != "nt":
2760-
import resource
2761-
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
2762-
huge = soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF)
2763-
else:
2764-
remaining = get_c_recursion_remaining()
2765-
# A negative value means integer overflow in the estimate
2766-
# (e.g. with an unlimited RLIMIT_STACK).
2767-
huge = remaining >= depth or remaining < 0
27682777
return unittest.skipIf(
2769-
huge, f"the C stack is large enough for {depth} recursive calls")
2778+
_has_huge_c_stack(depth),
2779+
f"the C stack is large enough for {depth} recursive calls")
2780+
2781+
2782+
# Small enough to be exhausted by tens of thousands of recursive calls,
2783+
# but not smaller than Py_C_STACK_SIZE (4 MiB) which the interpreter
2784+
# assumes if it cannot query the thread stack size.
2785+
C_STACK_SIZE = 8 * 1024 * 1024
2786+
2787+
2788+
def run_with_limited_c_stack(depth=150_000, size=C_STACK_SIZE):
2789+
"""Decorator for tests exhausting the C stack with *depth* recursive calls.
2790+
2791+
Run the test in a separate thread with the C stack of *size* bytes, so
2792+
that the outcome does not depend on the C stack size of the main thread
2793+
(which can be large or unlimited, see RLIMIT_STACK).
2794+
2795+
If a thread with the limited C stack cannot be created, run the test in
2796+
the current thread, but skip it if the C stack is too large.
2797+
"""
2798+
reason = f"the C stack is large enough for {depth} recursive calls"
2799+
def decorator(test):
2800+
@functools.wraps(test)
2801+
def wrapper(*args, **kwargs):
2802+
def run_test():
2803+
# The C stack can still be too large if limiting it failed.
2804+
if _has_huge_c_stack(depth):
2805+
raise unittest.SkipTest(reason)
2806+
test(*args, **kwargs)
2807+
2808+
try:
2809+
import threading
2810+
old_size = threading.stack_size(size)
2811+
except (ImportError, ValueError, RuntimeError):
2812+
# Setting the thread stack size is not supported.
2813+
return run_test()
2814+
2815+
exceptions = []
2816+
def run():
2817+
try:
2818+
run_test()
2819+
except BaseException as exc:
2820+
exceptions.append(exc)
2821+
2822+
thread = threading.Thread(target=run)
2823+
try:
2824+
thread.start()
2825+
except RuntimeError:
2826+
# Threads are not supported.
2827+
return run_test()
2828+
finally:
2829+
threading.stack_size(old_size)
2830+
thread.join()
2831+
if exceptions:
2832+
raise exceptions[0]
2833+
return wrapper
2834+
return decorator
27702835

27712836

27722837
# Windows doesn't have os.uname() but it doesn't support s390x.

Lib/test/test_ast/test_ast.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,8 @@ def next(self):
990990
enum._test_simple_enum(_Precedence, _ast_unparse._Precedence)
991991

992992
@support.cpython_only
993-
@support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 500_000)
993+
@support.run_with_limited_c_stack(
994+
100_000 if sys.platform == "android" else 500_000)
994995
@skip_wasi_stack_overflow()
995996
@skip_emscripten_stack_overflow()
996997
def test_ast_recursion_limit(self):

Lib/test/test_compile.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,8 @@ def test_yet_more_evil_still_undecodable(self):
724724

725725
@support.cpython_only
726726
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
727-
@support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 500_000)
727+
@support.run_with_limited_c_stack(
728+
100_000 if sys.platform == "android" else 500_000)
728729
@support.skip_emscripten_stack_overflow()
729730
def test_compiler_recursion_limit(self):
730731
# Compiler frames are small

Lib/test/test_dict.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@ def __repr__(self):
678678
d = {1: BadRepr()}
679679
self.assertRaises(Exc, repr, d)
680680

681-
@support.skip_if_huge_c_stack()
681+
@support.run_with_limited_c_stack()
682682
@support.skip_wasi_stack_overflow()
683683
@support.skip_emscripten_stack_overflow()
684684
def test_repr_deep(self):

Lib/test/test_dictviews.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pickle
44
import unittest
55
from test.support import (skip_emscripten_stack_overflow,
6-
skip_wasi_stack_overflow, skip_if_huge_c_stack,
6+
skip_wasi_stack_overflow, run_with_limited_c_stack,
77
exceeds_recursion_limit)
88

99
class DictSetTest(unittest.TestCase):
@@ -279,7 +279,7 @@ def test_recursive_repr(self):
279279
# Again.
280280
self.assertIsInstance(r, str)
281281

282-
@skip_if_huge_c_stack()
282+
@run_with_limited_c_stack()
283283
@skip_wasi_stack_overflow()
284284
@skip_emscripten_stack_overflow()
285285
def test_deeply_nested_repr(self):

Lib/test/test_exception_group.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import types
33
import unittest
44
from test.support import (skip_emscripten_stack_overflow,
5-
skip_wasi_stack_overflow, skip_if_huge_c_stack,
5+
skip_wasi_stack_overflow, run_with_limited_c_stack,
66
exceeds_recursion_limit)
77

88
class TestExceptionGroupTypeHierarchy(unittest.TestCase):
@@ -537,15 +537,15 @@ def make_deep_eg(self):
537537
e = ExceptionGroup('eg', [e])
538538
return e
539539

540-
@skip_if_huge_c_stack()
540+
@run_with_limited_c_stack()
541541
@skip_emscripten_stack_overflow()
542542
@skip_wasi_stack_overflow()
543543
def test_deep_split(self):
544544
e = self.make_deep_eg()
545545
with self.assertRaises(RecursionError):
546546
e.split(TypeError)
547547

548-
@skip_if_huge_c_stack()
548+
@run_with_limited_c_stack()
549549
@skip_emscripten_stack_overflow()
550550
@skip_wasi_stack_overflow()
551551
def test_deep_subgroup(self):

Lib/test/test_json/test_recursion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def default(self, o):
6969

7070

7171
@support.skip_if_pgo_task # fails during PGO training w/ some stack sizes
72-
@support.skip_if_huge_c_stack(500_000)
72+
@support.run_with_limited_c_stack(500_000)
7373
@support.skip_emscripten_stack_overflow()
7474
@support.skip_wasi_stack_overflow()
7575
def test_highly_nested_objects_decoding(self):

Lib/test/test_pyexpat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ def test_trigger_leak(self):
815815
parser.ElementDeclHandler = lambda _1, _2: None
816816
self.assertRaises(TypeError, parser.Parse, data, True)
817817

818-
@support.skip_if_huge_c_stack(800_000)
818+
@support.run_with_limited_c_stack(800_000)
819819
@support.skip_emscripten_stack_overflow()
820820
@support.skip_wasi_stack_overflow()
821821
def test_deeply_nested_content_model(self):

0 commit comments

Comments
 (0)