Files

598 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for trim_diff_hunk and _parse_hunk_header.
Reference: app/src/code_review/comments/diff_hunk_parser_tests.rs
"""
import sys
import os
import unittest
sys.path.insert(0, os.path.dirname(__file__))
from trim_diff_hunk import trim_diff_hunk, _parse_hunk_header, _prepare_lines, _annotate_hunk_body, line_in_hunk, last_reachable_line
# ---------------------------------------------------------------------------
# Helper to build large hunks for testing
# ---------------------------------------------------------------------------
def _make_context_hunk(old_start, new_start, count):
"""Build a pure-context hunk with ``count`` lines."""
header = f"@@ -{old_start},{count} +{new_start},{count} @@"
body = [f" line {old_start + i}" for i in range(count)]
return "\n".join([header] + body)
# ---------------------------------------------------------------------------
# _prepare_lines
# ---------------------------------------------------------------------------
class TestPrepareLines(unittest.TestCase):
def test_splits_and_strips_trailing_empties(self):
assert _prepare_lines("a\nb\nc\n\n") == ["a", "b", "c"]
def test_no_trailing_empties(self):
assert _prepare_lines("a\nb") == ["a", "b"]
def test_single_line(self):
assert _prepare_lines("@@ -1,1 +1,1 @@") == ["@@ -1,1 +1,1 @@"]
def test_empty_string(self):
assert _prepare_lines("") == []
# ---------------------------------------------------------------------------
# _parse_hunk_header
# ---------------------------------------------------------------------------
class TestParseHunkHeader(unittest.TestCase):
def test_standard_header(self):
assert _parse_hunk_header("@@ -10,5 +20,7 @@") == (10, 5, 20, 7, "")
def test_header_with_context_text(self):
result = _parse_hunk_header("@@ -10,5 +20,7 @@ fn main()")
assert result == (10, 5, 20, 7, " fn main()")
def test_omitted_counts_default_to_one(self):
assert _parse_hunk_header("@@ -10 +20 @@") == (10, 1, 20, 1, "")
def test_invalid_headers(self):
assert _parse_hunk_header("not a header") is None
assert _parse_hunk_header("@@ invalid @@") is None
assert _parse_hunk_header("") is None
def test_only_old_count_omitted(self):
assert _parse_hunk_header("@@ -10 +20,3 @@") == (10, 1, 20, 3, "")
def test_only_new_count_omitted(self):
assert _parse_hunk_header("@@ -10,3 +20 @@") == (10, 3, 20, 1, "")
# ---------------------------------------------------------------------------
# trim_diff_hunk basic / passthrough cases
# ---------------------------------------------------------------------------
class TestTrimPassthrough(unittest.TestCase):
"""Cases where trim_diff_hunk should return the input unchanged."""
def test_empty_string(self):
assert trim_diff_hunk("", 1) == ""
def test_none_input(self):
assert trim_diff_hunk(None, 1) is None
def test_small_hunk_unchanged(self):
hunk = "@@ -1,2 +1,3 @@\n first line\n+added line\n last line"
assert trim_diff_hunk(hunk, 2, context_lines=3) == hunk
def test_invalid_header_unchanged(self):
hunk = "not a valid header\n+line1\n+line2\n+line3\n+line4\n+line5"
assert trim_diff_hunk(hunk, 1) == hunk
def test_target_not_found_unchanged(self):
hunk = _make_context_hunk(1, 1, 20)
assert trim_diff_hunk(hunk, 999) == hunk
# ---------------------------------------------------------------------------
# trim_diff_hunk trimming behaviour
# ---------------------------------------------------------------------------
class TestTrimBehaviour(unittest.TestCase):
def test_trims_to_context_window(self):
"""Large hunk trimmed to ±3 lines around target."""
hunk = _make_context_hunk(1, 1, 20)
result = trim_diff_hunk(hunk, 10, context_lines=3)
result_lines = result.split("\n")
# Target ± 3 → lines 7-13 = 7 body lines + header
assert result_lines[0] == "@@ -7,7 +7,7 @@"
assert " line 7" in result
assert " line 10" in result
assert " line 13" in result
assert " line 6" not in result
assert " line 14" not in result
def test_target_at_beginning(self):
hunk = _make_context_hunk(1, 1, 20)
result = trim_diff_hunk(hunk, 1, context_lines=3)
result_lines = result.split("\n")
# Can't go before line 1 → lo=0, hi=3
assert result_lines[0] == "@@ -1,4 +1,4 @@"
assert " line 1" in result
assert " line 4" in result
assert " line 5" not in result
def test_target_at_end(self):
hunk = _make_context_hunk(1, 1, 20)
result = trim_diff_hunk(hunk, 20, context_lines=3)
result_lines = result.split("\n")
assert result_lines[0] == "@@ -17,4 +17,4 @@"
assert " line 17" in result
assert " line 20" in result
assert " line 16" not in result
def test_preserves_whitespace(self):
"""Mirrors Rust test_parse_preserves_whitespace."""
lines = ["@@ -1,15 +1,16 @@"]
for i in range(1, 8):
lines.append(f" line {i}")
lines.append("+ heavily indented") # new line 8
for i in range(8, 16):
lines.append(f" line {i}")
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 8, side="RIGHT")
assert "+ heavily indented" in result
def test_preserves_header_context_text(self):
lines = ["@@ -1,20 +1,20 @@ fn example()"]
for i in range(1, 21):
lines.append(f" line {i}")
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 10)
assert result.split("\n")[0].endswith(" fn example()")
def test_trailing_empty_lines_stripped(self):
hunk = _make_context_hunk(1, 1, 20) + "\n\n\n"
result = trim_diff_hunk(hunk, 10)
assert not result.endswith("\n")
# ---------------------------------------------------------------------------
# trim_diff_hunk LEFT / RIGHT side targeting
# ---------------------------------------------------------------------------
class TestSideTargeting(unittest.TestCase):
def test_right_side_skips_deletions(self):
"""RIGHT side tracks new-file line numbers; deletions are invisible."""
lines = ["@@ -1,12 +1,14 @@"]
for i in range(1, 4):
lines.append(f" line {i}") # old 1-3, new 1-3
lines.append("-deleted A") # old 4
lines.append("-deleted B") # old 5
lines.append("+added A") # new 4
lines.append("+added B") # new 5
lines.append("+added C") # new 6
lines.append("+added D") # new 7
for i in range(6, 14):
lines.append(f" line {i}") # old 6-13, new 8-15
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 6, side="RIGHT")
assert "+added C" in result
def test_left_side_skips_additions(self):
"""LEFT side tracks old-file line numbers; additions are invisible."""
lines = ["@@ -10,15 +10,16 @@"]
for i in range(15):
if i == 7:
lines.append("+added line") # new-only, no old num
lines.append(f" context {i}")
hunk = "\n".join(lines)
# old line 15 = old_start(10) + 5 context lines → " context 5"
result = trim_diff_hunk(hunk, 15, side="LEFT")
assert " context 5" in result
def test_left_targets_deletion(self):
"""Targeting a deleted line by old-file number."""
lines = ["@@ -1,12 +1,10 @@"]
for i in range(1, 5):
lines.append(f" line {i}")
lines.append("-removed A") # old 5
lines.append("-removed B") # old 6
for i in range(5, 13):
lines.append(f" line {i}")
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 5, side="LEFT")
assert "-removed A" in result
# ---------------------------------------------------------------------------
# trim_diff_hunk multi-line comment ranges
# ---------------------------------------------------------------------------
class TestMultilineRange(unittest.TestCase):
def test_keeps_full_range_plus_context(self):
hunk = _make_context_hunk(1, 1, 20)
# start_line=8, target=12, context=3 → window is [8-3, 12+3] = [5, 15]
result = trim_diff_hunk(hunk, 12, start_line=8, context_lines=3)
result_lines = result.split("\n")
assert result_lines[0] == "@@ -5,11 +5,11 @@"
assert " line 5" in result
assert " line 8" in result
assert " line 12" in result
assert " line 15" in result
assert " line 4" not in result
assert " line 16" not in result
def test_range_start_at_hunk_boundary(self):
hunk = _make_context_hunk(1, 1, 20)
# start_line=1, context=3 → lo clamped to 0
result = trim_diff_hunk(hunk, 5, start_line=1, context_lines=3)
assert " line 1" in result
assert " line 8" in result
# ---------------------------------------------------------------------------
# trim_diff_hunk pure additions / pure deletions
# ---------------------------------------------------------------------------
class TestPureAdditionsAndDeletions(unittest.TestCase):
def test_only_additions(self):
"""Old-file count should be 0 in trimmed header."""
lines = ["@@ -5,0 +5,15 @@"]
for i in range(5, 20):
lines.append(f"+new line {i}")
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 10, side="RIGHT")
header = result.split("\n")[0]
# All trimmed lines are additions → old count = 0
assert ",0 +" in header
assert "+new line 10" in result
def test_only_deletions(self):
"""New-file count should be 0 in trimmed header."""
lines = ["@@ -5,15 +5,0 @@"]
for i in range(5, 20):
lines.append(f"-old line {i}")
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 10, side="LEFT")
header = result.split("\n")[0]
assert "+5,0 @@" in header
assert "-old line 10" in result
# ---------------------------------------------------------------------------
# trim_diff_hunk special markers
# ---------------------------------------------------------------------------
class TestSpecialMarkers(unittest.TestCase):
def test_no_newline_marker_does_not_shift_line_numbers(self):
r"""'\ No newline at end of file' must not affect line counting."""
lines = ["@@ -1,15 +1,16 @@"]
for i in range(1, 8):
lines.append(f" line {i}")
lines.append("+added line") # new line 8
lines.append("\\ No newline at end of file")
for i in range(8, 16):
lines.append(f" line {i}")
hunk = "\n".join(lines)
# Use context=3 so the marker is within the trim window
result = trim_diff_hunk(hunk, 8, side="RIGHT", context_lines=3)
assert "+added line" in result
def test_no_newline_marker_excluded_from_counts(self):
r"""The marker should not inflate old/new counts in the header."""
lines = ["@@ -1,15 +1,16 @@"]
for i in range(1, 8):
lines.append(f" line {i}")
lines.append("+added line") # new line 8
lines.append("\\ No newline at end of file")
for i in range(8, 16):
lines.append(f" line {i}")
hunk = "\n".join(lines)
# Use context=3 so the marker is within the trim window
result = trim_diff_hunk(hunk, 8, side="RIGHT", context_lines=3)
header = result.split("\n")[0]
parsed = _parse_hunk_header(header)
old_count, new_count = parsed[1], parsed[3]
# Count body lines manually to verify
body = result.split("\n")[1:]
expected_old = sum(
1 for l in body if l and l[0] not in ("+", "\\")
)
expected_new = sum(
1 for l in body if l and l[0] not in ("-", "\\")
)
assert old_count == expected_old
assert new_count == expected_new
# ---------------------------------------------------------------------------
# trim_diff_hunk zero context (default)
# ---------------------------------------------------------------------------
class TestZeroContext(unittest.TestCase):
"""With context_lines=0, only the exact commented line(s) are kept."""
def test_isolates_single_context_line(self):
hunk = _make_context_hunk(1, 1, 20)
result = trim_diff_hunk(hunk, 10)
result_lines = result.split("\n")
assert result_lines[0] == "@@ -10,1 +10,1 @@"
assert len(result_lines) == 2
assert " line 10" in result
def test_isolates_addition(self):
lines = ["@@ -1,5 +1,6 @@"]
for i in range(1, 4):
lines.append(f" line {i}")
lines.append("+added line") # new line 4
for i in range(4, 6):
lines.append(f" line {i}")
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 4, side="RIGHT")
result_lines = result.split("\n")
assert len(result_lines) == 2
assert "+added line" in result
header = _parse_hunk_header(result_lines[0])
assert header[1] == 0 # old count = 0
assert header[3] == 1 # new count = 1
def test_isolates_deletion(self):
lines = ["@@ -1,6 +1,5 @@"]
for i in range(1, 4):
lines.append(f" line {i}")
lines.append("-deleted line") # old line 4
for i in range(5, 7):
lines.append(f" line {i}")
hunk = "\n".join(lines)
result = trim_diff_hunk(hunk, 4, side="LEFT")
result_lines = result.split("\n")
assert len(result_lines) == 2
assert "-deleted line" in result
header = _parse_hunk_header(result_lines[0])
assert header[1] == 1 # old count = 1
assert header[3] == 0 # new count = 0
def test_multiline_range_no_context(self):
hunk = _make_context_hunk(1, 1, 20)
result = trim_diff_hunk(hunk, 12, start_line=8)
result_lines = result.split("\n")
assert result_lines[0] == "@@ -8,5 +8,5 @@"
assert " line 8" in result
assert " line 12" in result
assert " line 7" not in result
assert " line 13" not in result
# ---------------------------------------------------------------------------
# line_in_hunk
# ---------------------------------------------------------------------------
class TestLineInHunk(unittest.TestCase):
def test_target_within_range(self):
hunk = _make_context_hunk(10, 20, 5)
assert line_in_hunk(hunk, 22, side="RIGHT") is True
def test_target_within_range_left(self):
hunk = _make_context_hunk(10, 20, 5)
assert line_in_hunk(hunk, 12, side="LEFT") is True
def test_target_past_end(self):
hunk = _make_context_hunk(10, 20, 5) # new 20-24
assert line_in_hunk(hunk, 30, side="RIGHT") is False
def test_target_before_start(self):
hunk = _make_context_hunk(10, 20, 5) # new 20-24
assert line_in_hunk(hunk, 5, side="RIGHT") is False
def test_right_target_in_deletion_only_hunk(self):
"""A hunk with only deletions has no reachable RIGHT lines."""
hunk = "@@ -5,3 +5,0 @@\n-del A\n-del B\n-del C"
assert line_in_hunk(hunk, 5, side="RIGHT") is False
def test_empty_hunk(self):
assert line_in_hunk("", 1) is False
def test_none_hunk(self):
assert line_in_hunk(None, 1) is False
def test_target_past_hunk_end(self):
"""Comment 2892844016: target 11078, hunk new 10816-10822."""
hunk = (
"@@ -10808,9 +10816,7 @@ impl Workspace {\n"
" comment,\n"
" diff_mode,\n"
" } => {\n"
"- if !pane_group.as_ref(ctx).right_panel_open {\n"
"- self.open_code_review_panel_from_arg(open_code_review, pane_group.clone(), ctx);\n"
"- }\n"
"+ self.open_code_review_panel_from_arg(open_code_review, pane_group.clone(), ctx);"
)
assert line_in_hunk(hunk, 11078, side="RIGHT") is False
def test_target_before_hunk_start(self):
"""Comment 2898341466: target 2899, hunk new 3066-3082."""
hunk = (
"@@ -2926,6 +3066,17 @@ fn render_response_footer(props: Props, app: &AppContext) -> Option<Box<dyn Elem\n"
" }\n"
" }\n"
" \n"
"+ // Bulk-import review comments button, shown on the latest exchange when the conversation\n"
"+ // has any imported review comments.\n"
"+ if props.conversation_has_imported_comments && !props.shared_session_status.is_viewer() {"
)
assert line_in_hunk(hunk, 2899, side="RIGHT") is False
def test_truncated_hunk(self):
"""Comment 2954102612: header claims +4770,109 but body has only 5 lines.
Simulates a truncated hunk where the target (4918) is past the body.
"""
lines = ["@@ -4769,6 +4770,109 @@ impl AIBlock {"]
for i in range(4770, 4775):
lines.append(f"+ line {i}")
hunk = "\n".join(lines)
assert line_in_hunk(hunk, 4918, side="RIGHT") is False
# ---------------------------------------------------------------------------
# last_reachable_line
# ---------------------------------------------------------------------------
class TestLastReachableLine(unittest.TestCase):
def test_context_hunk(self):
hunk = _make_context_hunk(10, 20, 5) # new 20-24
assert last_reachable_line(hunk, side="RIGHT") == 24
def test_addition_hunk(self):
hunk = "@@ -5,0 +5,3 @@\n+new A\n+new B\n+new C"
assert last_reachable_line(hunk, side="RIGHT") == 7
def test_deletion_only_right(self):
hunk = "@@ -5,3 +5,0 @@\n-del A\n-del B\n-del C"
assert last_reachable_line(hunk, side="RIGHT") is None
def test_deletion_only_left(self):
hunk = "@@ -5,3 +5,0 @@\n-del A\n-del B\n-del C"
assert last_reachable_line(hunk, side="LEFT") == 7
def test_empty_hunk(self):
assert last_reachable_line("", side="RIGHT") is None
def test_none_hunk(self):
assert last_reachable_line(None, side="RIGHT") is None
# ---------------------------------------------------------------------------
# trim_diff_hunk multi-hunk inputs
# ---------------------------------------------------------------------------
class TestMultiHunk(unittest.TestCase):
"""trim_diff_hunk should find and trim the correct sub-hunk."""
_MULTI = (
"@@ -1,3 +1,4 @@ fn foo()\n"
" ctx1\n"
"+added_early\n"
" ctx2\n"
" ctx3\n"
"@@ -100,3 +101,4 @@ fn bar()\n"
" ctx100\n"
"+added_late\n"
" ctx101\n"
" ctx102"
)
def test_target_in_second_hunk(self):
"""Target 102 (new-file line in second sub-hunk) is found and trimmed."""
result = trim_diff_hunk(self._MULTI, 102, side="RIGHT")
assert "+added_late" in result
# The result should NOT contain the first hunk's content.
assert "+added_early" not in result
header = _parse_hunk_header(result.split("\n")[0])
assert header is not None
# new_start should be from the second hunk's range (101+)
assert header[2] == 102 # trimmed to just line 102
def test_target_in_first_hunk(self):
result = trim_diff_hunk(self._MULTI, 2, side="RIGHT")
assert "+added_early" in result
assert "+added_late" not in result
def test_target_in_no_hunk(self):
result = trim_diff_hunk(self._MULTI, 9999, side="RIGHT")
assert result == self._MULTI
def test_multi_hunk_line_in_hunk(self):
"""line_in_hunk should search across all sub-hunks."""
assert line_in_hunk(self._MULTI, 102, side="RIGHT") is True
assert line_in_hunk(self._MULTI, 2, side="RIGHT") is True
assert line_in_hunk(self._MULTI, 9999, side="RIGHT") is False
# ---------------------------------------------------------------------------
# Edge case: markdown list items in pure-addition hunks
# ---------------------------------------------------------------------------
class TestMarkdownListItem(unittest.TestCase):
r"""Hunks from where commented lines are markdown list items.
The line `+- \`specs/<issue-number>/TECH.md\`` starts with `+-`. The `-`
is a markdown list marker, NOT a diff deletion prefix.
"""
_HUNK = (
"@@ -0,0 +1,116 @@\n"
"+---\n"
"+name: write-tech-spec\n"
"+description: desc\n"
"+---\n"
"+\n"
"+# write-tech-spec\n"
"+\n"
"+Write a spec.\n"
"+\n"
"+## Overview\n"
"+\n"
"+The tech spec overview.\n"
"+\n"
"+Write specs into source control under:\n"
"+\n"
"+- `specs/<issue-number>/TECH.md`"
)
def test_annotate_hunk_body_classifies_plus_dash_as_addition(self):
"""_annotate_hunk_body must treat `+-` lines as additions."""
body = _prepare_lines(self._HUNK)[1:] # skip header
annotated = _annotate_hunk_body(body, 0, 1)
# Line 16 (new-file) should be the markdown list item.
target_entry = annotated[15] # 0-indexed
old_num, new_num, text = target_entry
assert old_num is None, f"Expected no old-file line number, got {old_num}"
assert new_num == 16, f"Expected new-file line 16, got {new_num}"
assert text == "+- `specs/<issue-number>/TECH.md`"
def test_trim_preserves_plus_dash_line(self):
"""trim_diff_hunk must preserve the full `+-` line text."""
result = trim_diff_hunk(self._HUNK, 16, side="RIGHT")
assert "+- `specs/<issue-number>/TECH.md`" in result
def test_trim_header_counts_plus_dash_as_new(self):
"""Trimmed header must count the `+-` line as a new-file line."""
result = trim_diff_hunk(self._HUNK, 16, side="RIGHT")
header = _parse_hunk_header(result.split("\n")[0])
assert header is not None
old_count, new_count = header[1], header[3]
# A single addition line: old_count=0, new_count=1
assert old_count == 0, f"Expected old_count=0, got {old_count}"
assert new_count == 1, f"Expected new_count=1, got {new_count}"
def test_line_in_hunk_finds_plus_dash_line(self):
"""line_in_hunk must locate line 16 on the RIGHT side."""
assert line_in_hunk(self._HUNK, 16, side="RIGHT") is True
def test_last_reachable_line_includes_plus_dash(self):
"""last_reachable_line must reach line 16 on the RIGHT side."""
assert last_reachable_line(self._HUNK, side="RIGHT") == 16
if __name__ == "__main__":
unittest.main()