from functools import cmp_to_key
from test import seq_tests
-from test.support import ALWAYS_EQ, NEVER_EQ, skip_if_huge_c_stack
+from test.support import ALWAYS_EQ, NEVER_EQ, run_with_limited_c_stack
from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow
self.assertEqual(str(a2), "[0, 1, 2, [...], 3]")
self.assertEqual(repr(a2), "[0, 1, 2, [...], 3]")
- @skip_if_huge_c_stack(200_000)
+ @run_with_limited_c_stack(200_000)
@skip_wasi_stack_overflow()
@skip_emscripten_stack_overflow()
def test_repr_deep(self):
d = self._full_mapping({1: BadRepr()})
self.assertRaises(Exc, repr, d)
- @support.skip_if_huge_c_stack()
+ @support.run_with_limited_c_stack()
@support.skip_wasi_stack_overflow()
@support.skip_emscripten_stack_overflow()
@support.skip_if_sanitizer("requires deep stack", ub=True)
"check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer",
"requires_limited_api", "requires_specialization", "thread_unsafe",
"skip_if_unlimited_stack_size", "skip_if_huge_c_stack",
+ "run_with_limited_c_stack",
# sys
"MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi",
"is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval",
return 150_000
+def _has_huge_c_stack(depth):
+ """Check that *depth* recursive calls cannot exhaust the C stack."""
+ try:
+ from _testinternalcapi import get_c_recursion_remaining
+ except ImportError:
+ # Fall back to checking for an unlimited stack size.
+ if is_emscripten or is_wasi or os.name == "nt":
+ return False
+ import resource
+ soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
+ return 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). The estimate is based on
+ # the size of the interpreter loop frame, so it is only a lower
+ # bound for recursion with smaller C frames.
+ return remaining >= depth or remaining < 0
+
+
def skip_if_huge_c_stack(depth=150_000):
"""Skip decorator for tests which cannot overflow the C stack.
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.
+
+ Prefer run_with_limited_c_stack() for tests recursing to a fixed depth.
"""
- 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")
+ _has_huge_c_stack(depth),
+ f"the C stack is large enough for {depth} recursive calls")
+
+
+# Small enough to be exhausted by tens of thousands of recursive calls,
+# but not smaller than Py_C_STACK_SIZE (4 MiB) which the interpreter
+# assumes if it cannot query the thread stack size.
+C_STACK_SIZE = 8 * 1024 * 1024
+
+
+def run_with_limited_c_stack(depth=150_000, size=C_STACK_SIZE):
+ """Decorator for tests exhausting the C stack with *depth* recursive calls.
+
+ Run the test in a separate thread with the C stack of *size* bytes, so
+ that the outcome does not depend on the C stack size of the main thread
+ (which can be large or unlimited, see RLIMIT_STACK).
+
+ If a thread with the limited C stack cannot be created, run the test in
+ the current thread, but skip it if the C stack is too large.
+ """
+ reason = f"the C stack is large enough for {depth} recursive calls"
+ def decorator(test):
+ @functools.wraps(test)
+ def wrapper(*args, **kwargs):
+ def run_test():
+ # The C stack can still be too large if limiting it failed.
+ if _has_huge_c_stack(depth):
+ raise unittest.SkipTest(reason)
+ test(*args, **kwargs)
+
+ try:
+ import threading
+ old_size = threading.stack_size(size)
+ except (ImportError, ValueError, RuntimeError):
+ # Setting the thread stack size is not supported.
+ return run_test()
+
+ exceptions = []
+ def run():
+ try:
+ run_test()
+ except BaseException as exc:
+ exceptions.append(exc)
+
+ thread = threading.Thread(target=run)
+ try:
+ thread.start()
+ except RuntimeError:
+ # Threads are not supported.
+ return run_test()
+ finally:
+ threading.stack_size(old_size)
+ thread.join()
+ if exceptions:
+ raise exceptions[0]
+ return wrapper
+ return decorator
# Windows doesn't have os.uname() but it doesn't support s390x.
enum._test_simple_enum(_Precedence, _ast_unparse._Precedence)
@support.cpython_only
- @support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 500_000)
+ @support.run_with_limited_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):
@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.run_with_limited_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
d = {1: BadRepr()}
self.assertRaises(Exc, repr, d)
- @support.skip_if_huge_c_stack()
+ @support.run_with_limited_c_stack()
@support.skip_wasi_stack_overflow()
@support.skip_emscripten_stack_overflow()
def test_repr_deep(self):
import pickle
import unittest
from test.support import (skip_emscripten_stack_overflow,
- skip_wasi_stack_overflow, skip_if_huge_c_stack,
+ skip_wasi_stack_overflow, run_with_limited_c_stack,
exceeds_recursion_limit)
class DictSetTest(unittest.TestCase):
# Again.
self.assertIsInstance(r, str)
- @skip_if_huge_c_stack()
+ @run_with_limited_c_stack()
@skip_wasi_stack_overflow()
@skip_emscripten_stack_overflow()
def test_deeply_nested_repr(self):
import types
import unittest
from test.support import (skip_emscripten_stack_overflow,
- skip_wasi_stack_overflow, skip_if_huge_c_stack,
+ skip_wasi_stack_overflow, run_with_limited_c_stack,
exceeds_recursion_limit)
class TestExceptionGroupTypeHierarchy(unittest.TestCase):
e = ExceptionGroup('eg', [e])
return e
- @skip_if_huge_c_stack()
+ @run_with_limited_c_stack()
@skip_emscripten_stack_overflow()
@skip_wasi_stack_overflow()
def test_deep_split(self):
with self.assertRaises(RecursionError):
e.split(TypeError)
- @skip_if_huge_c_stack()
+ @run_with_limited_c_stack()
@skip_emscripten_stack_overflow()
@skip_wasi_stack_overflow()
def test_deep_subgroup(self):
@support.skip_if_pgo_task # fails during PGO training w/ some stack sizes
- @support.skip_if_huge_c_stack(500_000)
+ @support.run_with_limited_c_stack(500_000)
@support.skip_emscripten_stack_overflow()
@support.skip_wasi_stack_overflow()
def test_highly_nested_objects_decoding(self):
parser.ElementDeclHandler = lambda _1, _2: None
self.assertRaises(TypeError, parser.Parse, data, True)
- @support.skip_if_huge_c_stack(800_000)
+ @support.run_with_limited_c_stack(800_000)
@support.skip_emscripten_stack_overflow()
@support.skip_wasi_stack_overflow()
def test_deeply_nested_content_model(self):
from test.support import (
captured_stderr, cpython_only, requires_docstrings, import_helper, run_code,
subTests, EqualToForwardRef,
- exceeds_recursion_limit, skip_if_huge_c_stack, skip_wasi_stack_overflow,
- skip_emscripten_stack_overflow,
+ exceeds_recursion_limit, run_with_limited_c_stack,
+ skip_wasi_stack_overflow, skip_emscripten_stack_overflow,
)
from test.typinganndata import (
ann_module695, mod_generics_cache, _typed_dict_helper,
self.assertEqual(MM2.__bases__, (collections.abc.MutableMapping, Generic))
@cpython_only
- @skip_if_huge_c_stack()
+ @run_with_limited_c_stack()
@skip_wasi_stack_overflow()
@skip_emscripten_stack_overflow()
def test_parameters_deep_recursion(self):
self.assertEqual([c.tag for c in children[3:]],
[a.tag, b.tag, a.tag, b.tag])
- @support.skip_if_huge_c_stack(500_000)
+ @support.run_with_limited_c_stack(500_000)
@support.skip_emscripten_stack_overflow()
@support.skip_wasi_stack_overflow()
def test_deeply_nested_deepcopy(self):
--- /dev/null
+Add ``test.support.run_with_limited_c_stack()`` and use it in tests that
+exhaust the C stack with a fixed number of recursive calls, so that their
+outcome no longer depends on the C stack size.