File size: 6,552 Bytes
53b2ac9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | # SPDX-License-Identifier: MIT
"""Tests for tool_patch_file.
Standard unified diffs carry context lines. The old implementation ignored
them and removed lines counted from the hunk start, so a diff that changed
line 2 deleted line 1 instead and still reported success.
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
import trashclaw
@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
monkeypatch.setattr(trashclaw, "CWD", str(tmp_path))
monkeypatch.setattr(trashclaw, "UNDO_STACK", [])
return tmp_path
def _write(tmp_path, text, name="f.txt"):
p = tmp_path / name
p.write_text(text)
return p
def test_context_lines_are_honoured(tmp_path):
p = _write(tmp_path, "a\nb\nc\n")
patch = "--- a/f.txt\n+++ b/f.txt\n@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n"
result = trashclaw.tool_patch_file(str(p), patch)
assert result.startswith("Patched")
assert p.read_text() == "a\nB\nc\n"
def test_mismatched_hunk_is_rejected_and_file_untouched(tmp_path):
p = _write(tmp_path, "a\nb\nc\n")
result = trashclaw.tool_patch_file(str(p), "@@ -1,3 +1,3 @@\n a\n-x\n+X\n c\n")
assert result.startswith("Error")
assert "No changes written" in result
assert p.read_text() == "a\nb\nc\n"
assert trashclaw.UNDO_STACK == [] # nothing was changed, nothing to undo
def test_partial_failure_writes_nothing(tmp_path):
original = "".join(f"l{i}\n" for i in range(1, 11))
p = _write(tmp_path, original)
patch = ("@@ -1,2 +1,2 @@\n l1\n-l2\n+L2\n"
"@@ -8,2 +8,2 @@\n l8\n-nope\n+NOPE\n")
result = trashclaw.tool_patch_file(str(p), patch)
assert "hunk 2 of 2" in result
assert p.read_text() == original
def test_multiple_hunks_with_line_count_changes(tmp_path):
p = _write(tmp_path, "".join(f"l{i}\n" for i in range(1, 21)))
patch = ("@@ -1,3 +1,3 @@\n l1\n-l2\n+L2\n l3\n"
"@@ -9,3 +9,4 @@\n l9\n-l10\n+L10\n+new\n l11\n")
result = trashclaw.tool_patch_file(str(p), patch)
assert "2 hunks, 3 additions, 2 removals" in result
lines = p.read_text().splitlines()
assert lines[:3] == ["l1", "L2", "l3"]
assert lines[8:12] == ["l9", "L10", "new", "l11"]
assert len(lines) == 21
def test_drifted_line_numbers_still_apply(tmp_path):
p = _write(tmp_path, "x\ny\na\nb\nc\n")
result = trashclaw.tool_patch_file(str(p), "@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n")
assert result.startswith("Patched")
assert p.read_text() == "x\ny\na\nB\nc\n"
def test_blank_context_line_without_leading_space(tmp_path):
p = _write(tmp_path, "def f():\n\n return 1\n")
patch = "@@ -1,3 +1,3 @@\n def f():\n\n- return 1\n+ return 2\n"
result = trashclaw.tool_patch_file(str(p), patch)
assert result.startswith("Patched")
assert p.read_text() == "def f():\n\n return 2\n"
def test_pure_insertion_after_line(tmp_path):
p = _write(tmp_path, "a\nb\n")
result = trashclaw.tool_patch_file(str(p), "@@ -2,0 +3,1 @@\n+c\n")
assert result.startswith("Patched")
assert p.read_text() == "a\nb\nc\n"
def test_create_new_file(tmp_path):
p = tmp_path / "new.txt"
result = trashclaw.tool_patch_file(str(p), "--- /dev/null\n+++ b/new.txt\n@@ -0,0 +1,2 @@\n+x\n+y\n")
assert result.startswith("Patched")
assert p.read_text() == "x\ny\n"
def test_patch_without_hunks_is_an_error(tmp_path):
p = _write(tmp_path, "a\n")
result = trashclaw.tool_patch_file(str(p), "just some text")
assert result.startswith("Error")
assert p.read_text() == "a\n"
def test_successful_patch_is_undoable(tmp_path):
p = _write(tmp_path, "a\nb\n")
trashclaw.tool_patch_file(str(p), "@@ -1,2 +1,2 @@\n a\n-b\n+B\n")
assert trashclaw.UNDO_STACK[-1]["content"] == "a\nb\n"
assert trashclaw.UNDO_STACK[-1]["action"] == "patch"
# --- Review follow-ups: each case below was reported as "Patched" before. ---
def _assert_refused(p, result, original):
assert result.startswith("Error"), result
assert "No changes written" in result
assert p.read_text() == original
assert trashclaw.UNDO_STACK == []
def test_removed_and_added_lines_that_look_like_file_headers(tmp_path):
# "-- old" / "++ new" (Lua/SQL comments) show up in the diff as
# "--- old" / "+++ new"; the hunk counts say they are body lines.
p = _write(tmp_path, "a\nb\n-- x\nc\n")
result = trashclaw.tool_patch_file(str(p), "@@ -2,2 +2,2 @@\n-b\n+B\n--- x\n+++ y\n")
assert result.startswith("Patched"), result
assert p.read_text() == "a\nB\n++ y\nc\n"
def test_hunk_shorter_than_header_is_refused(tmp_path):
p = _write(tmp_path, "a\nb\nc\n")
result = trashclaw.tool_patch_file(str(p), "@@ -1,3 +1,3 @@\n a\n-b\n+B\n")
_assert_refused(p, result, "a\nb\nc\n")
assert "-3/+3" in result
def test_hunk_longer_than_header_is_refused(tmp_path):
p = _write(tmp_path, "a\nb\nc\n")
result = trashclaw.tool_patch_file(str(p), "@@ -1,2 +1,2 @@\n a\n-b\n+B\n c\n")
_assert_refused(p, result, "a\nb\nc\n")
def test_trailing_blank_context_line_is_verified(tmp_path):
p = _write(tmp_path, "a\nX\n")
result = trashclaw.tool_patch_file(str(p), "@@ -1,2 +1,2 @@\n-a\n+A\n \n")
_assert_refused(p, result, "a\nX\n")
q = _write(tmp_path, "a\n\n", name="g.txt")
assert trashclaw.tool_patch_file(str(q), "@@ -1,2 +1,2 @@\n-a\n+A\n \n").startswith("Patched")
assert q.read_text() == "A\n\n"
def test_drift_is_carried_into_later_insertion(tmp_path):
p = _write(tmp_path, "x\na\nb\n")
result = trashclaw.tool_patch_file(str(p), "@@ -1 +1 @@\n-a\n+A\n@@ -2,0 +3,1 @@\n+c\n")
assert result.startswith("Patched"), result
assert p.read_text() == "x\nA\nb\nc\n"
def test_out_of_range_insertion_is_refused(tmp_path):
p = _write(tmp_path, "a\nb\n")
result = trashclaw.tool_patch_file(str(p), "@@ -999,0 +1000,1 @@\n+z\n")
_assert_refused(p, result, "a\nb\n")
def test_insertion_after_last_line_without_newline(tmp_path):
p = _write(tmp_path, "a\nb")
result = trashclaw.tool_patch_file(str(p), "@@ -2,0 +3,1 @@\n+c\n")
assert result.startswith("Patched"), result
assert p.read_text() == "a\nb\nc\n"
def test_multi_file_diff_is_refused(tmp_path):
p = _write(tmp_path, "a\n")
patch = ("--- a/f.txt\n+++ b/f.txt\n@@ -1 +1 @@\n-a\n+A\n"
"--- a/g.txt\n+++ b/g.txt\n@@ -1 +1 @@\n-A\n+Z\n")
result = trashclaw.tool_patch_file(str(p), patch)
_assert_refused(p, result, "a\n")
assert "more than one file" in result
|