codeflash-agent/packages/codeflash-api/tests/test_diff.py
Kevin Turcios 3a07579bb0 Raise codeflash-api test coverage from 81% to 92%
Add 182 new tests across optimize, V4A diff, CST utils, and postprocess
modules. Key coverage improvements:
- optimize/_pipeline.py: 29% → 97%
- optimize/_router.py: 40% → 93%
- diff/_v4a.py: 40% → 97%
- languages/python/_cst_utils.py: 67% → 96%
- languages/python/_postprocess.py: 67% → 87%

Also apply ruff format to 5 files that had formatting drift.
2026-04-22 23:39:54 -05:00

1169 lines
32 KiB
Python

from __future__ import annotations
import pytest
from codeflash_api.diff._base import DiffError, DiffMethod
from codeflash_api.diff._search_replace import (
SearchAndReplaceDiff,
SearchReplaceBlock,
apply_patches,
extract_patches,
find_with_whitespace_flexibility,
parse_diff,
)
from codeflash_api.diff._v4a import (
V4ADiff,
_norm,
apply_update,
extract_update_sections,
find_context,
find_context_core,
)
class TestDiffMethod:
"""Tests for DiffMethod enum."""
def test_values(self) -> None:
"""
All three diff methods exist.
"""
assert "no_diff" == DiffMethod.NO_DIFF.value
assert "v4a" == DiffMethod.V4A.value
assert "search_and_replace" == DiffMethod.SEARCH_AND_REPLACE.value
class TestSearchReplaceParseDiff:
"""Tests for parse_diff."""
def test_single_block(self) -> None:
"""
Single SEARCH/REPLACE block is parsed correctly.
"""
diff = "<<<<<<< SEARCH\nold code\n=======\nnew code\n>>>>>>> REPLACE\n"
blocks = parse_diff(diff)
assert 1 == len(blocks)
assert "old code" == blocks[0].search
assert "new code" == blocks[0].replace
def test_multiple_blocks(self) -> None:
"""
Multiple SEARCH/REPLACE blocks are all parsed.
"""
diff = (
"<<<<<<< SEARCH\nfoo\n=======\nbar\n>>>>>>> REPLACE\n"
"<<<<<<< SEARCH\nbaz\n=======\nqux\n>>>>>>> REPLACE\n"
)
blocks = parse_diff(diff)
assert 2 == len(blocks)
def test_empty_search(self) -> None:
"""
Empty search content (append mode) is valid.
"""
diff = "<<<<<<< SEARCH\n=======\nnew stuff\n>>>>>>> REPLACE\n"
blocks = parse_diff(diff)
assert "" == blocks[0].search
assert "new stuff" == blocks[0].replace
def test_empty_input_raises(self) -> None:
"""
Empty string raises ValueError.
"""
with pytest.raises(ValueError, match="Empty"):
parse_diff("")
def test_missing_delimiter_raises(self) -> None:
"""
Missing ======= marker raises ValueError.
"""
with pytest.raises(ValueError, match="======="):
parse_diff("<<<<<<< SEARCH\nfoo\n>>>>>>> REPLACE\n")
def test_missing_replace_marker_raises(self) -> None:
"""
Missing >>>>>>> REPLACE marker raises ValueError.
"""
with pytest.raises(ValueError, match="REPLACE"):
parse_diff("<<<<<<< SEARCH\nfoo\n=======\nbar\n")
class TestFindWithWhitespaceFlexibility:
"""Tests for find_with_whitespace_flexibility."""
def test_exact_match(self) -> None:
"""
Exact content is found.
"""
result = find_with_whitespace_flexibility(
"hello world", "prefix hello world suffix"
)
assert result is not None
assert 7 == result[0]
def test_flexible_whitespace(self) -> None:
"""
Tabs match spaces.
"""
result = find_with_whitespace_flexibility(
"hello world", "hello\tworld"
)
assert result is not None
def test_no_match(self) -> None:
"""
Non-existent content returns None.
"""
assert find_with_whitespace_flexibility("xyz", "abc") is None
class TestApplyPatches:
"""Tests for apply_patches."""
def test_simple_replace(self) -> None:
"""
Exact match is replaced.
"""
diff = "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE\n"
result = apply_patches(diff, "before old after")
assert "before new after" == result
def test_append_mode(self) -> None:
"""
Empty search appends to content.
"""
diff = "<<<<<<< SEARCH\n=======\n# appended\n>>>>>>> REPLACE\n"
result = apply_patches(diff, "existing")
assert "existing# appended" == result
def test_parse_error_returns_original(self) -> None:
"""
Invalid diff returns content unchanged.
"""
result = apply_patches("not a diff", "original")
assert "original" == result
class TestSearchAndReplaceDiff:
"""Tests for SearchAndReplaceDiff.run."""
def test_single_file_patch(self) -> None:
"""
Patch applied to matching file.
"""
content = (
"<replace_in_file>"
"<path>foo.py</path>"
"<diff>\n<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE\n</diff>"
"</replace_in_file>"
)
diff = SearchAndReplaceDiff(
content=content,
source_code={"foo.py": "old code"},
)
result = diff.run()
assert "new code" == result["foo.py"]
class TestNorm:
"""Tests for _norm."""
def test_strips_cr(self) -> None:
"""
Carriage returns are removed.
"""
assert "hello" == _norm("hello\r")
def test_no_cr(self) -> None:
"""
Lines without CR are unchanged.
"""
assert "hello" == _norm("hello")
class TestFindContextCore:
"""Tests for find_context_core."""
def test_exact_match(self) -> None:
"""
Exact match returns fuzz=0.
"""
lines = ["a", "b", "c", "d"]
idx, fuzz = find_context_core(lines, ["b", "c"], 0)
assert 1 == idx
assert 0 == fuzz
def test_rstrip_match(self) -> None:
"""
Trailing whitespace match returns fuzz=1.
"""
lines = ["a ", "b "]
idx, fuzz = find_context_core(lines, ["a", "b"], 0)
assert 0 == idx
assert 1 == fuzz
def test_strip_match(self) -> None:
"""
Leading+trailing whitespace match returns fuzz=100.
"""
lines = [" a ", " b "]
idx, fuzz = find_context_core(lines, ["a", "b"], 0)
assert 0 == idx
assert 100 == fuzz
def test_not_found(self) -> None:
"""
Missing context returns -1.
"""
idx, fuzz = find_context_core(["a", "b"], ["x", "y"], 0)
assert -1 == idx
class TestFindContext:
"""Tests for find_context with EOF handling."""
def test_eof_penalty(self) -> None:
"""
EOF match not at end gets +10000 fuzz penalty.
"""
lines = ["a", "b", "c", "d"]
idx, fuzz = find_context(lines, ["a", "b"], 0, eof=True)
assert 0 == idx
assert fuzz >= 10_000
class TestExtractUpdateSections:
"""Tests for extract_update_sections."""
def test_single_file(self) -> None:
"""
Single UPDATE section is extracted.
"""
content = (
"*** Begin Patch\n"
"*** Update File: foo.py\n"
" context\n"
"-old\n"
"+new\n"
"*** End Patch\n"
)
sections = extract_update_sections(content)
assert "foo.py" in sections
def test_no_patch_markers(self) -> None:
"""
Missing markers return empty dict.
"""
assert {} == extract_update_sections("no patch here")
class TestApplyUpdate:
"""Tests for apply_update."""
def test_simple_replacement(self) -> None:
"""
Single chunk replaces lines correctly.
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=1,
del_lines=["old_line"],
ins_lines=["new_line"],
)
],
)
result = apply_update("first\nold_line\nlast", action, "test.py")
assert "first\nnew_line\nlast\n" == result
def test_mismatch_raises(self) -> None:
"""
Deleted line mismatch raises DiffError.
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=0,
del_lines=["expected"],
ins_lines=["new"],
)
],
)
with pytest.raises(DiffError, match="mismatch"):
apply_update("actual", action, "test.py")
class TestV4ADiff:
"""Tests for V4ADiff.run."""
def test_no_changes(self) -> None:
"""
No matching sections returns original code.
"""
diff = V4ADiff(
content="*** Begin Patch\n*** End Patch\n",
source_code={"foo.py": "original"},
)
result = diff.run()
assert "original" == result["foo.py"]
class TestFindContextCoreNoMatch:
"""Tests for find_context_core when no match exists."""
def test_empty_lines(self) -> None:
"""
Empty lines list returns -1.
"""
idx, fuzz = find_context_core([], ["a"], 0)
assert -1 == idx
assert 0 == fuzz
def test_context_longer_than_lines(self) -> None:
"""
Context longer than the lines list returns -1.
"""
idx, fuzz = find_context_core(["a"], ["a", "b"], 0)
assert -1 == idx
def test_start_past_match_range(self) -> None:
"""
Start index beyond possible match range returns -1.
"""
lines = ["a", "b", "c"]
idx, fuzz = find_context_core(lines, ["a", "b"], 2)
assert -1 == idx
def test_exact_match_at_offset(self) -> None:
"""
Exact match found when start skips earlier occurrences.
"""
lines = ["a", "b", "a", "b"]
idx, fuzz = find_context_core(lines, ["a", "b"], 2)
assert 2 == idx
assert 0 == fuzz
class TestFindContextEOFAndNonEOF:
"""Tests for find_context EOF and non-EOF paths."""
def test_eof_match_at_end_no_penalty(self) -> None:
"""
EOF match at the actual end of file returns fuzz without penalty.
"""
lines = ["x", "y", "a", "b"]
idx, fuzz = find_context(lines, ["a", "b"], 0, eof=True)
assert 2 == idx
assert 0 == fuzz
def test_eof_no_match_returns_negative(self) -> None:
"""
EOF search with no match anywhere returns -1.
"""
lines = ["x", "y", "z"]
idx, fuzz = find_context(lines, ["a", "b"], 0, eof=True)
assert -1 == idx
assert 0 == fuzz
def test_non_eof_delegates_to_core(self) -> None:
"""
Non-EOF call delegates directly to find_context_core.
"""
lines = ["a", "b", "c"]
idx, fuzz = find_context(lines, ["a", "b"], 0, eof=False)
assert 0 == idx
assert 0 == fuzz
def test_non_eof_no_match(self) -> None:
"""
Non-EOF with no match returns -1.
"""
lines = ["x", "y"]
idx, fuzz = find_context(lines, ["a"], 0, eof=False)
assert -1 == idx
def test_eof_multiple_candidates_prefers_end(self) -> None:
"""
EOF search finds the match closest to the end of file.
"""
lines = ["a", "b", "c", "a", "b"]
idx, fuzz = find_context(lines, ["a", "b"], 0, eof=True)
assert 3 == idx
assert 0 == fuzz
class TestPeekNextSection:
"""Tests for peek_next_section state machine."""
def test_keep_lines(self) -> None:
"""
Lines starting with space are context (keep) lines.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" hello", " world"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert ["hello", "world"] == ctx
assert [] == chunks
assert 2 == next_i
assert is_eof is False
def test_delete_then_add(self) -> None:
"""
A delete line followed by an add line produces one chunk.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" ctx", "-old", "+new", " after"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 4 == next_i
assert 1 == len(chunks)
assert ["old"] == chunks[0].del_lines
assert ["new"] == chunks[0].ins_lines
def test_add_only(self) -> None:
"""
Add lines without preceding deletes produce an insertion chunk.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" before", "+inserted", " after"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 1 == len(chunks)
assert [] == chunks[0].del_lines
assert ["inserted"] == chunks[0].ins_lines
def test_delete_only(self) -> None:
"""
Delete lines without adds produce a deletion chunk.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" before", "-removed", " after"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 1 == len(chunks)
assert ["removed"] == chunks[0].del_lines
assert [] == chunks[0].ins_lines
def test_stops_at_scope_marker(self) -> None:
"""
Parsing stops when an @@ scope marker is encountered.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" context", "@@ next scope @@"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 1 == next_i
assert ["context"] == ctx
def test_stops_at_update_marker(self) -> None:
"""
Parsing stops when a *** marker (non-End of File) is encountered.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" context", "*** Update File: bar.py"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 1 == next_i
assert is_eof is False
def test_end_of_file_marker(self) -> None:
"""
'*** End of File' sets the is_eof flag and advances past it.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" tail", "*** End of File"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert is_eof is True
assert 2 == next_i
def test_invalid_line_raises(self) -> None:
"""
A line that doesn't start with space, +, -, @@, or *** raises DiffError.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = ["bad line with no prefix"]
with pytest.raises(DiffError, match="Invalid patch line"):
peek_next_section(lines, 0)
def test_empty_line_treated_as_context(self) -> None:
"""
An empty line (no prefix) is treated as an empty context line.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = ["", " after"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert ["", "after"] == ctx
assert 2 == next_i
def test_add_followed_by_delete_flushes_chunk(self) -> None:
"""
A delete line after add lines flushes the current chunk and starts a new one.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" ctx", "+added", "-removed", " end"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 2 == len(chunks)
assert ["added"] == chunks[0].ins_lines
assert [] == chunks[0].del_lines
assert ["removed"] == chunks[1].del_lines
def test_trailing_add_flushed_at_end(self) -> None:
"""
Pending add/delete lines at end of input are flushed into a chunk.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" ctx", "+trailing"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 1 == len(chunks)
assert ["trailing"] == chunks[0].ins_lines
def test_multiple_deletes_then_adds(self) -> None:
"""
Multiple delete lines followed by multiple add lines form one chunk.
"""
from codeflash_api.diff._v4a import peek_next_section
lines = [" ctx", "-del1", "-del2", "+add1", "+add2", " end"]
ctx, chunks, next_i, is_eof = peek_next_section(lines, 0)
assert 1 == len(chunks)
assert ["del1", "del2"] == chunks[0].del_lines
assert ["add1", "add2"] == chunks[0].ins_lines
class TestParseUpdateFileSections:
"""Tests for _parse_update_file_sections."""
def test_simple_section(self) -> None:
"""
A single context+change section is parsed into a PatchAction.
"""
from codeflash_api.diff._v4a import _parse_update_file_sections
patch_lines = [
" first",
"-old_line",
"+new_line",
" last",
]
file_content = "first\nold_line\nlast"
action, next_i, fuzz = _parse_update_file_sections(
patch_lines, 0, file_content
)
assert 1 == len(action.chunks)
assert 0 == fuzz
def test_scope_marker_navigation(self) -> None:
"""
@@ scope @@ markers navigate to the matching line in the file.
"""
from codeflash_api.diff._v4a import _parse_update_file_sections
patch_lines = [
"@@ def foo(): @@",
" def foo():",
"- old",
"+ new",
]
file_content = "import os\n\ndef foo():\n old\n rest"
action, next_i, fuzz = _parse_update_file_sections(
patch_lines, 0, file_content
)
assert 1 == len(action.chunks)
def test_scope_not_found_raises(self) -> None:
"""
A scope that doesn't exist in the file raises DiffError.
"""
from codeflash_api.diff._v4a import _parse_update_file_sections
patch_lines = [
"@@ def nonexistent(): @@",
" some context",
]
file_content = "def foo():\n pass"
with pytest.raises(DiffError, match="Could not find scope"):
_parse_update_file_sections(patch_lines, 0, file_content)
def test_context_not_found_raises(self) -> None:
"""
Context lines that don't match the file raise DiffError.
"""
from codeflash_api.diff._v4a import _parse_update_file_sections
patch_lines = [
" nonexistent context",
"-old",
"+new",
]
file_content = "completely different\ncontent here"
with pytest.raises(DiffError, match="Could not find context"):
_parse_update_file_sections(patch_lines, 0, file_content)
def test_skips_blank_lines_between_sections(self) -> None:
"""
Blank lines between sections are skipped.
"""
from codeflash_api.diff._v4a import _parse_update_file_sections
patch_lines = [
"",
" first",
"-old",
"+new",
]
file_content = "first\nold\nlast"
action, next_i, fuzz = _parse_update_file_sections(
patch_lines, 0, file_content
)
assert 1 == len(action.chunks)
def test_stops_at_next_file_marker(self) -> None:
"""
Parsing stops at *** Update File marker for the next file.
"""
from codeflash_api.diff._v4a import _parse_update_file_sections
patch_lines = [
" first",
"-old",
"+new",
"*** Update File: other.py",
]
file_content = "first\nold\nlast"
action, next_i, fuzz = _parse_update_file_sections(
patch_lines, 0, file_content
)
assert 3 == next_i
def test_chunks_without_context(self) -> None:
"""
Changes at the start of a section with no preceding context are offset
by current_file_idx.
"""
from codeflash_api.diff._v4a import _parse_update_file_sections
patch_lines = [
"+inserted_line",
]
file_content = "existing"
action, next_i, fuzz = _parse_update_file_sections(
patch_lines, 0, file_content
)
assert 1 == len(action.chunks)
assert 0 == action.chunks[0].orig_index
class TestParsePatchText:
"""Tests for parse_patch_text."""
def test_single_file_update(self) -> None:
"""
A patch updating one file is parsed correctly.
"""
from codeflash_api.diff._v4a import parse_patch_text
lines = [
"*** Update File: foo.py",
" hello",
"-old",
"+new",
" world",
]
files = {"foo.py": "hello\nold\nworld"}
patch = parse_patch_text(lines, files)
assert "foo.py" in patch.actions
assert 1 == len(patch.actions["foo.py"].chunks)
def test_multiple_files(self) -> None:
"""
A patch updating multiple files parses all of them.
"""
from codeflash_api.diff._v4a import parse_patch_text
lines = [
"*** Update File: a.py",
" line_a",
"-old_a",
"+new_a",
"*** Update File: b.py",
" line_b",
"-old_b",
"+new_b",
]
files = {"a.py": "line_a\nold_a", "b.py": "line_b\nold_b"}
patch = parse_patch_text(lines, files)
assert "a.py" in patch.actions
assert "b.py" in patch.actions
def test_file_not_found_raises(self) -> None:
"""
Referencing a file not in current_files raises DiffError.
"""
from codeflash_api.diff._v4a import parse_patch_text
lines = ["*** Update File: missing.py", " ctx"]
files = {"other.py": "content"}
with pytest.raises(DiffError, match="File not found"):
parse_patch_text(lines, files)
def test_unknown_line_raises(self) -> None:
"""
A line that isn't an Update File marker raises DiffError.
"""
from codeflash_api.diff._v4a import parse_patch_text
lines = ["garbage line"]
files = {"foo.py": "content"}
with pytest.raises(DiffError, match="Unknown or misplaced"):
parse_patch_text(lines, files)
def test_blank_lines_skipped(self) -> None:
"""
Leading blank lines before a file section are skipped.
"""
from codeflash_api.diff._v4a import parse_patch_text
lines = [
"",
"*** Update File: foo.py",
" hello",
"-old",
"+new",
]
files = {"foo.py": "hello\nold\nrest"}
patch = parse_patch_text(lines, files)
assert "foo.py" in patch.actions
def test_duplicate_file_merges_chunks(self) -> None:
"""
Multiple UPDATE sections for the same file merge their chunks.
"""
from codeflash_api.diff._v4a import parse_patch_text
lines = [
"*** Update File: foo.py",
" aaa",
"-bbb",
"+ccc",
"*** Update File: foo.py",
" ddd",
"-eee",
"+fff",
]
files = {"foo.py": "aaa\nbbb\nddd\neee"}
patch = parse_patch_text(lines, files)
assert 2 == len(patch.actions["foo.py"].chunks)
def test_empty_lines_only(self) -> None:
"""
Input with only blank lines produces a patch with no actions.
"""
from codeflash_api.diff._v4a import parse_patch_text
lines = ["", "", ""]
files = {"foo.py": "content"}
patch = parse_patch_text(lines, files)
assert {} == patch.actions
class TestExtractUpdateSectionsEdgeCases:
"""Tests for extract_update_sections edge cases."""
def test_multiple_files(self) -> None:
"""
Multiple UPDATE sections are all extracted.
"""
content = (
"*** Begin Patch\n"
"*** Update File: a.py\n"
" ctx_a\n"
"-old_a\n"
"+new_a\n"
"*** Update File: b.py\n"
" ctx_b\n"
"-old_b\n"
"+new_b\n"
"*** End Patch\n"
)
sections = extract_update_sections(content)
assert "a.py" in sections
assert "b.py" in sections
def test_empty_section_skipped(self) -> None:
"""
Empty section parts between split markers are skipped.
"""
content = (
"*** Begin Patch\n"
"*** Update File: \n"
"*** Update File: real.py\n"
" ctx\n"
"*** End Patch\n"
)
sections = extract_update_sections(content)
assert "real.py" in sections
def test_file_path_without_newline_skipped(self) -> None:
"""
A section with just a filename and no newline is skipped.
"""
content = (
"*** Begin Patch\n*** Update File: only_name\n*** End Patch\n"
)
sections = extract_update_sections(content)
assert {} == sections
def test_section_content_preserved(self) -> None:
"""
The diff content within a section is reconstructed with the
UPDATE prefix.
"""
content = (
"*** Begin Patch\n"
"*** Update File: test.py\n"
" context\n"
"-old\n"
"+new\n"
"*** End Patch\n"
)
sections = extract_update_sections(content)
assert sections["test.py"].startswith("*** Update File: test.py")
assert "-old" in sections["test.py"]
assert "+new" in sections["test.py"]
class TestApplyUpdateEdgeCases:
"""Tests for apply_update edge cases."""
def test_overlapping_chunks_raises(self) -> None:
"""
Chunks with overlapping line ranges raise DiffError.
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=0,
del_lines=["line1", "line2"],
ins_lines=["new1"],
),
Chunk(
orig_index=1,
del_lines=["line2"],
ins_lines=["new2"],
),
],
)
with pytest.raises(DiffError, match="Overlapping"):
apply_update("line1\nline2\nline3", action, "test.py")
def test_insertion_only_chunk(self) -> None:
"""
A chunk with no deletions only inserts lines.
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=1,
del_lines=[],
ins_lines=["inserted"],
)
],
)
result = apply_update("first\nsecond\nthird", action, "test.py")
assert "first\ninserted\nsecond\nthird\n" == result
def test_deletion_only_chunk(self) -> None:
"""
A chunk with no insertions removes lines.
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=1,
del_lines=["second"],
ins_lines=[],
)
],
)
result = apply_update("first\nsecond\nthird", action, "test.py")
assert "first\nthird\n" == result
def test_multiple_non_overlapping_chunks(self) -> None:
"""
Multiple sorted, non-overlapping chunks are all applied.
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=1,
del_lines=["b"],
ins_lines=["B"],
),
Chunk(
orig_index=3,
del_lines=["d"],
ins_lines=["D"],
),
],
)
result = apply_update("a\nb\nc\nd\ne", action, "test.py")
assert "a\nB\nc\nD\ne\n" == result
def test_empty_file_with_insertion(self) -> None:
"""
Inserting into an empty file works.
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=0,
del_lines=[],
ins_lines=["new_content"],
)
],
)
result = apply_update("", action, "test.py")
assert "new_content\n" == result
def test_whitespace_tolerant_delete_match(self) -> None:
"""
Deleted-line matching is whitespace-tolerant (strips both sides).
"""
from codeflash_api.diff._v4a import Chunk, PatchAction
action = PatchAction(
path="test.py",
chunks=[
Chunk(
orig_index=0,
del_lines=[" hello "],
ins_lines=["goodbye"],
)
],
)
result = apply_update(" hello", action, "test.py")
assert "goodbye\n" == result
def test_no_chunks_returns_original(self) -> None:
"""
A PatchAction with no chunks returns the original text with trailing newline.
"""
from codeflash_api.diff._v4a import PatchAction
action = PatchAction(path="test.py", chunks=[])
result = apply_update("hello\nworld", action, "test.py")
assert "hello\nworld\n" == result
class TestV4ADiffApply:
"""Tests for V4ADiff.run with actual patching."""
def test_single_file_patch(self) -> None:
"""
A complete V4A patch modifies matching file content.
"""
content = (
"*** Begin Patch\n"
"*** Update File: main.py\n"
" def foo():\n"
"- return 1\n"
"+ return 2\n"
"*** End Patch\n"
)
diff = V4ADiff(
content=content,
source_code={"main.py": "def foo():\n return 1\n"},
)
result = diff.run()
assert "def foo():\n return 2\n" == result["main.py"]
def test_unmatched_file_unchanged(self) -> None:
"""
Files not mentioned in the patch are returned unchanged.
"""
content = (
"*** Begin Patch\n"
"*** Update File: a.py\n"
" line\n"
"-old\n"
"+new\n"
"*** End Patch\n"
)
diff = V4ADiff(
content=content,
source_code={
"a.py": "line\nold\nrest",
"b.py": "untouched",
},
)
result = diff.run()
assert "untouched" == result["b.py"]
def test_multi_file_patch(self) -> None:
"""
A patch updating multiple files modifies all of them.
"""
content = (
"*** Begin Patch\n"
"*** Update File: x.py\n"
" alpha\n"
"-beta\n"
"+BETA\n"
"*** Update File: y.py\n"
" gamma\n"
"-delta\n"
"+DELTA\n"
"*** End Patch\n"
)
diff = V4ADiff(
content=content,
source_code={
"x.py": "alpha\nbeta\nepsilon",
"y.py": "gamma\ndelta\nzeta",
},
)
result = diff.run()
assert "BETA" in result["x.py"]
assert "DELTA" in result["y.py"]
def test_patch_no_actions_for_file(self) -> None:
"""
When a section is extracted but parses into no actions, the original
code is returned.
"""
content = (
"*** Begin Patch\n*** Update File: empty.py\n\n*** End Patch\n"
)
diff = V4ADiff(
content=content,
source_code={"empty.py": "original code"},
)
result = diff.run()
assert "original code" == result["empty.py"]
def test_multi_hunk_single_file(self) -> None:
"""
Multiple changes in different parts of the same file are all applied.
"""
content = (
"*** Begin Patch\n"
"*** Update File: code.py\n"
" aaa\n"
"-bbb\n"
"+BBB\n"
" ccc\n"
" ddd\n"
"-eee\n"
"+EEE\n"
"*** End Patch\n"
)
diff = V4ADiff(
content=content,
source_code={"code.py": "aaa\nbbb\nccc\nddd\neee\nfff"},
)
result = diff.run()
assert "aaa\nBBB\nccc\nddd\nEEE\nfff\n" == result["code.py"]