// diff_test.go -- internal/diff 包的单元测试. // // 覆盖场景: // - Myers diff 基础:单行替换,多行插入,多行删除 // - 多 hunk:文件头尾各有一处变更 // - 边界:空文件 vs 非空,两个完全不同的文件 // - Unicode:中文内容的 diff // - 大文件:10000+ 行的降级策略 // - 上下文:contextLines=0/1/3/5 不同效果 // - FormatUnified 格式正确性 package diff import ( "fmt" "strings" "testing" ) // ───────────────────────────────────────────────────────────────────── // Myers Diff 基础测试 // ───────────────────────────────────────────────────────────────────── // TestDiff_SingleLineReplace 测试单行替换 func TestDiff_SingleLineReplace(t *testing.T) { old := []string{"hello"} new := []string{"goodbye"} edits := Diff(old, new) // 应该有一个删除和一个插入 deletes := countEditType(edits, EditDelete) inserts := countEditType(edits, EditInsert) if deletes != 1 { t.Errorf("期望 1 个删除,实际 %d", deletes) } if inserts != 1 { t.Errorf("期望 1 个插入,实际 %d", inserts) } // 验证内容 for _, e := range edits { if e.Type == EditDelete && e.Content != "hello" { t.Errorf("删除行内容应为 'hello',实际 %q", e.Content) } if e.Type == EditInsert && e.Content != "goodbye" { t.Errorf("插入行内容应为 'goodbye',实际 %q", e.Content) } } } // TestDiff_MultiLineInsert 测试多行插入 func TestDiff_MultiLineInsert(t *testing.T) { old := []string{"a", "b"} new := []string{"a", "x", "y", "z", "b"} edits := Diff(old, new) inserts := countEditType(edits, EditInsert) if inserts != 3 { t.Errorf("期望 3 个插入,实际 %d", inserts) } // 不应有删除 deletes := countEditType(edits, EditDelete) if deletes != 0 { t.Errorf("不应有删除操作,实际 %d 个", deletes) } // 相同行应有 2 个(a 和 b) equals := countEditType(edits, EditEqual) if equals != 2 { t.Errorf("期望 2 个相同行,实际 %d", equals) } } // TestDiff_MultiLineDelete 测试多行删除 func TestDiff_MultiLineDelete(t *testing.T) { old := []string{"a", "x", "y", "z", "b"} new := []string{"a", "b"} edits := Diff(old, new) deletes := countEditType(edits, EditDelete) if deletes != 3 { t.Errorf("期望 3 个删除,实际 %d", deletes) } inserts := countEditType(edits, EditInsert) if inserts != 0 { t.Errorf("不应有插入操作,实际 %d 个", inserts) } } // TestDiff_IdenticalContent 测试完全相同的内容 func TestDiff_IdenticalContent(t *testing.T) { lines := []string{"a", "b", "c"} edits := Diff(lines, lines) for _, e := range edits { if e.Type != EditEqual { t.Errorf("完全相同的内容不应有非 Equal 操作: %+v", e) } } } // TestDiff_LineNumbers 测试行号正确性 func TestDiff_LineNumbers(t *testing.T) { old := []string{"a", "b", "c"} new := []string{"a", "B", "c"} edits := Diff(old, new) for _, e := range edits { switch e.Type { case EditEqual: if e.OldLine == 0 || e.NewLine == 0 { t.Errorf("Equal 行的行号不应为 0: %+v", e) } case EditDelete: if e.OldLine == 0 { t.Errorf("Delete 行的 OldLine 不应为 0: %+v", e) } case EditInsert: if e.NewLine == 0 { t.Errorf("Insert 行的 NewLine 不应为 0: %+v", e) } } } } // ───────────────────────────────────────────────────────────────────── // 边界条件测试 // ───────────────────────────────────────────────────────────────────── // TestDiff_EmptyToNonEmpty 测试空文件到非空 func TestDiff_EmptyToNonEmpty(t *testing.T) { old := []string{} new := []string{"a", "b", "c"} edits := Diff(old, new) if len(edits) != 3 { t.Fatalf("期望 3 个编辑操作,实际 %d", len(edits)) } for _, e := range edits { if e.Type != EditInsert { t.Errorf("空到非空应全部是插入: %+v", e) } } } // TestDiff_NonEmptyToEmpty 测试非空到空文件 func TestDiff_NonEmptyToEmpty(t *testing.T) { old := []string{"a", "b", "c"} new := []string{} edits := Diff(old, new) if len(edits) != 3 { t.Fatalf("期望 3 个编辑操作,实际 %d", len(edits)) } for _, e := range edits { if e.Type != EditDelete { t.Errorf("非空到空应全部是删除: %+v", e) } } } // TestDiff_BothEmpty 测试两个空文件 func TestDiff_BothEmpty(t *testing.T) { edits := Diff([]string{}, []string{}) if len(edits) != 0 { t.Errorf("两个空文件不应产生编辑操作,实际 %d 个", len(edits)) } } // TestDiff_CompletelyDifferent 测试两个完全不同的文件 func TestDiff_CompletelyDifferent(t *testing.T) { old := []string{"a", "b", "c"} new := []string{"x", "y", "z"} edits := Diff(old, new) deletes := countEditType(edits, EditDelete) inserts := countEditType(edits, EditInsert) if deletes != 3 { t.Errorf("完全不同应有 3 个删除,实际 %d", deletes) } if inserts != 3 { t.Errorf("完全不同应有 3 个插入,实际 %d", inserts) } } // ───────────────────────────────────────────────────────────────────── // Unicode 测试 // ───────────────────────────────────────────────────────────────────── // TestDiff_ChineseContent 测试中文内容的 diff func TestDiff_ChineseContent(t *testing.T) { old := []string{"你好世界", "这是第二行", "这是第三行"} new := []string{"你好世界", "这是修改后的第二行", "这是第三行"} edits := Diff(old, new) deletes := countEditType(edits, EditDelete) inserts := countEditType(edits, EditInsert) equals := countEditType(edits, EditEqual) if deletes != 1 { t.Errorf("期望 1 个删除,实际 %d", deletes) } if inserts != 1 { t.Errorf("期望 1 个插入,实际 %d", inserts) } if equals != 2 { t.Errorf("期望 2 个相同行,实际 %d", equals) } // 验证删除和插入的内容 for _, e := range edits { if e.Type == EditDelete && e.Content != "这是第二行" { t.Errorf("删除行应为 '这是第二行',实际 %q", e.Content) } if e.Type == EditInsert && e.Content != "这是修改后的第二行" { t.Errorf("插入行应为 '这是修改后的第二行',实际 %q", e.Content) } } } // ───────────────────────────────────────────────────────────────────── // 大文件降级策略测试 // ───────────────────────────────────────────────────────────────────── // TestDiff_LargeFileFallback 测试大文件降级策略 func TestDiff_LargeFileFallback(t *testing.T) { // 创建一个超过阈值的文件 size := largeDiffThreshold/2 + 1 old := make([]string, size) new := make([]string, size) for i := 0; i < size; i++ { old[i] = fmt.Sprintf("line %d", i) new[i] = fmt.Sprintf("line %d", i) } // 修改中间的一行 midIdx := size / 2 new[midIdx] = "MODIFIED LINE" edits := Diff(old, new) // 应该能正确检测到变更 hasDelete := false hasInsert := false for _, e := range edits { if e.Type == EditDelete && e.Content == fmt.Sprintf("line %d", midIdx) { hasDelete = true } if e.Type == EditInsert && e.Content == "MODIFIED LINE" { hasInsert = true } } if !hasDelete { t.Error("降级策略应检测到删除行") } if !hasInsert { t.Error("降级策略应检测到插入行") } // 验证公共前缀被保留为 Equal equalsBeforeMid := 0 for _, e := range edits { if e.Type == EditEqual { equalsBeforeMid++ } else { break } } if equalsBeforeMid != midIdx { t.Errorf("降级策略应保留 %d 行公共前缀,实际 %d", midIdx, equalsBeforeMid) } } // TestDiff_LargeFileBothEndsCommon 测试大文件首尾公共部分被保留 func TestDiff_LargeFileBothEndsCommon(t *testing.T) { size := largeDiffThreshold/2 + 1 old := make([]string, size) new := make([]string, size+2) // 新文件多两行 // 首尾相同,中间不同 commonPrefix := 100 commonSuffix := 100 for i := 0; i < commonPrefix; i++ { old[i] = fmt.Sprintf("prefix %d", i) new[i] = fmt.Sprintf("prefix %d", i) } for i := commonPrefix; i < size-commonSuffix; i++ { old[i] = fmt.Sprintf("old middle %d", i) } for i := commonPrefix; i < size+2-commonSuffix; i++ { new[i] = fmt.Sprintf("new middle %d", i) } for i := 0; i < commonSuffix; i++ { old[size-commonSuffix+i] = fmt.Sprintf("suffix %d", i) new[size+2-commonSuffix+i] = fmt.Sprintf("suffix %d", i) } edits := Diff(old, new) // 验证公共前缀 for i := 0; i < commonPrefix && i < len(edits); i++ { if edits[i].Type != EditEqual { t.Errorf("前缀第 %d 行应为 Equal,实际 %v", i, edits[i].Type) break } } // 验证公共后缀(从末尾检查) suffixEdits := edits[len(edits)-commonSuffix:] for i, e := range suffixEdits { if e.Type != EditEqual { t.Errorf("后缀第 %d 行应为 Equal,实际 %v", i, e.Type) break } } } // ───────────────────────────────────────────────────────────────────── // StructuredPatch 测试 // ───────────────────────────────────────────────────────────────────── // TestStructuredPatch_SingleChange 测试单处变更 func TestStructuredPatch_SingleChange(t *testing.T) { old := "a\nb\nc\nd\ne" new := "a\nb\nC\nd\ne" hunks := StructuredPatch(old, new, 1) if len(hunks) != 1 { t.Fatalf("期望 1 个 hunk,实际 %d", len(hunks)) } h := hunks[0] if h.OldStart != 2 || h.NewStart != 2 { t.Errorf("hunk 起始行号不正确: old=%d new=%d", h.OldStart, h.NewStart) } // 验证包含上下文,删除和插入行 hasContext := false hasDelete := false hasInsert := false for _, line := range h.Lines { if strings.HasPrefix(line, " ") { hasContext = true } if strings.HasPrefix(line, "-") { hasDelete = true } if strings.HasPrefix(line, "+") { hasInsert = true } } if !hasContext { t.Error("hunk 应包含上下文行") } if !hasDelete { t.Error("hunk 应包含删除行") } if !hasInsert { t.Error("hunk 应包含插入行") } } // TestStructuredPatch_MultiHunk 测试多 hunk(文件头尾各有变更) func TestStructuredPatch_MultiHunk(t *testing.T) { // 构造 20 行文件,第 2 行和第 19 行有变更 oldLines := make([]string, 20) newLines := make([]string, 20) for i := 0; i < 20; i++ { oldLines[i] = fmt.Sprintf("line%d", i+1) newLines[i] = fmt.Sprintf("line%d", i+1) } oldLines[1] = "old_line2" newLines[1] = "new_line2" oldLines[18] = "old_line19" newLines[18] = "new_line19" old := strings.Join(oldLines, "\n") new := strings.Join(newLines, "\n") hunks := StructuredPatch(old, new, 3) if len(hunks) != 2 { t.Errorf("期望 2 个 hunk(头尾各一个),实际 %d", len(hunks)) for i, h := range hunks { t.Logf(" hunk[%d]: OldStart=%d OldLines=%d NewStart=%d NewLines=%d", i, h.OldStart, h.OldLines, h.NewStart, h.NewLines) } } } // TestStructuredPatch_MergedHunks 测试变更区域间距小于 2*contextLines 时合并 func TestStructuredPatch_MergedHunks(t *testing.T) { // 5 行文件,第 2 行和第 4 行有变更,contextLines=3 // 间距 1 行 < 2*3=6,应合并为一个 hunk old := "a\nb\nc\nd\ne" new := "a\nB\nc\nD\ne" hunks := StructuredPatch(old, new, 3) if len(hunks) != 1 { t.Errorf("相近的变更应合并为 1 个 hunk,实际 %d", len(hunks)) } } // TestStructuredPatch_NoChange 测试无变更 func TestStructuredPatch_NoChange(t *testing.T) { text := "a\nb\nc" hunks := StructuredPatch(text, text, 3) if len(hunks) != 0 { t.Errorf("无变更不应产生 hunk,实际 %d 个", len(hunks)) } } // TestStructuredPatch_EmptyToNonEmpty 测试空文本到非空 func TestStructuredPatch_EmptyToNonEmpty(t *testing.T) { hunks := StructuredPatch("", "a\nb\nc", 3) if len(hunks) != 1 { t.Fatalf("期望 1 个 hunk,实际 %d", len(hunks)) } // 所有行应为插入 for _, line := range hunks[0].Lines { if !strings.HasPrefix(line, "+") { t.Errorf("空到非空的行应全部以 + 开头: %q", line) } } } // TestStructuredPatch_NonEmptyToEmpty 测试非空到空文本 func TestStructuredPatch_NonEmptyToEmpty(t *testing.T) { hunks := StructuredPatch("a\nb\nc", "", 3) if len(hunks) != 1 { t.Fatalf("期望 1 个 hunk,实际 %d", len(hunks)) } // 所有行应为删除 for _, line := range hunks[0].Lines { if !strings.HasPrefix(line, "-") { t.Errorf("非空到空的行应全部以 - 开头: %q", line) } } } // ───────────────────────────────────────────────────────────────────── // 上下文行数测试 // ───────────────────────────────────────────────────────────────────── // TestStructuredPatch_ContextLines 测试不同 contextLines 的效果 func TestStructuredPatch_ContextLines(t *testing.T) { // 10 行文件,第 5 行有变更 lines := make([]string, 10) for i := 0; i < 10; i++ { lines[i] = fmt.Sprintf("line%d", i+1) } old := strings.Join(lines, "\n") lines[4] = "MODIFIED" new := strings.Join(lines, "\n") tests := []struct { name string contextLines int }{ {"context=0", 0}, {"context=1", 1}, {"context=3", 3}, {"context=5", 5}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hunks := StructuredPatch(old, new, tt.contextLines) if len(hunks) != 1 { t.Fatalf("期望 1 个 hunk,实际 %d", len(hunks)) } h := hunks[0] contextCount := 0 for _, line := range h.Lines { if strings.HasPrefix(line, " ") { contextCount++ } } // 上下文行数不应超过 2*contextLines(变更前后各 contextLines 行) maxContext := 2 * tt.contextLines if contextCount > maxContext { t.Errorf("context=%d 时,上下文行数不应超过 %d,实际 %d", tt.contextLines, maxContext, contextCount) } // context=0 时不应有上下文行 if tt.contextLines == 0 && contextCount != 0 { t.Errorf("context=0 时不应有上下文行,实际 %d", contextCount) } }) } } // ───────────────────────────────────────────────────────────────────── // FormatUnified 测试 // ───────────────────────────────────────────────────────────────────── // TestFormatUnified_BasicFormat 测试基本格式 func TestFormatUnified_BasicFormat(t *testing.T) { hunks := []Hunk{ { OldStart: 1, OldLines: 3, NewStart: 1, NewLines: 3, Lines: []string{ " context", "-old line", "+new line", " context2", }, }, } result := FormatUnified("test.go", hunks) // 验证文件头 if !strings.Contains(result, "--- test.go") { t.Error("应包含旧文件头") } if !strings.Contains(result, "+++ test.go") { t.Error("应包含新文件头") } // 验证 hunk header if !strings.Contains(result, "@@ -1,3 +1,3 @@") { t.Errorf("hunk header 不正确: %s", result) } // 验证内容行 if !strings.Contains(result, " context\n") { t.Error("应包含上下文行") } if !strings.Contains(result, "-old line\n") { t.Error("应包含删除行") } if !strings.Contains(result, "+new line\n") { t.Error("应包含插入行") } } // TestFormatUnified_MultiHunk 测试多 hunk 格式 func TestFormatUnified_MultiHunk(t *testing.T) { hunks := []Hunk{ { OldStart: 1, OldLines: 2, NewStart: 1, NewLines: 2, Lines: []string{"-old1", "+new1", " ctx"}, }, { OldStart: 10, OldLines: 2, NewStart: 10, NewLines: 2, Lines: []string{" ctx", "-old10", "+new10"}, }, } result := FormatUnified("multi.go", hunks) // 应有两个 @@ 标记 atCount := strings.Count(result, "@@") // 每个 hunk header 有两个 @@(左右各一个),所以 2 个 hunk 有 4 个 @@ if atCount != 4 { t.Errorf("2 个 hunk 应有 4 个 @@ 标记,实际 %d", atCount) } } // TestFormatUnified_EmptyHunks 测试空 hunks func TestFormatUnified_EmptyHunks(t *testing.T) { result := FormatUnified("empty.go", nil) if result != "" { t.Errorf("空 hunks 应返回空字符串,实际 %q", result) } } // TestFormatUnified_Integration 测试 StructuredPatch + FormatUnified 集成 func TestFormatUnified_Integration(t *testing.T) { old := "line1\nline2\nline3\nline4\nline5" new := "line1\nline2\nLINE3_MODIFIED\nline4\nline5" hunks := StructuredPatch(old, new, 3) result := FormatUnified("test.go", hunks) if !strings.Contains(result, "-line3") { t.Errorf("diff 应包含删除行: %s", result) } if !strings.Contains(result, "+LINE3_MODIFIED") { t.Errorf("diff 应包含插入行: %s", result) } if !strings.Contains(result, "--- test.go") { t.Error("应包含文件头") } } // ───────────────────────────────────────────────────────────────────── // 应用完整性测试:Diff 结果可以重建新文本 // ───────────────────────────────────────────────────────────────────── // TestDiff_Reconstruct 验证从 Diff 结果可以重建新文本 func TestDiff_Reconstruct(t *testing.T) { cases := []struct { name string old []string new []string }{ {"单行替换", []string{"a"}, []string{"b"}}, {"多行插入", []string{"a", "b"}, []string{"a", "x", "y", "b"}}, {"多行删除", []string{"a", "x", "y", "b"}, []string{"a", "b"}}, {"混合操作", []string{"a", "b", "c", "d"}, []string{"a", "B", "d", "e"}}, {"空到非空", []string{}, []string{"a", "b"}}, {"非空到空", []string{"a", "b"}, []string{}}, {"完全不同", []string{"a", "b"}, []string{"x", "y", "z"}}, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { edits := Diff(tt.old, tt.new) // 从 edits 重建新文本 var reconstructed []string for _, e := range edits { switch e.Type { case EditEqual, EditInsert: reconstructed = append(reconstructed, e.Content) } } if len(reconstructed) != len(tt.new) { t.Fatalf("重建长度不匹配: 期望 %d, 实际 %d", len(tt.new), len(reconstructed)) } for i := range tt.new { if reconstructed[i] != tt.new[i] { t.Errorf("重建第 %d 行不匹配: 期望 %q, 实际 %q", i, tt.new[i], reconstructed[i]) } } }) } } // ───────────────────────────────────────────────────────────────────── // ApplyPatch 测试 // ───────────────────────────────────────────────────────────────────── // TestApplyPatch_RoundTrip 是最强的正确性证明: // 对于任意 (old, new) 文本对, ApplyPatch(old, StructuredPatch(old, new)) == new. func TestApplyPatch_RoundTrip(t *testing.T) { cases := []struct { name string old string new string ctx int // contextLines }{ {"单行替换", "a\nb\nc", "a\nB\nc", 3}, {"多行插入", "a\nb", "a\nx\ny\nz\nb", 3}, {"多行删除", "a\nx\ny\nz\nb", "a\nb", 3}, {"混合操作", "a\nb\nc\nd", "a\nB\nd\ne", 3}, {"空到非空", "", "a\nb\nc", 3}, {"非空到空", "a\nb\nc", "", 3}, {"完全不同", "a\nb\nc", "x\ny\nz", 3}, {"相同文本", "a\nb\nc", "a\nb\nc", 3}, {"单行文件", "hello", "goodbye", 3}, {"context=0", "a\nb\nc\nd\ne", "a\nb\nC\nd\ne", 0}, {"context=1", "a\nb\nc\nd\ne", "a\nb\nC\nd\ne", 1}, {"context=5", "a\nb\nc\nd\ne", "a\nb\nC\nd\ne", 5}, {"首行变更", "a\nb\nc", "A\nb\nc", 3}, {"末行变更", "a\nb\nc", "a\nb\nC", 3}, {"首尾都变", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj", "A\nb\nc\nd\ne\nf\ng\nh\ni\nJ", 3}, {"连续多行替换", "a\nb\nc\nd", "a\nX\nY\nd", 3}, {"插入到开头", "a\nb", "x\na\nb", 3}, {"插入到末尾", "a\nb", "a\nb\nx", 3}, {"删除开头", "x\na\nb", "a\nb", 3}, {"删除末尾", "a\nb\nx", "a\nb", 3}, {"带尾换行", "a\nb\n", "a\nc\n", 3}, {"中文内容", "你好\n世界\n测试", "你好\n新世界\n测试", 3}, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { hunks := StructuredPatch(tt.old, tt.new, tt.ctx) result, err := ApplyPatch(tt.old, hunks) if err != nil { t.Fatalf("ApplyPatch failed: %v", err) } if result != tt.new { t.Errorf("round-trip mismatch:\nold: %q\nnew: %q\ngot: %q", tt.old, tt.new, result) } }) } } // TestApplyPatch_MultiHunk 测试多 hunk 的正确应用(首尾各有变更) func TestApplyPatch_MultiHunk(t *testing.T) { // 20 行文件, 第 2 行和第 19 行变更 -- 产生两个独立 hunk. oldLines := make([]string, 20) newLines := make([]string, 20) for i := range 20 { line := fmt.Sprintf("line%d", i+1) oldLines[i] = line newLines[i] = line } oldLines[1] = "old_line2" newLines[1] = "new_line2" oldLines[18] = "old_line19" newLines[18] = "new_line19" old := strings.Join(oldLines, "\n") expected := strings.Join(newLines, "\n") hunks := StructuredPatch(old, expected, 3) if len(hunks) < 2 { t.Fatalf("expected >=2 hunks for distant changes, got %d", len(hunks)) } result, err := ApplyPatch(old, hunks) if err != nil { t.Fatalf("ApplyPatch failed: %v", err) } if result != expected { t.Errorf("multi-hunk apply mismatch") } } // TestApplyPatch_EmptyHunks 空 hunks 返回原文 func TestApplyPatch_EmptyHunks(t *testing.T) { text := "a\nb\nc" result, err := ApplyPatch(text, nil) if err != nil { t.Fatal(err) } if result != text { t.Errorf("empty hunks should return original text, got %q", result) } } // TestApplyPatch_ContextMismatch 上下文行不匹配时报错 func TestApplyPatch_ContextMismatch(t *testing.T) { hunks := []Hunk{ { OldStart: 1, OldLines: 3, NewStart: 1, NewLines: 3, Lines: []string{" a", "-b", "+B", " c"}, }, } // 原文第 1 行是 "x" 而非 "a" -- 上下文失配. _, err := ApplyPatch("x\nb\nc", hunks) if err == nil { t.Fatal("expected context mismatch error, got nil") } if !strings.Contains(err.Error(), "context mismatch") { t.Errorf("error should mention 'context mismatch', got: %v", err) } } // TestApplyPatch_DeleteMismatch 删除行不匹配时报错 func TestApplyPatch_DeleteMismatch(t *testing.T) { hunks := []Hunk{ { OldStart: 1, OldLines: 3, NewStart: 1, NewLines: 3, Lines: []string{" a", "-b", "+B", " c"}, }, } // 原文第 2 行是 "z" 而非 "b" -- 删除行失配. _, err := ApplyPatch("a\nz\nc", hunks) if err == nil { t.Fatal("expected delete mismatch error, got nil") } if !strings.Contains(err.Error(), "delete mismatch") { t.Errorf("error should mention 'delete mismatch', got: %v", err) } } // TestApplyPatch_OutOfRange hunk 超出文件范围时报错 func TestApplyPatch_OutOfRange(t *testing.T) { hunks := []Hunk{ { OldStart: 10, OldLines: 1, NewStart: 10, NewLines: 1, Lines: []string{" a"}, }, } _, err := ApplyPatch("only\nthree\nlines", hunks) if err == nil { t.Fatal("expected out-of-range error, got nil") } } // TestApplyPatch_OverlappingHunks 重叠 hunks 报错 func TestApplyPatch_OverlappingHunks(t *testing.T) { hunks := []Hunk{ {OldStart: 1, OldLines: 3, NewStart: 1, NewLines: 3, Lines: []string{" a", " b", " c"}}, {OldStart: 2, OldLines: 2, NewStart: 2, NewLines: 2, Lines: []string{" b", " c"}}, } _, err := ApplyPatch("a\nb\nc\nd", hunks) if err == nil { t.Fatal("expected overlapping hunks error, got nil") } if !strings.Contains(err.Error(), "overlaps") { t.Errorf("error should mention 'overlaps', got: %v", err) } } // TestApplyPatch_UnknownPrefix 未知行前缀报错 func TestApplyPatch_UnknownPrefix(t *testing.T) { hunks := []Hunk{ {OldStart: 1, OldLines: 1, NewStart: 1, NewLines: 1, Lines: []string{"?bad"}}, } _, err := ApplyPatch("a", hunks) if err == nil { t.Fatal("expected unknown prefix error, got nil") } if !strings.Contains(err.Error(), "unknown prefix") { t.Errorf("error should mention 'unknown prefix', got: %v", err) } } // TestApplyPatch_HunksReorderedInput 乱序 hunks 被自动排序后正确应用 func TestApplyPatch_HunksReorderedInput(t *testing.T) { old := "a\nb\nc\nd\ne\nf\ng\nh\ni\nj" new := "A\nb\nc\nd\ne\nf\ng\nh\ni\nJ" hunks := StructuredPatch(old, new, 3) if len(hunks) < 2 { t.Skipf("need >=2 hunks to test reorder, got %d", len(hunks)) } // 反转 hunk 顺序. reversed := make([]Hunk, len(hunks)) for i, h := range hunks { reversed[len(hunks)-1-i] = h } result, err := ApplyPatch(old, reversed) if err != nil { t.Fatalf("ApplyPatch with reordered hunks failed: %v", err) } if result != new { t.Errorf("reordered hunks should produce same result") } } // TestApplyPatch_PureInsert patch 只有插入(空文件变非空) func TestApplyPatch_PureInsert(t *testing.T) { hunks := StructuredPatch("", "hello\nworld", 3) result, err := ApplyPatch("", hunks) if err != nil { t.Fatal(err) } if result != "hello\nworld" { t.Errorf("got %q", result) } } // TestApplyPatch_PureDelete patch 只有删除(非空变空) func TestApplyPatch_PureDelete(t *testing.T) { hunks := StructuredPatch("hello\nworld", "", 3) result, err := ApplyPatch("hello\nworld", hunks) if err != nil { t.Fatal(err) } if result != "" { t.Errorf("expected empty, got %q", result) } } // ───────────────────────────────────────────────────────────────────── // ReverseHunks 测试 // ───────────────────────────────────────────────────────────────────── // TestReverseHunks_BidirectionalRoundTrip 双向 round-trip: // ApplyPatch(old, hunks) == new AND ApplyPatch(new, ReverseHunks(hunks)) == old. func TestReverseHunks_BidirectionalRoundTrip(t *testing.T) { cases := []struct { name string old string new string }{ {"替换", "a\nb\nc", "a\nB\nc"}, {"插入", "a\nb", "a\nx\nb"}, {"删除", "a\nx\nb", "a\nb"}, {"混合", "a\nb\nc\nd", "a\nB\nd\ne"}, {"空到非空", "", "hello"}, {"非空到空", "hello", ""}, {"中文", "你好\n世界", "你好\n新世界"}, {"多行替换", "a\nb\nc\nd\ne", "a\nX\nY\nZ\ne"}, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { hunks := StructuredPatch(tt.old, tt.new, 3) // Forward: old → new forward, err := ApplyPatch(tt.old, hunks) if err != nil { t.Fatalf("forward apply failed: %v", err) } if forward != tt.new { t.Fatalf("forward mismatch: got %q, want %q", forward, tt.new) } // Reverse: new → old revHunks := ReverseHunks(hunks) backward, err := ApplyPatch(tt.new, revHunks) if err != nil { t.Fatalf("reverse apply failed: %v", err) } if backward != tt.old { t.Errorf("reverse mismatch: got %q, want %q", backward, tt.old) } }) } } // TestReverseHunks_Symmetry ReverseHunks 的结构对称性 func TestReverseHunks_Symmetry(t *testing.T) { hunks := []Hunk{ { OldStart: 5, OldLines: 3, NewStart: 5, NewLines: 4, Lines: []string{" ctx", "-del", "+ins1", "+ins2", " ctx2"}, }, } reversed := ReverseHunks(hunks) if len(reversed) != 1 { t.Fatalf("expected 1 reversed hunk, got %d", len(reversed)) } rh := reversed[0] // OldStart/OldLines 和 NewStart/NewLines 互换 if rh.OldStart != 5 || rh.OldLines != 4 { t.Errorf("OldStart/OldLines: got %d/%d, want 5/4", rh.OldStart, rh.OldLines) } if rh.NewStart != 5 || rh.NewLines != 3 { t.Errorf("NewStart/NewLines: got %d/%d, want 5/3", rh.NewStart, rh.NewLines) } // 行前缀翻转 expected := []string{" ctx", "+del", "-ins1", "-ins2", " ctx2"} for i, line := range rh.Lines { if line != expected[i] { t.Errorf("line %d: got %q, want %q", i, line, expected[i]) } } } // TestReverseHunks_DoubleReverse 两次 reverse 等于原始 func TestReverseHunks_DoubleReverse(t *testing.T) { hunks := []Hunk{ { OldStart: 1, OldLines: 2, NewStart: 1, NewLines: 3, Lines: []string{" a", "-b", "+c", "+d"}, }, } doubleReversed := ReverseHunks(ReverseHunks(hunks)) if len(doubleReversed) != len(hunks) { t.Fatalf("length mismatch") } h := doubleReversed[0] orig := hunks[0] if h.OldStart != orig.OldStart || h.OldLines != orig.OldLines || h.NewStart != orig.NewStart || h.NewLines != orig.NewLines { t.Error("double reverse should restore header fields") } for i := range h.Lines { if h.Lines[i] != orig.Lines[i] { t.Errorf("double reverse line %d: got %q, want %q", i, h.Lines[i], orig.Lines[i]) } } } // TestReverseHunks_Empty 空 hunks 反转仍为空 func TestReverseHunks_Empty(t *testing.T) { reversed := ReverseHunks(nil) if len(reversed) != 0 { t.Errorf("reverse of nil should be empty, got %d", len(reversed)) } } // ───────────────────────────────────────────────────────────────────── // 集成测试: 大文件 round-trip // ───────────────────────────────────────────────────────────────────── // TestApplyPatch_LargeFile 大文件(500 行)多处变更的 round-trip. func TestApplyPatch_LargeFile(t *testing.T) { const n = 500 oldLines := make([]string, n) newLines := make([]string, n) for i := range n { line := fmt.Sprintf("line %04d: content here", i+1) oldLines[i] = line newLines[i] = line } // 在 5 个位置做变更. for _, idx := range []int{10, 100, 250, 400, 490} { newLines[idx] = fmt.Sprintf("MODIFIED line %04d", idx+1) } old := strings.Join(oldLines, "\n") expected := strings.Join(newLines, "\n") hunks := StructuredPatch(old, expected, 3) result, err := ApplyPatch(old, hunks) if err != nil { t.Fatalf("large file apply failed: %v", err) } if result != expected { t.Error("large file round-trip mismatch") } // Reverse round-trip. revResult, err := ApplyPatch(expected, ReverseHunks(hunks)) if err != nil { t.Fatalf("large file reverse apply failed: %v", err) } if revResult != old { t.Error("large file reverse round-trip mismatch") } } // ───────────────────────────────────────────────────────────────────── // 辅助函数 // ───────────────────────────────────────────────────────────────────── // countEditType 统计编辑脚本中指定类型的操作数 func countEditType(edits []Edit, t EditType) int { count := 0 for _, e := range edits { if e.Type == t { count++ } } return count }