Repository navigation
fix(tools): report deeply nested progress snapshots as usage errors - #12602
Conversation
rooklz
left a comment
There was a problem hiding this comment.
First macOS run (arm64, head 3d92bfa50d applied to main 3a2b919816). The code change is correct, but the new test fails on Python 3.14 and later.
Results of python3 -m unittest test_progress
| Python | head | main progress.py + new test |
|---|---|---|
| 3.9.6 | new test OK (1 unrelated hardlink_to error, already on main) |
RED, 2 RecursionError |
| 3.12.13 / 3.13.13 | 49/49 OK | RED, 2 RecursionError |
| 3.14.8 / 3.15.1 | FAIL ×2: 'recursion' not found in '... base snapshot.json: snapshot must be an object' |
— |
Python 3.14 changed the C JSON decoder so it checks for real stack exhaustion instead of sys.getrecursionlimit(). On 3.14/3.15 here, json.loads("[" * 100000 + "]" * 100000) parses. The input then reaches validate_snapshot and is reported as "snapshot must be an object". That is already rc 2 with no traceback, so the tool behaves correctly, but the assertIn("recursion", ...) assertion fails. At 10^6 levels both versions raise RecursionError, with different messages: 3.13 says maximum recursion depth exceeded while decoding a JSON array… and 3.14 says Stack overflow (used 16352 kB) while decoding a JSON array…. A fixed depth or a fixed message therefore can't be portable. CI's Ubuntu python3 is 3.12, so CI won't show this. Contributors on Homebrew or a current python.org install will.
tools/tests/test_performance_compare.py:33 (excessive_json_value, #12266) already solves this by probing for a depth that actually raises. Proposed test-only diff:
--- a/tools/tests/test_progress.py
+++ b/tools/tests/test_progress.py
@@ -10,6 +10,17 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import progress
+def excessive_nesting():
+ depth = sys.getrecursionlimit() + 64
+ for _ in range(8):
+ try:
+ json.loads("[" * depth + "]" * depth)
+ except RecursionError:
+ return "[" * (depth + 128) + "]" * (depth + 128)
+ depth *= 2
+ raise AssertionError("JSON decoder accepted every bounded nesting probe")
+
+
class ProgressSnapshotTests(unittest.TestCase):
def test_library_counters_reject_invalid_types_and_negative_values(self):
for field in ("done", "todo"):
@@ -65,7 +76,7 @@ class ProgressSnapshotTests(unittest.TestCase):
paths = [Path(directory) / "base snapshot.json", Path(directory) / "head snapshot.json"]
for path in paths:
path.write_text(json.dumps(self.snapshot()), encoding="utf-8")
- paths[position].write_text("[" * 100000 + "]" * 100000, encoding="utf-8")
+ paths[position].write_text(excessive_nesting(), encoding="utf-8")
stdout, stderr = io.StringIO(), io.StringIO()
with patch("sys.stdout", stdout), patch("sys.stderr", stderr):
with self.assertRaises(SystemExit) as raised:
@@ -73,7 +84,8 @@ class ProgressSnapshotTests(unittest.TestCase):
self.assertEqual(raised.exception.code, 2)
self.assertEqual(stdout.getvalue(), "")
self.assertIn(str(paths[position]), stderr.getvalue())
- self.assertIn("recursion", stderr.getvalue())
+ self.assertNotIn("must be an object", stderr.getvalue())
+ self.assertNotIn("Traceback", stderr.getvalue())
def test_non_object_snapshots_are_usage_errors(self):
for snapshot in (None, [], "progress", 7, True):The "must be an object" check makes sure the input fails while decoding and not in validate_snapshot. Without it, a decoder that accepts the input would pass the test vacuously.
With the diff applied: ProgressSnapshotTests OK on 3.9.6, 3.12, 3.13, 3.14 and 3.15. Main progress.py + the fixed test is RED (2 errors) on 3.9.6, 3.13, 3.14 and 3.15. The drop-RecursionError mutant is killed on 3.14. The test takes 0.09 s. check_conventions --base upstream/main is clean.
AI-assisted review (Claude Code).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…thon Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
3d92bfa to
c76feb5
Compare
|
Windows run of this head (
AI-assisted: yes (Claude Code). |
THEMANnt
left a comment
There was a problem hiding this comment.
Reviewed c76feb5836f72811f8b3afb7f53e647ec2960882 against 3a2b91981651569163768854a1fcdc7193b6667b. Both progress snapshot inputs translate decoder recursion failures into path-specific CLI usage errors before comparison/report generation. No actionable finding in this scoped review.
Windows/Python 3.11 validation: head 49 passed, 0 skipped; baseline 48 passed, 0 skipped. The deep-snapshot controls raise RecursionError on old production and pass on the head. Input/report preservation is covered; no change to scanner counting or title behavior is claimed.
What
progress.py --comparereads the two snapshots withjson.loadsand reports an unreadable one as a usage error, but only forOSErrorandValueError. A snapshot nested deeper than the interpreter's recursion limit raisesRecursionError, which is neither, so the tool printed a traceback and exited with 1 instead of the usage error and exit code 2 that every other bad snapshot gets. This catchesRecursionErrorthere as well, the wayperformance_comparedoes after #12266, and adds a test that feeds a deeply nested file in either position.Tested
main2199ee0d47withRecursionErrorand passes with this change (head3d92bfa50d); all 47 tests oftools/tests/test_progress.pypass.main2199ee0d47and passes with this change; 47 of 47 pass.check_conventions --base upstream/mainpasses.Overlap
Four open PRs change the same files: #12492 and #10992 (both
tools/progress.pyandtools/tests/test_progress.py), #12588 (those two anddocs/dev/PROGRESS.md) and #12540 (tools/tests/test_progress.py). I took the changed files of each of the 1346 open PRs open at the first pass fromgit diff --name-onlyagainst its merge base, and of the 47 opened since from the pull request file lists. Agit merge-treeof each of the four against this branch is clean, and so is each of them alone againstmain319621ba2e, so none is a real pair and none needs aDepends on. No open or merged PR or issue deals withRecursionErrorinprogress.py; #12355 and #12501 changeprogress_history.py.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)AI-assisted (Claude Code).