Skip to content

Commit 95eb984

Browse files
authored
[3.11] gh-108851: Fix tomllib recursion tests (#108853) (#109013)
gh-108851: Fix tomllib recursion tests (#108853) * Add get_recursion_available() and get_recursion_depth() functions to the test.support module. * Change infinite_recursion() default max_depth from 75 to 100. * Fix test_tomllib recursion tests for WASI buildbots: reduce the recursion limit and compute the maximum nested array/dict depending on the current available recursion limit. * test.pythoninfo logs sys.getrecursionlimit(). * Enhance test_sys tests on sys.getrecursionlimit() and sys.setrecursionlimit(). Backport notes: * Set support.infinite_recursion() minimum to 4 frames. * test_support.test_get_recursion_depth() uses limit-2, apparently f-string counts for 2 frames in Python 3.11. * test_sys.test_setrecursionlimit_to_depth() tests depth+2 instead of depth+1. (cherry picked from commit 8ff1142)
1 parent d61b8f9 commit 95eb984

File tree

7 files changed

+184
-42
lines changed

7 files changed

+184
-42
lines changed

Lib/test/pythoninfo.py

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def collect_sys(info_add):
112112

113113
call_func(info_add, 'sys.androidapilevel', sys, 'getandroidapilevel')
114114
call_func(info_add, 'sys.windowsversion', sys, 'getwindowsversion')
115+
call_func(info_add, 'sys.getrecursionlimit', sys, 'getrecursionlimit')
115116

116117
encoding = sys.getfilesystemencoding()
117118
if hasattr(sys, 'getfilesystemencodeerrors'):

Lib/test/support/__init__.py

+49-8
Original file line numberDiff line numberDiff line change
@@ -2197,20 +2197,61 @@ def check_disallow_instantiation(testcase, tp, *args, **kwds):
21972197
msg = f"cannot create '{re.escape(qualname)}' instances"
21982198
testcase.assertRaisesRegex(TypeError, msg, tp, *args, **kwds)
21992199

2200+
def get_recursion_depth():
2201+
"""Get the recursion depth of the caller function.
2202+
2203+
In the __main__ module, at the module level, it should be 1.
2204+
"""
2205+
try:
2206+
import _testinternalcapi
2207+
depth = _testinternalcapi.get_recursion_depth()
2208+
except (ImportError, RecursionError) as exc:
2209+
# sys._getframe() + frame.f_back implementation.
2210+
try:
2211+
depth = 0
2212+
frame = sys._getframe()
2213+
while frame is not None:
2214+
depth += 1
2215+
frame = frame.f_back
2216+
finally:
2217+
# Break any reference cycles.
2218+
frame = None
2219+
2220+
# Ignore get_recursion_depth() frame.
2221+
return max(depth - 1, 1)
2222+
2223+
def get_recursion_available():
2224+
"""Get the number of available frames before RecursionError.
2225+
2226+
It depends on the current recursion depth of the caller function and
2227+
sys.getrecursionlimit().
2228+
"""
2229+
limit = sys.getrecursionlimit()
2230+
depth = get_recursion_depth()
2231+
return limit - depth
2232+
22002233
@contextlib.contextmanager
2201-
def infinite_recursion(max_depth=75):
2234+
def set_recursion_limit(limit):
2235+
"""Temporarily change the recursion limit."""
2236+
original_limit = sys.getrecursionlimit()
2237+
try:
2238+
sys.setrecursionlimit(limit)
2239+
yield
2240+
finally:
2241+
sys.setrecursionlimit(original_limit)
2242+
2243+
def infinite_recursion(max_depth=100):
22022244
"""Set a lower limit for tests that interact with infinite recursions
22032245
(e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some
22042246
debug windows builds, due to not enough functions being inlined the
22052247
stack size might not handle the default recursion limit (1000). See
22062248
bpo-11105 for details."""
2207-
2208-
original_depth = sys.getrecursionlimit()
2209-
try:
2210-
sys.setrecursionlimit(max_depth)
2211-
yield
2212-
finally:
2213-
sys.setrecursionlimit(original_depth)
2249+
if max_depth < 4:
2250+
raise ValueError("max_depth must be at least 4, got {max_depth}")
2251+
depth = get_recursion_depth()
2252+
depth = max(depth - 1, 1) # Ignore infinite_recursion() frame.
2253+
limit = depth + max_depth
2254+
return set_recursion_limit(limit)
22142255

22152256
def ignore_deprecations_from(module: str, *, like: str) -> object:
22162257
token = object()

Lib/test/test_support.py

+79
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,85 @@ def test_has_strftime_extensions(self):
698698
else:
699699
self.assertTrue(support.has_strftime_extensions)
700700

701+
def test_get_recursion_depth(self):
702+
# test support.get_recursion_depth()
703+
code = textwrap.dedent("""
704+
from test import support
705+
import sys
706+
707+
def check(cond):
708+
if not cond:
709+
raise AssertionError("test failed")
710+
711+
# depth 1
712+
check(support.get_recursion_depth() == 1)
713+
714+
# depth 2
715+
def test_func():
716+
check(support.get_recursion_depth() == 2)
717+
test_func()
718+
719+
def test_recursive(depth, limit):
720+
if depth >= limit:
721+
# cannot call get_recursion_depth() at this depth,
722+
# it can raise RecursionError
723+
return
724+
get_depth = support.get_recursion_depth()
725+
print(f"test_recursive: {depth}/{limit}: "
726+
f"get_recursion_depth() says {get_depth}")
727+
check(get_depth == depth)
728+
test_recursive(depth + 1, limit)
729+
730+
# depth up to 25
731+
with support.infinite_recursion(max_depth=25):
732+
limit = sys.getrecursionlimit()
733+
print(f"test with sys.getrecursionlimit()={limit}")
734+
# Use limit-2 since f-string seems to consume 2 frames.
735+
test_recursive(2, limit - 2)
736+
737+
# depth up to 500
738+
with support.infinite_recursion(max_depth=500):
739+
limit = sys.getrecursionlimit()
740+
print(f"test with sys.getrecursionlimit()={limit}")
741+
# limit-2 since f-string seems to consume 2 frames
742+
test_recursive(2, limit - 2)
743+
""")
744+
script_helper.assert_python_ok("-c", code)
745+
746+
def test_recursion(self):
747+
# Test infinite_recursion() and get_recursion_available() functions.
748+
def recursive_function(depth):
749+
if depth:
750+
recursive_function(depth - 1)
751+
752+
for max_depth in (5, 25, 250):
753+
with support.infinite_recursion(max_depth):
754+
available = support.get_recursion_available()
755+
756+
# Recursion up to 'available' additional frames should be OK.
757+
recursive_function(available)
758+
759+
# Recursion up to 'available+1' additional frames must raise
760+
# RecursionError. Avoid self.assertRaises(RecursionError) which
761+
# can consume more than 3 frames and so raises RecursionError.
762+
try:
763+
recursive_function(available + 1)
764+
except RecursionError:
765+
pass
766+
else:
767+
self.fail("RecursionError was not raised")
768+
769+
# Test the bare minimumum: max_depth=4
770+
with support.infinite_recursion(4):
771+
try:
772+
recursive_function(4)
773+
except RecursionError:
774+
pass
775+
else:
776+
self.fail("RecursionError was not raised")
777+
778+
#self.assertEqual(available, 2)
779+
701780
# XXX -follows a list of untested API
702781
# make_legacy_pyc
703782
# is_resource_enabled

Lib/test/test_sys.py

+31-26
Original file line numberDiff line numberDiff line change
@@ -269,20 +269,29 @@ def test_switchinterval(self):
269269
finally:
270270
sys.setswitchinterval(orig)
271271

272-
def test_recursionlimit(self):
272+
def test_getrecursionlimit(self):
273+
limit = sys.getrecursionlimit()
274+
self.assertIsInstance(limit, int)
275+
self.assertGreater(limit, 1)
276+
273277
self.assertRaises(TypeError, sys.getrecursionlimit, 42)
274-
oldlimit = sys.getrecursionlimit()
275-
self.assertRaises(TypeError, sys.setrecursionlimit)
276-
self.assertRaises(ValueError, sys.setrecursionlimit, -42)
277-
sys.setrecursionlimit(10000)
278-
self.assertEqual(sys.getrecursionlimit(), 10000)
279-
sys.setrecursionlimit(oldlimit)
278+
279+
def test_setrecursionlimit(self):
280+
old_limit = sys.getrecursionlimit()
281+
try:
282+
sys.setrecursionlimit(10_005)
283+
self.assertEqual(sys.getrecursionlimit(), 10_005)
284+
285+
self.assertRaises(TypeError, sys.setrecursionlimit)
286+
self.assertRaises(ValueError, sys.setrecursionlimit, -42)
287+
finally:
288+
sys.setrecursionlimit(old_limit)
280289

281290
def test_recursionlimit_recovery(self):
282291
if hasattr(sys, 'gettrace') and sys.gettrace():
283292
self.skipTest('fatal error if run with a trace function')
284293

285-
oldlimit = sys.getrecursionlimit()
294+
old_limit = sys.getrecursionlimit()
286295
def f():
287296
f()
288297
try:
@@ -301,35 +310,31 @@ def f():
301310
with self.assertRaises(RecursionError):
302311
f()
303312
finally:
304-
sys.setrecursionlimit(oldlimit)
313+
sys.setrecursionlimit(old_limit)
305314

306315
@test.support.cpython_only
307-
def test_setrecursionlimit_recursion_depth(self):
316+
def test_setrecursionlimit_to_depth(self):
308317
# Issue #25274: Setting a low recursion limit must be blocked if the
309318
# current recursion depth is already higher than limit.
310319

311-
from _testinternalcapi import get_recursion_depth
312-
313-
def set_recursion_limit_at_depth(depth, limit):
314-
recursion_depth = get_recursion_depth()
315-
if recursion_depth >= depth:
320+
old_limit = sys.getrecursionlimit()
321+
try:
322+
depth = support.get_recursion_depth()
323+
with self.subTest(limit=sys.getrecursionlimit(), depth=depth):
324+
# depth + 2 is OK
325+
sys.setrecursionlimit(depth + 2)
326+
327+
# reset the limit to be able to call self.assertRaises()
328+
# context manager
329+
sys.setrecursionlimit(old_limit)
316330
with self.assertRaises(RecursionError) as cm:
317-
sys.setrecursionlimit(limit)
331+
sys.setrecursionlimit(depth + 1)
318332
self.assertRegex(str(cm.exception),
319333
"cannot set the recursion limit to [0-9]+ "
320334
"at the recursion depth [0-9]+: "
321335
"the limit is too low")
322-
else:
323-
set_recursion_limit_at_depth(depth, limit)
324-
325-
oldlimit = sys.getrecursionlimit()
326-
try:
327-
sys.setrecursionlimit(1000)
328-
329-
for limit in (10, 25, 50, 75, 100, 150, 200):
330-
set_recursion_limit_at_depth(limit, limit)
331336
finally:
332-
sys.setrecursionlimit(oldlimit)
337+
sys.setrecursionlimit(old_limit)
333338

334339
def test_getwindowsversion(self):
335340
# Raise SkipTest if sys doesn't have getwindowsversion attribute

Lib/test/test_tomllib/test_misc.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import tempfile
1111
import unittest
12+
from test import support
1213

1314
from . import tomllib
1415

@@ -92,13 +93,23 @@ def test_deepcopy(self):
9293
self.assertEqual(obj_copy, expected_obj)
9394

9495
def test_inline_array_recursion_limit(self):
95-
# 465 with default recursion limit
96-
nest_count = int(sys.getrecursionlimit() * 0.465)
97-
recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]"
98-
tomllib.loads(recursive_array_toml)
96+
with support.infinite_recursion(max_depth=100):
97+
available = support.get_recursion_available()
98+
nest_count = (available // 2) - 2
99+
# Add details if the test fails
100+
with self.subTest(limit=sys.getrecursionlimit(),
101+
available=available,
102+
nest_count=nest_count):
103+
recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]"
104+
tomllib.loads(recursive_array_toml)
99105

100106
def test_inline_table_recursion_limit(self):
101-
# 310 with default recursion limit
102-
nest_count = int(sys.getrecursionlimit() * 0.31)
103-
recursive_table_toml = nest_count * "key = {" + nest_count * "}"
104-
tomllib.loads(recursive_table_toml)
107+
with support.infinite_recursion(max_depth=100):
108+
available = support.get_recursion_available()
109+
nest_count = (available // 3) - 1
110+
# Add details if the test fails
111+
with self.subTest(limit=sys.getrecursionlimit(),
112+
available=available,
113+
nest_count=nest_count):
114+
recursive_table_toml = nest_count * "key = {" + nest_count * "}"
115+
tomllib.loads(recursive_table_toml)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add ``get_recursion_available()`` and ``get_recursion_depth()`` functions to
2+
the :mod:`test.support` module. Patch by Victor Stinner.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix ``test_tomllib`` recursion tests for WASI buildbots: reduce the recursion
2+
limit and compute the maximum nested array/dict depending on the current
3+
available recursion limit. Patch by Victor Stinner.

0 commit comments

Comments
 (0)