Skip to content

Commit 8ff1142

Browse files
authored
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().
1 parent 2cd170d commit 8ff1142

File tree

7 files changed

+177
-41
lines changed

7 files changed

+177
-41
lines changed

Lib/test/pythoninfo.py

Lines changed: 1 addition & 0 deletions
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

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2241,6 +2241,39 @@ def check_disallow_instantiation(testcase, tp, *args, **kwds):
22412241
msg = f"cannot create '{re.escape(qualname)}' instances"
22422242
testcase.assertRaisesRegex(TypeError, msg, tp, *args, **kwds)
22432243

2244+
def get_recursion_depth():
2245+
"""Get the recursion depth of the caller function.
2246+
2247+
In the __main__ module, at the module level, it should be 1.
2248+
"""
2249+
try:
2250+
import _testinternalcapi
2251+
depth = _testinternalcapi.get_recursion_depth()
2252+
except (ImportError, RecursionError) as exc:
2253+
# sys._getframe() + frame.f_back implementation.
2254+
try:
2255+
depth = 0
2256+
frame = sys._getframe()
2257+
while frame is not None:
2258+
depth += 1
2259+
frame = frame.f_back
2260+
finally:
2261+
# Break any reference cycles.
2262+
frame = None
2263+
2264+
# Ignore get_recursion_depth() frame.
2265+
return max(depth - 1, 1)
2266+
2267+
def get_recursion_available():
2268+
"""Get the number of available frames before RecursionError.
2269+
2270+
It depends on the current recursion depth of the caller function and
2271+
sys.getrecursionlimit().
2272+
"""
2273+
limit = sys.getrecursionlimit()
2274+
depth = get_recursion_depth()
2275+
return limit - depth
2276+
22442277
@contextlib.contextmanager
22452278
def set_recursion_limit(limit):
22462279
"""Temporarily change the recursion limit."""
@@ -2251,14 +2284,18 @@ def set_recursion_limit(limit):
22512284
finally:
22522285
sys.setrecursionlimit(original_limit)
22532286

2254-
def infinite_recursion(max_depth=75):
2287+
def infinite_recursion(max_depth=100):
22552288
"""Set a lower limit for tests that interact with infinite recursions
22562289
(e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some
22572290
debug windows builds, due to not enough functions being inlined the
22582291
stack size might not handle the default recursion limit (1000). See
22592292
bpo-11105 for details."""
2260-
return set_recursion_limit(max_depth)
2261-
2293+
if max_depth < 3:
2294+
raise ValueError("max_depth must be at least 3, got {max_depth}")
2295+
depth = get_recursion_depth()
2296+
depth = max(depth - 1, 1) # Ignore infinite_recursion() frame.
2297+
limit = depth + max_depth
2298+
return set_recursion_limit(limit)
22622299

22632300
def ignore_deprecations_from(module: str, *, like: str) -> object:
22642301
token = object()

Lib/test/test_support.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,83 @@ def test_has_strftime_extensions(self):
685685
else:
686686
self.assertTrue(support.has_strftime_extensions)
687687

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

Lib/test/test_sys.py

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -279,20 +279,29 @@ def test_switchinterval(self):
279279
finally:
280280
sys.setswitchinterval(orig)
281281

282-
def test_recursionlimit(self):
282+
def test_getrecursionlimit(self):
283+
limit = sys.getrecursionlimit()
284+
self.assertIsInstance(limit, int)
285+
self.assertGreater(limit, 1)
286+
283287
self.assertRaises(TypeError, sys.getrecursionlimit, 42)
284-
oldlimit = sys.getrecursionlimit()
285-
self.assertRaises(TypeError, sys.setrecursionlimit)
286-
self.assertRaises(ValueError, sys.setrecursionlimit, -42)
287-
sys.setrecursionlimit(10000)
288-
self.assertEqual(sys.getrecursionlimit(), 10000)
289-
sys.setrecursionlimit(oldlimit)
288+
289+
def test_setrecursionlimit(self):
290+
old_limit = sys.getrecursionlimit()
291+
try:
292+
sys.setrecursionlimit(10_005)
293+
self.assertEqual(sys.getrecursionlimit(), 10_005)
294+
295+
self.assertRaises(TypeError, sys.setrecursionlimit)
296+
self.assertRaises(ValueError, sys.setrecursionlimit, -42)
297+
finally:
298+
sys.setrecursionlimit(old_limit)
290299

291300
def test_recursionlimit_recovery(self):
292301
if hasattr(sys, 'gettrace') and sys.gettrace():
293302
self.skipTest('fatal error if run with a trace function')
294303

295-
oldlimit = sys.getrecursionlimit()
304+
old_limit = sys.getrecursionlimit()
296305
def f():
297306
f()
298307
try:
@@ -311,35 +320,31 @@ def f():
311320
with self.assertRaises(RecursionError):
312321
f()
313322
finally:
314-
sys.setrecursionlimit(oldlimit)
323+
sys.setrecursionlimit(old_limit)
315324

316325
@test.support.cpython_only
317-
def test_setrecursionlimit_recursion_depth(self):
326+
def test_setrecursionlimit_to_depth(self):
318327
# Issue #25274: Setting a low recursion limit must be blocked if the
319328
# current recursion depth is already higher than limit.
320329

321-
from _testinternalcapi import get_recursion_depth
322-
323-
def set_recursion_limit_at_depth(depth, limit):
324-
recursion_depth = get_recursion_depth()
325-
if recursion_depth >= depth:
326-
with self.assertRaises(RecursionError) as cm:
327-
sys.setrecursionlimit(limit)
328-
self.assertRegex(str(cm.exception),
329-
"cannot set the recursion limit to [0-9]+ "
330-
"at the recursion depth [0-9]+: "
331-
"the limit is too low")
332-
else:
333-
set_recursion_limit_at_depth(depth, limit)
334-
335-
oldlimit = sys.getrecursionlimit()
330+
old_limit = sys.getrecursionlimit()
336331
try:
337-
sys.setrecursionlimit(1000)
338-
339-
for limit in (10, 25, 50, 75, 100, 150, 200):
340-
set_recursion_limit_at_depth(limit, limit)
332+
depth = support.get_recursion_depth()
333+
with self.subTest(limit=sys.getrecursionlimit(), depth=depth):
334+
# depth + 1 is OK
335+
sys.setrecursionlimit(depth + 1)
336+
337+
# reset the limit to be able to call self.assertRaises()
338+
# context manager
339+
sys.setrecursionlimit(old_limit)
340+
with self.assertRaises(RecursionError) as cm:
341+
sys.setrecursionlimit(depth)
342+
self.assertRegex(str(cm.exception),
343+
"cannot set the recursion limit to [0-9]+ "
344+
"at the recursion depth [0-9]+: "
345+
"the limit is too low")
341346
finally:
342-
sys.setrecursionlimit(oldlimit)
347+
sys.setrecursionlimit(old_limit)
343348

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

Lib/test/test_tomllib/test_misc.py

Lines changed: 19 additions & 8 deletions
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)
Lines changed: 2 additions & 0 deletions
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.
Lines changed: 3 additions & 0 deletions
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)