Skip to content

Commit 55d7e8f

Browse files
[3.12] gh-108851: Fix tomllib recursion tests (GH-108853) (#109012)
gh-108851: Fix tomllib recursion tests (GH-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(). (cherry picked from commit 8ff1142) Co-authored-by: Victor Stinner <[email protected]>
1 parent 038b0a9 commit 55d7e8f

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
@@ -2243,6 +2243,39 @@ def check_disallow_instantiation(testcase, tp, *args, **kwds):
22432243
msg = f"cannot create '{re.escape(qualname)}' instances"
22442244
testcase.assertRaisesRegex(TypeError, msg, tp, *args, **kwds)
22452245

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

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

22652302
def ignore_deprecations_from(module: str, *, like: str) -> object:
22662303
token = object()

Lib/test/test_support.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,83 @@ def test_has_strftime_extensions(self):
688688
else:
689689
self.assertTrue(support.has_strftime_extensions)
690690

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

Lib/test/test_sys.py

Lines changed: 35 additions & 30 deletions
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:
316-
with self.assertRaises(RecursionError) as cm:
317-
sys.setrecursionlimit(limit)
318-
self.assertRegex(str(cm.exception),
319-
"cannot set the recursion limit to [0-9]+ "
320-
"at the recursion depth [0-9]+: "
321-
"the limit is too low")
322-
else:
323-
set_recursion_limit_at_depth(depth, limit)
324-
325-
oldlimit = sys.getrecursionlimit()
320+
old_limit = sys.getrecursionlimit()
326321
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)
322+
depth = support.get_recursion_depth()
323+
with self.subTest(limit=sys.getrecursionlimit(), depth=depth):
324+
# depth + 1 is OK
325+
sys.setrecursionlimit(depth + 1)
326+
327+
# reset the limit to be able to call self.assertRaises()
328+
# context manager
329+
sys.setrecursionlimit(old_limit)
330+
with self.assertRaises(RecursionError) as cm:
331+
sys.setrecursionlimit(depth)
332+
self.assertRegex(str(cm.exception),
333+
"cannot set the recursion limit to [0-9]+ "
334+
"at the recursion depth [0-9]+: "
335+
"the limit is too low")
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

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)