|
46 | 46 | "check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer", |
47 | 47 | "requires_limited_api", "requires_specialization", "thread_unsafe", |
48 | 48 | "skip_if_unlimited_stack_size", "skip_if_huge_c_stack", |
| 49 | + "run_with_limited_c_stack", |
49 | 50 | # sys |
50 | 51 | "MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi", |
51 | 52 | "is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval", |
@@ -2806,30 +2807,94 @@ def exceeds_recursion_limit(): |
2806 | 2807 | return 150_000 |
2807 | 2808 |
|
2808 | 2809 |
|
| 2810 | +def _has_huge_c_stack(depth): |
| 2811 | + """Check that *depth* recursive calls cannot exhaust the C stack.""" |
| 2812 | + try: |
| 2813 | + from _testinternalcapi import get_c_recursion_remaining |
| 2814 | + except ImportError: |
| 2815 | + # Fall back to checking for an unlimited stack size. |
| 2816 | + if is_emscripten or is_wasi or os.name == "nt": |
| 2817 | + return False |
| 2818 | + import resource |
| 2819 | + soft, hard = resource.getrlimit(resource.RLIMIT_STACK) |
| 2820 | + return soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF) |
| 2821 | + else: |
| 2822 | + remaining = get_c_recursion_remaining() |
| 2823 | + # A negative value means integer overflow in the estimate |
| 2824 | + # (e.g. with an unlimited RLIMIT_STACK). The estimate is based on |
| 2825 | + # the size of the interpreter loop frame, so it is only a lower |
| 2826 | + # bound for recursion with smaller C frames. |
| 2827 | + return remaining >= depth or remaining < 0 |
| 2828 | + |
| 2829 | + |
2809 | 2830 | def skip_if_huge_c_stack(depth=150_000): |
2810 | 2831 | """Skip decorator for tests which cannot overflow the C stack. |
2811 | 2832 |
|
2812 | 2833 | Tests exhausting the C stack with *depth* recursive calls cannot |
2813 | 2834 | trigger the recursion protection if the C stack is too large (e.g. |
2814 | 2835 | with a large or unlimited RLIMIT_STACK), and either fail, or run |
2815 | 2836 | for a very long time, or crash, or consume all memory. |
| 2837 | +
|
| 2838 | + Prefer run_with_limited_c_stack() for tests recursing to a fixed depth. |
2816 | 2839 | """ |
2817 | | - try: |
2818 | | - from _testinternalcapi import get_c_recursion_remaining |
2819 | | - except ImportError: |
2820 | | - # Fall back to checking for an unlimited stack size. |
2821 | | - huge = False |
2822 | | - if not (is_emscripten or is_wasi) and os.name != "nt": |
2823 | | - import resource |
2824 | | - soft, hard = resource.getrlimit(resource.RLIMIT_STACK) |
2825 | | - huge = soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF) |
2826 | | - else: |
2827 | | - remaining = get_c_recursion_remaining() |
2828 | | - # A negative value means integer overflow in the estimate |
2829 | | - # (e.g. with an unlimited RLIMIT_STACK). |
2830 | | - huge = remaining >= depth or remaining < 0 |
2831 | 2840 | return unittest.skipIf( |
2832 | | - huge, f"the C stack is large enough for {depth} recursive calls") |
| 2841 | + _has_huge_c_stack(depth), |
| 2842 | + f"the C stack is large enough for {depth} recursive calls") |
| 2843 | + |
| 2844 | + |
| 2845 | +# Small enough to be exhausted by tens of thousands of recursive calls, |
| 2846 | +# but not smaller than Py_C_STACK_SIZE (4 MiB) which the interpreter |
| 2847 | +# assumes if it cannot query the thread stack size. |
| 2848 | +C_STACK_SIZE = 8 * 1024 * 1024 |
| 2849 | + |
| 2850 | + |
| 2851 | +def run_with_limited_c_stack(depth=150_000, size=C_STACK_SIZE): |
| 2852 | + """Decorator for tests exhausting the C stack with *depth* recursive calls. |
| 2853 | +
|
| 2854 | + Run the test in a separate thread with the C stack of *size* bytes, so |
| 2855 | + that the outcome does not depend on the C stack size of the main thread |
| 2856 | + (which can be large or unlimited, see RLIMIT_STACK). |
| 2857 | +
|
| 2858 | + If a thread with the limited C stack cannot be created, run the test in |
| 2859 | + the current thread, but skip it if the C stack is too large. |
| 2860 | + """ |
| 2861 | + reason = f"the C stack is large enough for {depth} recursive calls" |
| 2862 | + def decorator(test): |
| 2863 | + @functools.wraps(test) |
| 2864 | + def wrapper(*args, **kwargs): |
| 2865 | + def run_test(): |
| 2866 | + # The C stack can still be too large if limiting it failed. |
| 2867 | + if _has_huge_c_stack(depth): |
| 2868 | + raise unittest.SkipTest(reason) |
| 2869 | + test(*args, **kwargs) |
| 2870 | + |
| 2871 | + try: |
| 2872 | + import threading |
| 2873 | + old_size = threading.stack_size(size) |
| 2874 | + except (ImportError, ValueError, RuntimeError): |
| 2875 | + # Setting the thread stack size is not supported. |
| 2876 | + return run_test() |
| 2877 | + |
| 2878 | + exceptions = [] |
| 2879 | + def run(): |
| 2880 | + try: |
| 2881 | + run_test() |
| 2882 | + except BaseException as exc: |
| 2883 | + exceptions.append(exc) |
| 2884 | + |
| 2885 | + thread = threading.Thread(target=run) |
| 2886 | + try: |
| 2887 | + thread.start() |
| 2888 | + except RuntimeError: |
| 2889 | + # Threads are not supported. |
| 2890 | + return run_test() |
| 2891 | + finally: |
| 2892 | + threading.stack_size(old_size) |
| 2893 | + thread.join() |
| 2894 | + if exceptions: |
| 2895 | + raise exceptions[0] |
| 2896 | + return wrapper |
| 2897 | + return decorator |
2833 | 2898 |
|
2834 | 2899 |
|
2835 | 2900 | # Windows doesn't have os.uname() but it doesn't support s390x. |
|
0 commit comments