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().
This commit is contained in:
Victor Stinner 2023-09-06 17:34:31 +02:00 committed by GitHub
parent 2cd170db40
commit 8ff1142578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 176 additions and 40 deletions

View File

@ -112,6 +112,7 @@ def collect_sys(info_add):
call_func(info_add, 'sys.androidapilevel', sys, 'getandroidapilevel') call_func(info_add, 'sys.androidapilevel', sys, 'getandroidapilevel')
call_func(info_add, 'sys.windowsversion', sys, 'getwindowsversion') call_func(info_add, 'sys.windowsversion', sys, 'getwindowsversion')
call_func(info_add, 'sys.getrecursionlimit', sys, 'getrecursionlimit')
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()
if hasattr(sys, 'getfilesystemencodeerrors'): if hasattr(sys, 'getfilesystemencodeerrors'):

View File

@ -2241,6 +2241,39 @@ def check_disallow_instantiation(testcase, tp, *args, **kwds):
msg = f"cannot create '{re.escape(qualname)}' instances" msg = f"cannot create '{re.escape(qualname)}' instances"
testcase.assertRaisesRegex(TypeError, msg, tp, *args, **kwds) testcase.assertRaisesRegex(TypeError, msg, tp, *args, **kwds)
def get_recursion_depth():
"""Get the recursion depth of the caller function.
In the __main__ module, at the module level, it should be 1.
"""
try:
import _testinternalcapi
depth = _testinternalcapi.get_recursion_depth()
except (ImportError, RecursionError) as exc:
# sys._getframe() + frame.f_back implementation.
try:
depth = 0
frame = sys._getframe()
while frame is not None:
depth += 1
frame = frame.f_back
finally:
# Break any reference cycles.
frame = None
# Ignore get_recursion_depth() frame.
return max(depth - 1, 1)
def get_recursion_available():
"""Get the number of available frames before RecursionError.
It depends on the current recursion depth of the caller function and
sys.getrecursionlimit().
"""
limit = sys.getrecursionlimit()
depth = get_recursion_depth()
return limit - depth
@contextlib.contextmanager @contextlib.contextmanager
def set_recursion_limit(limit): def set_recursion_limit(limit):
"""Temporarily change the recursion limit.""" """Temporarily change the recursion limit."""
@ -2251,14 +2284,18 @@ def set_recursion_limit(limit):
finally: finally:
sys.setrecursionlimit(original_limit) sys.setrecursionlimit(original_limit)
def infinite_recursion(max_depth=75): def infinite_recursion(max_depth=100):
"""Set a lower limit for tests that interact with infinite recursions """Set a lower limit for tests that interact with infinite recursions
(e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some (e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some
debug windows builds, due to not enough functions being inlined the debug windows builds, due to not enough functions being inlined the
stack size might not handle the default recursion limit (1000). See stack size might not handle the default recursion limit (1000). See
bpo-11105 for details.""" bpo-11105 for details."""
return set_recursion_limit(max_depth) if max_depth < 3:
raise ValueError("max_depth must be at least 3, got {max_depth}")
depth = get_recursion_depth()
depth = max(depth - 1, 1) # Ignore infinite_recursion() frame.
limit = depth + max_depth
return set_recursion_limit(limit)
def ignore_deprecations_from(module: str, *, like: str) -> object: def ignore_deprecations_from(module: str, *, like: str) -> object:
token = object() token = object()

View File

@ -685,6 +685,83 @@ class TestSupport(unittest.TestCase):
else: else:
self.assertTrue(support.has_strftime_extensions) self.assertTrue(support.has_strftime_extensions)
def test_get_recursion_depth(self):
# test support.get_recursion_depth()
code = textwrap.dedent("""
from test import support
import sys
def check(cond):
if not cond:
raise AssertionError("test failed")
# depth 1
check(support.get_recursion_depth() == 1)
# depth 2
def test_func():
check(support.get_recursion_depth() == 2)
test_func()
def test_recursive(depth, limit):
if depth >= limit:
# cannot call get_recursion_depth() at this depth,
# it can raise RecursionError
return
get_depth = support.get_recursion_depth()
print(f"test_recursive: {depth}/{limit}: "
f"get_recursion_depth() says {get_depth}")
check(get_depth == depth)
test_recursive(depth + 1, limit)
# depth up to 25
with support.infinite_recursion(max_depth=25):
limit = sys.getrecursionlimit()
print(f"test with sys.getrecursionlimit()={limit}")
test_recursive(2, limit)
# depth up to 500
with support.infinite_recursion(max_depth=500):
limit = sys.getrecursionlimit()
print(f"test with sys.getrecursionlimit()={limit}")
test_recursive(2, limit)
""")
script_helper.assert_python_ok("-c", code)
def test_recursion(self):
# Test infinite_recursion() and get_recursion_available() functions.
def recursive_function(depth):
if depth:
recursive_function(depth - 1)
for max_depth in (5, 25, 250):
with support.infinite_recursion(max_depth):
available = support.get_recursion_available()
# Recursion up to 'available' additional frames should be OK.
recursive_function(available)
# Recursion up to 'available+1' additional frames must raise
# RecursionError. Avoid self.assertRaises(RecursionError) which
# can consume more than 3 frames and so raises RecursionError.
try:
recursive_function(available + 1)
except RecursionError:
pass
else:
self.fail("RecursionError was not raised")
# Test the bare minimumum: max_depth=3
with support.infinite_recursion(3):
try:
recursive_function(3)
except RecursionError:
pass
else:
self.fail("RecursionError was not raised")
#self.assertEqual(available, 2)
# XXX -follows a list of untested API # XXX -follows a list of untested API
# make_legacy_pyc # make_legacy_pyc
# is_resource_enabled # is_resource_enabled

View File

@ -279,20 +279,29 @@ class SysModuleTest(unittest.TestCase):
finally: finally:
sys.setswitchinterval(orig) sys.setswitchinterval(orig)
def test_recursionlimit(self): def test_getrecursionlimit(self):
limit = sys.getrecursionlimit()
self.assertIsInstance(limit, int)
self.assertGreater(limit, 1)
self.assertRaises(TypeError, sys.getrecursionlimit, 42) self.assertRaises(TypeError, sys.getrecursionlimit, 42)
oldlimit = sys.getrecursionlimit()
def test_setrecursionlimit(self):
old_limit = sys.getrecursionlimit()
try:
sys.setrecursionlimit(10_005)
self.assertEqual(sys.getrecursionlimit(), 10_005)
self.assertRaises(TypeError, sys.setrecursionlimit) self.assertRaises(TypeError, sys.setrecursionlimit)
self.assertRaises(ValueError, sys.setrecursionlimit, -42) self.assertRaises(ValueError, sys.setrecursionlimit, -42)
sys.setrecursionlimit(10000) finally:
self.assertEqual(sys.getrecursionlimit(), 10000) sys.setrecursionlimit(old_limit)
sys.setrecursionlimit(oldlimit)
def test_recursionlimit_recovery(self): def test_recursionlimit_recovery(self):
if hasattr(sys, 'gettrace') and sys.gettrace(): if hasattr(sys, 'gettrace') and sys.gettrace():
self.skipTest('fatal error if run with a trace function') self.skipTest('fatal error if run with a trace function')
oldlimit = sys.getrecursionlimit() old_limit = sys.getrecursionlimit()
def f(): def f():
f() f()
try: try:
@ -311,35 +320,31 @@ class SysModuleTest(unittest.TestCase):
with self.assertRaises(RecursionError): with self.assertRaises(RecursionError):
f() f()
finally: finally:
sys.setrecursionlimit(oldlimit) sys.setrecursionlimit(old_limit)
@test.support.cpython_only @test.support.cpython_only
def test_setrecursionlimit_recursion_depth(self): def test_setrecursionlimit_to_depth(self):
# Issue #25274: Setting a low recursion limit must be blocked if the # Issue #25274: Setting a low recursion limit must be blocked if the
# current recursion depth is already higher than limit. # current recursion depth is already higher than limit.
from _testinternalcapi import get_recursion_depth old_limit = sys.getrecursionlimit()
try:
depth = support.get_recursion_depth()
with self.subTest(limit=sys.getrecursionlimit(), depth=depth):
# depth + 1 is OK
sys.setrecursionlimit(depth + 1)
def set_recursion_limit_at_depth(depth, limit): # reset the limit to be able to call self.assertRaises()
recursion_depth = get_recursion_depth() # context manager
if recursion_depth >= depth: sys.setrecursionlimit(old_limit)
with self.assertRaises(RecursionError) as cm: with self.assertRaises(RecursionError) as cm:
sys.setrecursionlimit(limit) sys.setrecursionlimit(depth)
self.assertRegex(str(cm.exception), self.assertRegex(str(cm.exception),
"cannot set the recursion limit to [0-9]+ " "cannot set the recursion limit to [0-9]+ "
"at the recursion depth [0-9]+: " "at the recursion depth [0-9]+: "
"the limit is too low") "the limit is too low")
else:
set_recursion_limit_at_depth(depth, limit)
oldlimit = sys.getrecursionlimit()
try:
sys.setrecursionlimit(1000)
for limit in (10, 25, 50, 75, 100, 150, 200):
set_recursion_limit_at_depth(limit, limit)
finally: finally:
sys.setrecursionlimit(oldlimit) sys.setrecursionlimit(old_limit)
def test_getwindowsversion(self): def test_getwindowsversion(self):
# Raise SkipTest if sys doesn't have getwindowsversion attribute # Raise SkipTest if sys doesn't have getwindowsversion attribute

View File

@ -9,6 +9,7 @@ from pathlib import Path
import sys import sys
import tempfile import tempfile
import unittest import unittest
from test import support
from . import tomllib from . import tomllib
@ -92,13 +93,23 @@ class TestMiscellaneous(unittest.TestCase):
self.assertEqual(obj_copy, expected_obj) self.assertEqual(obj_copy, expected_obj)
def test_inline_array_recursion_limit(self): def test_inline_array_recursion_limit(self):
# 465 with default recursion limit with support.infinite_recursion(max_depth=100):
nest_count = int(sys.getrecursionlimit() * 0.465) available = support.get_recursion_available()
nest_count = (available // 2) - 2
# Add details if the test fails
with self.subTest(limit=sys.getrecursionlimit(),
available=available,
nest_count=nest_count):
recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]" recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]"
tomllib.loads(recursive_array_toml) tomllib.loads(recursive_array_toml)
def test_inline_table_recursion_limit(self): def test_inline_table_recursion_limit(self):
# 310 with default recursion limit with support.infinite_recursion(max_depth=100):
nest_count = int(sys.getrecursionlimit() * 0.31) available = support.get_recursion_available()
nest_count = (available // 3) - 1
# Add details if the test fails
with self.subTest(limit=sys.getrecursionlimit(),
available=available,
nest_count=nest_count):
recursive_table_toml = nest_count * "key = {" + nest_count * "}" recursive_table_toml = nest_count * "key = {" + nest_count * "}"
tomllib.loads(recursive_table_toml) tomllib.loads(recursive_table_toml)

View File

@ -0,0 +1,2 @@
Add ``get_recursion_available()`` and ``get_recursion_depth()`` functions to
the :mod:`test.support` module. Patch by Victor Stinner.

View File

@ -0,0 +1,3 @@
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. Patch by Victor Stinner.