V4A patches with only + lines (no context or - lines) were silently dropped because search_lines was empty and the 'if search_lines:' block was the only code path. Addition-only hunks are common when the model generates patches for new functions or blocks. Adds an else branch that inserts at the context_hint position when available, or appends at end of file. Includes 2 regression tests for addition-only hunks with and without context hints. Salvaged from PR #3092 by thakoreh. Co-authored-by: Hiren <hiren.thakore58@gmail.com>
256 lines
6.8 KiB
Python
256 lines
6.8 KiB
Python
"""Tests for the V4A patch format parser."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from tools.patch_parser import (
|
|
OperationType,
|
|
apply_v4a_operations,
|
|
parse_v4a_patch,
|
|
)
|
|
|
|
|
|
class TestParseUpdateFile:
|
|
def test_basic_update(self):
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Update File: src/main.py
|
|
@@ def greet @@
|
|
def greet():
|
|
- print("hello")
|
|
+ print("hi")
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 1
|
|
|
|
op = ops[0]
|
|
assert op.operation == OperationType.UPDATE
|
|
assert op.file_path == "src/main.py"
|
|
assert len(op.hunks) == 1
|
|
|
|
hunk = op.hunks[0]
|
|
assert hunk.context_hint == "def greet"
|
|
prefixes = [l.prefix for l in hunk.lines]
|
|
assert " " in prefixes
|
|
assert "-" in prefixes
|
|
assert "+" in prefixes
|
|
|
|
def test_multiple_hunks(self):
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Update File: f.py
|
|
@@ first @@
|
|
a
|
|
-b
|
|
+c
|
|
@@ second @@
|
|
x
|
|
-y
|
|
+z
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 1
|
|
assert len(ops[0].hunks) == 2
|
|
assert ops[0].hunks[0].context_hint == "first"
|
|
assert ops[0].hunks[1].context_hint == "second"
|
|
|
|
|
|
class TestParseAddFile:
|
|
def test_add_file(self):
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Add File: new/module.py
|
|
+import os
|
|
+
|
|
+print("hello")
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 1
|
|
|
|
op = ops[0]
|
|
assert op.operation == OperationType.ADD
|
|
assert op.file_path == "new/module.py"
|
|
assert len(op.hunks) == 1
|
|
|
|
contents = [l.content for l in op.hunks[0].lines if l.prefix == "+"]
|
|
assert contents[0] == "import os"
|
|
assert contents[2] == 'print("hello")'
|
|
|
|
|
|
class TestParseDeleteFile:
|
|
def test_delete_file(self):
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Delete File: old/stuff.py
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 1
|
|
assert ops[0].operation == OperationType.DELETE
|
|
assert ops[0].file_path == "old/stuff.py"
|
|
|
|
|
|
class TestParseMoveFile:
|
|
def test_move_file(self):
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Move File: old/path.py -> new/path.py
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 1
|
|
assert ops[0].operation == OperationType.MOVE
|
|
assert ops[0].file_path == "old/path.py"
|
|
assert ops[0].new_path == "new/path.py"
|
|
|
|
|
|
class TestParseInvalidPatch:
|
|
def test_empty_patch_returns_empty_ops(self):
|
|
ops, err = parse_v4a_patch("")
|
|
assert err is None
|
|
assert ops == []
|
|
|
|
def test_no_begin_marker_still_parses(self):
|
|
patch = """\
|
|
*** Update File: f.py
|
|
line1
|
|
-old
|
|
+new
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 1
|
|
|
|
def test_multiple_operations(self):
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Add File: a.py
|
|
+content_a
|
|
*** Delete File: b.py
|
|
*** Update File: c.py
|
|
keep
|
|
-remove
|
|
+add
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 3
|
|
assert ops[0].operation == OperationType.ADD
|
|
assert ops[1].operation == OperationType.DELETE
|
|
assert ops[2].operation == OperationType.UPDATE
|
|
|
|
|
|
class TestApplyUpdate:
|
|
def test_preserves_non_prefix_pipe_characters_in_unmodified_lines(self):
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Update File: sample.py
|
|
@@ result @@
|
|
result = 1
|
|
- return result
|
|
+ return result + 1
|
|
*** End Patch"""
|
|
operations, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
|
|
class FakeFileOps:
|
|
def __init__(self):
|
|
self.written = None
|
|
|
|
def read_file(self, path, offset=1, limit=500):
|
|
return SimpleNamespace(
|
|
content=(
|
|
'def run():\n'
|
|
' cmd = "echo a | sed s/a/b/"\n'
|
|
' result = 1\n'
|
|
' return result'
|
|
),
|
|
error=None,
|
|
)
|
|
|
|
def write_file(self, path, content):
|
|
self.written = content
|
|
return SimpleNamespace(error=None)
|
|
|
|
file_ops = FakeFileOps()
|
|
|
|
result = apply_v4a_operations(operations, file_ops)
|
|
|
|
assert result.success is True
|
|
assert file_ops.written == (
|
|
'def run():\n'
|
|
' cmd = "echo a | sed s/a/b/"\n'
|
|
' result = 1\n'
|
|
' return result + 1'
|
|
)
|
|
|
|
|
|
class TestAdditionOnlyHunks:
|
|
"""Regression tests for #3081 — addition-only hunks were silently dropped."""
|
|
|
|
def test_addition_only_hunk_with_context_hint(self):
|
|
"""A hunk with only + lines should insert at the context hint location."""
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Update File: src/app.py
|
|
@@ def main @@
|
|
+def helper():
|
|
+ return 42
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
assert len(ops) == 1
|
|
assert len(ops[0].hunks) == 1
|
|
|
|
hunk = ops[0].hunks[0]
|
|
# All lines should be additions
|
|
assert all(l.prefix == '+' for l in hunk.lines)
|
|
|
|
# Apply to a file that contains the context hint
|
|
class FakeFileOps:
|
|
written = None
|
|
def read_file(self, path, **kw):
|
|
return SimpleNamespace(
|
|
content="def main():\n pass\n",
|
|
error=None,
|
|
)
|
|
def write_file(self, path, content):
|
|
self.written = content
|
|
return SimpleNamespace(error=None)
|
|
|
|
file_ops = FakeFileOps()
|
|
result = apply_v4a_operations(ops, file_ops)
|
|
assert result.success is True
|
|
assert "def helper():" in file_ops.written
|
|
assert "return 42" in file_ops.written
|
|
|
|
def test_addition_only_hunk_without_context_hint(self):
|
|
"""A hunk with only + lines and no context hint appends at end of file."""
|
|
patch = """\
|
|
*** Begin Patch
|
|
*** Update File: src/app.py
|
|
+def new_func():
|
|
+ return True
|
|
*** End Patch"""
|
|
ops, err = parse_v4a_patch(patch)
|
|
assert err is None
|
|
|
|
class FakeFileOps:
|
|
written = None
|
|
def read_file(self, path, **kw):
|
|
return SimpleNamespace(
|
|
content="existing = True\n",
|
|
error=None,
|
|
)
|
|
def write_file(self, path, content):
|
|
self.written = content
|
|
return SimpleNamespace(error=None)
|
|
|
|
file_ops = FakeFileOps()
|
|
result = apply_v4a_operations(ops, file_ops)
|
|
assert result.success is True
|
|
assert file_ops.written.endswith("def new_func():\n return True\n")
|
|
assert "existing = True" in file_ops.written
|