]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-108851: Fix tomllib recursion tests (#108853)
authorVictor Stinner <vstinner@python.org>
Wed, 6 Sep 2023 15:34:31 +0000 (17:34 +0200)
committerGitHub <noreply@github.com>
Wed, 6 Sep 2023 15:34:31 +0000 (17:34 +0200)
* 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().

Lib/test/pythoninfo.py
Lib/test/support/__init__.py
Lib/test/test_support.py
Lib/test/test_sys.py
Lib/test/test_tomllib/test_misc.py
Misc/NEWS.d/next/Tests/2023-09-03-21-18-35.gh-issue-108851.CCuHyI.rst [new file with mode: 0644]
Misc/NEWS.d/next/Tests/2023-09-03-21-41-10.gh-issue-108851.xFTYOE.rst [new file with mode: 0644]

index 53af21db0755b19e0bf0afd2a7b62754d42da0db..46522b50dd1e985a1eecdf3a786287e35ddd8614 100644 (file)
@@ -112,6 +112,7 @@ def collect_sys(info_add):
 
     call_func(info_add, 'sys.androidapilevel', sys, 'getandroidapilevel')
     call_func(info_add, 'sys.windowsversion', sys, 'getwindowsversion')
+    call_func(info_add, 'sys.getrecursionlimit', sys, 'getrecursionlimit')
 
     encoding = sys.getfilesystemencoding()
     if hasattr(sys, 'getfilesystemencodeerrors'):
index 7bac1160fd8e0a02ccae945748cfa5fd5db05d43..d39f529499ca50999090825287b5f5f02a43d4be 100644 (file)
@@ -2241,6 +2241,39 @@ def check_disallow_instantiation(testcase, tp, *args, **kwds):
     msg = f"cannot create '{re.escape(qualname)}' instances"
     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
 def set_recursion_limit(limit):
     """Temporarily change the recursion limit."""
@@ -2251,14 +2284,18 @@ def set_recursion_limit(limit):
     finally:
         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
     (e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some
     debug windows builds, due to not enough functions being inlined the
     stack size might not handle the default recursion limit (1000). See
     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:
     token = object()
index 86d26b7e8df4d0246d5b6596636499786467baa0..64280739f009467422b03e8509d18f79c2a690e7 100644 (file)
@@ -685,6 +685,83 @@ class TestSupport(unittest.TestCase):
         else:
             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
     # make_legacy_pyc
     # is_resource_enabled
index e8a99244a3a28de5c9e8acfc04c9c412c8566c41..e4a341bd3e3db83ccbb9e650244467f89e1348be 100644 (file)
@@ -279,20 +279,29 @@ class SysModuleTest(unittest.TestCase):
         finally:
             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)
-        oldlimit = sys.getrecursionlimit()
-        self.assertRaises(TypeError, sys.setrecursionlimit)
-        self.assertRaises(ValueError, sys.setrecursionlimit, -42)
-        sys.setrecursionlimit(10000)
-        self.assertEqual(sys.getrecursionlimit(), 10000)
-        sys.setrecursionlimit(oldlimit)
+
+    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(ValueError, sys.setrecursionlimit, -42)
+        finally:
+            sys.setrecursionlimit(old_limit)
 
     def test_recursionlimit_recovery(self):
         if hasattr(sys, 'gettrace') and sys.gettrace():
             self.skipTest('fatal error if run with a trace function')
 
-        oldlimit = sys.getrecursionlimit()
+        old_limit = sys.getrecursionlimit()
         def f():
             f()
         try:
@@ -311,35 +320,31 @@ class SysModuleTest(unittest.TestCase):
                 with self.assertRaises(RecursionError):
                     f()
         finally:
-            sys.setrecursionlimit(oldlimit)
+            sys.setrecursionlimit(old_limit)
 
     @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
         # current recursion depth is already higher than limit.
 
-        from _testinternalcapi import get_recursion_depth
-
-        def set_recursion_limit_at_depth(depth, limit):
-            recursion_depth = get_recursion_depth()
-            if recursion_depth >= depth:
-                with self.assertRaises(RecursionError) as cm:
-                    sys.setrecursionlimit(limit)
-                self.assertRegex(str(cm.exception),
-                                 "cannot set the recursion limit to [0-9]+ "
-                                 "at the recursion depth [0-9]+: "
-                                 "the limit is too low")
-            else:
-                set_recursion_limit_at_depth(depth, limit)
-
-        oldlimit = sys.getrecursionlimit()
+        old_limit = sys.getrecursionlimit()
         try:
-            sys.setrecursionlimit(1000)
-
-            for limit in (10, 25, 50, 75, 100, 150, 200):
-                set_recursion_limit_at_depth(limit, limit)
+            depth = support.get_recursion_depth()
+            with self.subTest(limit=sys.getrecursionlimit(), depth=depth):
+                # depth + 1 is OK
+                sys.setrecursionlimit(depth + 1)
+
+                # reset the limit to be able to call self.assertRaises()
+                # context manager
+                sys.setrecursionlimit(old_limit)
+                with self.assertRaises(RecursionError) as cm:
+                    sys.setrecursionlimit(depth)
+            self.assertRegex(str(cm.exception),
+                             "cannot set the recursion limit to [0-9]+ "
+                             "at the recursion depth [0-9]+: "
+                             "the limit is too low")
         finally:
-            sys.setrecursionlimit(oldlimit)
+            sys.setrecursionlimit(old_limit)
 
     def test_getwindowsversion(self):
         # Raise SkipTest if sys doesn't have getwindowsversion attribute
index a477a219fd9ebd68e80c491295a78671a6d97df3..9e677a337a2835f459311e9743265dc39c5029d0 100644 (file)
@@ -9,6 +9,7 @@ from pathlib import Path
 import sys
 import tempfile
 import unittest
+from test import support
 
 from . import tomllib
 
@@ -92,13 +93,23 @@ class TestMiscellaneous(unittest.TestCase):
         self.assertEqual(obj_copy, expected_obj)
 
     def test_inline_array_recursion_limit(self):
-        # 465 with default recursion limit
-        nest_count = int(sys.getrecursionlimit() * 0.465)
-        recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]"
-        tomllib.loads(recursive_array_toml)
+        with support.infinite_recursion(max_depth=100):
+            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 * "]"
+                tomllib.loads(recursive_array_toml)
 
     def test_inline_table_recursion_limit(self):
-        # 310 with default recursion limit
-        nest_count = int(sys.getrecursionlimit() * 0.31)
-        recursive_table_toml = nest_count * "key = {" + nest_count * "}"
-        tomllib.loads(recursive_table_toml)
+        with support.infinite_recursion(max_depth=100):
+            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 * "}"
+                tomllib.loads(recursive_table_toml)
diff --git a/Misc/NEWS.d/next/Tests/2023-09-03-21-18-35.gh-issue-108851.CCuHyI.rst b/Misc/NEWS.d/next/Tests/2023-09-03-21-18-35.gh-issue-108851.CCuHyI.rst
new file mode 100644 (file)
index 0000000..7a5b305
--- /dev/null
@@ -0,0 +1,2 @@
+Add ``get_recursion_available()`` and ``get_recursion_depth()`` functions to
+the :mod:`test.support` module. Patch by Victor Stinner.
diff --git a/Misc/NEWS.d/next/Tests/2023-09-03-21-41-10.gh-issue-108851.xFTYOE.rst b/Misc/NEWS.d/next/Tests/2023-09-03-21-41-10.gh-issue-108851.xFTYOE.rst
new file mode 100644 (file)
index 0000000..b35aaeb
--- /dev/null
@@ -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.