Skip to content

Commit 61a977a

Browse files
committed
pythongh-108851: Fix tomllib recursion tests
* sys.setrecursionlimit() now allows setting the limit to the current recursion depth. * Add get_recursion_available() and get_recursion_depth() functions to the test.support module. * Fix test_tomllib recursion tests: reduce the limit to fix tests on WASI buildbots and compute the maximum nested array/dict depending on the current available recursion limit.
1 parent 03c4080 commit 61a977a

8 files changed

+177
-34
lines changed

Lib/test/support/__init__.py

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

2245+
def get_recursion_depth():
2246+
try:
2247+
import _testinternalcapi
2248+
except ImportError:
2249+
_testinternalcapi = None
2250+
2251+
if _testinternalcapi is not None:
2252+
depth = _testinternalcapi.get_recursion_depth()
2253+
else:
2254+
# Implementation using sys._getframe() and frame.f_back
2255+
try:
2256+
depth = 0
2257+
frame = sys._getframe()
2258+
while frame is not None:
2259+
depth += 1
2260+
frame = frame.f_back
2261+
finally:
2262+
# Break reference cycle
2263+
frame = None
2264+
2265+
# Ignore get_recursion_depth() frame
2266+
return max(depth - 1, 1)
2267+
2268+
def get_recursion_available():
2269+
limit = sys.getrecursionlimit()
2270+
depth = get_recursion_depth()
2271+
return limit - depth
2272+
22452273
@contextlib.contextmanager
22462274
def set_recursion_limit(limit):
22472275
"""Temporarily change the recursion limit."""
@@ -2252,14 +2280,16 @@ def set_recursion_limit(limit):
22522280
finally:
22532281
sys.setrecursionlimit(original_limit)
22542282

2255-
def infinite_recursion(max_depth=75):
2283+
def infinite_recursion(max_depth=100):
22562284
"""Set a lower limit for tests that interact with infinite recursions
22572285
(e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some
22582286
debug windows builds, due to not enough functions being inlined the
22592287
stack size might not handle the default recursion limit (1000). See
22602288
bpo-11105 for details."""
2261-
return set_recursion_limit(max_depth)
2262-
2289+
if max_depth < 1:
2290+
raise ValueError("max_depth must be at least 1")
2291+
limit = get_recursion_depth() + max_depth
2292+
return set_recursion_limit(limit)
22632293

22642294
def ignore_deprecations_from(module: str, *, like: str) -> object:
22652295
token = object()

Lib/test/test_support.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,64 @@ 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+
check(support.get_recursion_depth() == 1)
699+
700+
def test_func():
701+
check(support.get_recursion_depth() == 2)
702+
test_func()
703+
704+
def test_recursive(depth, limit):
705+
if depth >= limit:
706+
return
707+
get_depth = support.get_recursion_depth()
708+
print(f"test_recursive: {depth}/{limit}: get_recursion_depth() says {get_depth}")
709+
check(get_depth == depth)
710+
test_recursive(depth + 1, limit)
711+
712+
with support.infinite_recursion():
713+
limit = sys.getrecursionlimit()
714+
test_recursive(2, limit)
715+
""")
716+
script_helper.assert_python_ok("-c", code)
717+
718+
def test_recursion(self):
719+
# test infinite_recursion() and get_recursion_available() functions
720+
def recursive_function(depth):
721+
if depth:
722+
recursive_function(depth - 1)
723+
724+
# test also the bare minimum, max_depth=1: only allow one frame, one
725+
# function call!
726+
for max_depth in (1, 5, 25, 250):
727+
with support.infinite_recursion(max_depth):
728+
limit = sys.getrecursionlimit()
729+
available = support.get_recursion_available()
730+
try:
731+
recursive_function(available)
732+
# avoid self.assertRaises(RecursionError) which consumes
733+
# more than one frame and so raises RecursionError
734+
try:
735+
recursive_function(available + 1)
736+
except RecursionError:
737+
pass
738+
else:
739+
self.fail("RecursionError was not raised")
740+
except Exception as exc:
741+
# avoid self.subTest() since it consumes more
742+
# than one frame and so raises RecursionError!
743+
raise AssertionError(f"test failed with {max_depth=}, "
744+
f"{limit=}, {available=}") from exc
745+
688746
# XXX -follows a list of untested API
689747
# make_legacy_pyc
690748
# is_resource_enabled

Lib/test/test_sys.py

Lines changed: 59 additions & 22 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(10000)
293+
self.assertEqual(sys.getrecursionlimit(), 10000)
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,63 @@ 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_recursionerror(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:
330+
def set_recursion_limit_at_depth(desired_depth, limit):
331+
recursion_depth = support.get_recursion_depth()
332+
if recursion_depth < desired_depth:
333+
set_recursion_limit_at_depth(desired_depth, limit)
334+
else:
326335
with self.assertRaises(RecursionError) as cm:
327336
sys.setrecursionlimit(limit)
328337
self.assertRegex(str(cm.exception),
329338
"cannot set the recursion limit to [0-9]+ "
330339
"at the recursion depth [0-9]+: "
331340
"the limit is too low")
332-
else:
333-
set_recursion_limit_at_depth(depth, limit)
334341

335-
oldlimit = sys.getrecursionlimit()
342+
old_limit = sys.getrecursionlimit()
343+
try:
344+
with support.infinite_recursion(max_depth=500):
345+
for set_limit in (10, 25, 100, 250):
346+
# At depth limit+1, sys.setrecursionlimit(limit) must
347+
# raises RecursionError
348+
depth = set_limit + 1
349+
350+
# Add details if the test fails
351+
with self.subTest(limit=sys.getrecursionlimit(),
352+
available=support.get_recursion_depth(),
353+
set_limit=set_limit):
354+
set_recursion_limit_at_depth(depth, set_limit)
355+
finally:
356+
sys.setrecursionlimit(old_limit)
357+
358+
@test.support.cpython_only
359+
def test_setrecursionlimit_to_depth(self):
360+
def func():
361+
pass
362+
363+
old_limit = sys.getrecursionlimit()
336364
try:
337-
sys.setrecursionlimit(1000)
365+
depth = support.get_recursion_depth()
338366

339-
for limit in (10, 25, 50, 75, 100, 150, 200):
340-
set_recursion_limit_at_depth(limit, limit)
367+
# gh-108851: Corner case: set the limit to current recursion depth
368+
# should be permitted. Calling any Python function raises
369+
# RecursionError! Calling C functions is still ok, like the
370+
# sys.setrecursionlimit() call below.
371+
sys.setrecursionlimit(depth)
372+
try:
373+
func()
374+
except RecursionError:
375+
pass
376+
else:
377+
self.fail("RecursionError was not raised")
341378
finally:
342-
sys.setrecursionlimit(oldlimit)
379+
sys.setrecursionlimit(old_limit)
343380

344381
def test_getwindowsversion(self):
345382
# 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+
:func:`sys.setrecursionlimit` now allows setting the limit to the current
2+
recursion depth. Patch by Victor Stinner.
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: reduce the limit to fix tests on WASI
2+
buildbots and compute the maximum nested array/dict depending on the current
3+
available recursion limit. Patch by Victor Stinner.

Python/sysmodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,7 @@ sys_setrecursionlimit_impl(PyObject *module, int new_limit)
12711271
/* Reject too low new limit if the current recursion depth is higher than
12721272
the new low-water mark. */
12731273
int depth = tstate->py_recursion_limit - tstate->py_recursion_remaining;
1274-
if (depth >= new_limit) {
1274+
if (depth > new_limit) {
12751275
_PyErr_Format(tstate, PyExc_RecursionError,
12761276
"cannot set the recursion limit to %i at "
12771277
"the recursion depth %i: the limit is too low",

0 commit comments

Comments
 (0)