Skip to content

Add option to generate test operations in diffs - #248

Merged
stefankoegl merged 2 commits into
masterfrom
claude/funny-lamport-kge2fr
Nov 2, 2026
Merged

stefankoegl merged 2 commits into
masterfrom
claude/funny-lamport-kge2fr

Conversation

@stefankoegl

Copy link
Copy Markdown
Owner

Supersedes #154 by @RobertCochran, which no longer applied to master. The commit credits them as co-author.

What it does

make_patch, JsonPatch.from_diff and DiffBuilder take a new keyword argument, generate_test_ops (default False). When it is set, a test operation comes before each remove and replace and checks the value about to be removed or replaced. The patch then records the previous values, and applying it to a document that differs there fails with JsonPatchTestFailed.

>>> jsonpatch.make_patch({'a': 1, 'b': 2}, {'a': 3}, generate_test_ops=True).patch
[{'op': 'test', 'path': '/b', 'value': 2}, {'op': 'remove', 'path': '/b'},
 {'op': 'test', 'path': '/a', 'value': 1}, {'op': 'replace', 'path': '/a', 'value': 3}]
  • jsondiff gets a matching -t/--test-ops option.
  • When the diff turns a remove into a move, that removal gets no test. With optimization=False, every remove is tested.
  • A remove followed by an add at the same array index still becomes one replace, with one test in front of it.

Differences from #154

  • Rewritten for the new diff engine. The diff engine has been rewritten since Add option to generate test operations in diff #154 was opened. Its code (_on_undo_*, TestOperation objects in the linked list) no longer exists, so this PR implements the feature again. Test operations use the same locations as other operations and get their array indices when the operations are replayed. Their paths therefore stay correct when earlier operations shift items.
  • Existing callers are unaffected. The new argument is added at the end of each signature. Add option to generate test operations in diff #154 dropped optimization from from_diff and put the new argument before pointer_cls in make_patch, which broke callers passing those by position.
  • Explicit link between a remove and its test. When a remove becomes a move, its test is found through that link. Add option to generate test operations in diff #154 instead checked whether the previous node in the list was a test.

Tests

  • GenerateTestOpsTests in tests.py: checks exact outputs, including array indices, moves, optimization=False, a replace at the document root, and failing on changed values.
  • CLI: a test for jsondiff --test-ops.
  • Hypothesis property: with the flag, the patch equals the plain patch with a test at the same path before each remove/replace, and it still turns src into dst. It is checked with and without optimization.
  • Docs: a new example in doc/tutorial.rst runs as a doctest, and doc/commandline.rst shows the new option.

python tests.py and pytest pass.


Generated by Claude Code

make_patch, JsonPatch.from_diff and DiffBuilder take a new keyword
argument generate_test_ops (default False). When set, each remove and
replace operation of the generated patch is preceded by a test
operation that checks the value it removes or replaces. The patch then
records the previous values and fails with JsonPatchTestFailed when
applied to a document that differs there. Removals that the diff turns
into moves are not tested.

jsondiff gets a matching -t/--test-ops option.

Based on #154 by Robert Cochran, reimplemented on top of the current
diff engine, which resolves array indices when replaying operations, so
the test operations get the same indices as the operations they guard.
The new argument is added after the existing ones, so positional
callers (including of `optimization`) are unaffected.

Co-Authored-By: Robert Cochran <robert.cochran@peachfinance.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PewzpidDeSzMcCa4Krt54E
@coveralls

coveralls commented Nov 2, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 100.0%. remained the same — claude/funny-lamport-kge2fr into master

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The generated test value aliases mutable source containers, allowing later source mutations to alter the integrity check; it needs a deep copy to preserve the original snapshot.

1 open issue
What changed in this PR

Adds optional test operations to generated JSON patches, allowing callers to check previous values before removing or replacing them while preserving default behavior.

Changes:

  • Adds generate_test_ops to the diff APIs, with handling for moves and shifting array indices.
  • Exposes the feature through jsondiff -t/--test-ops and documents its usage.
  • Adds unit, CLI, and property-based coverage.
File Description
tests.py Tests guarded diffs, array indices, moves, and CLI options.
property_tests.py Checks operation ordering and round trips with test generation enabled.
jsonpatch.py Implements optional test generation in the diff engine and public APIs.
jsonpatch_cli.py Adds and forwards the --test-ops option.
doc/​tutorial.rst Demonstrates guarded patches and failures on changed values.
doc/​commandline.rst Documents the new CLI option.

Review effort: Balanced

Comment thread jsonpatch.py Resolved
The test operations referred to the values in src, so changing src after
diffing changed what the patch tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PewzpidDeSzMcCa4Krt54E

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The review identifies no active issues requiring changes to the optional JSON Patch value-check feature.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@stefankoegl
stefankoegl merged commit 6a6d1a7 into master Nov 2, 2026
12 checks passed
@stefankoegl stefankoegl mentioned this pull request Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants