Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207package pulls
import ( "html/template" "testing"
gitmirrorv1 "tangled.org/core/gitmirror/proto/gen")
func u32(v uint32) *uint32 { return &v }
// lines returns n lines named l0..l{n-1}, optionally overriding some by index.func lines(n int, override map[int]string) []template.HTML { out := make([]template.HTML, n) for i := range out { if v, ok := override[i]; ok { out[i] = template.HTML(v) } else { out[i] = template.HTML("l" + string(rune('0'+i%10))) } } return out}
func hunk(pairs ...*gitmirrorv1.LinePair) *gitmirrorv1.Hunk { return &gitmirrorv1.Hunk{Lines: pairs}}
// identity opposite maps + generous bounds, for exercising the merge/context logic directly.func identityOpp(n int) (map[int]int, map[int]int) { l, r := map[int]int{}, map[int]int{} for i := 0; i < n; i++ { l[i], r[i] = i, i } return l, r}
func TestExtractLines_FillsInteriorGap(t *testing.T) { got := extractLines([]linePair{{0, 0}, {2, 2}}) want := []linePair{{0, 0}, {1, 1}, {2, 2}} if len(got) != len(want) { t.Fatalf("got %+v, want %+v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("pair %d = %+v, want %+v", i, got[i], want[i]) } }}
func TestPadBeforeAfter_ClampAndCount(t *testing.T) { // n+1 = 4 lines when far from the edges. if got := padBefore(10, numContextLines); len(got) != 4 || got[0] != 6 || got[3] != 9 { t.Fatalf("padBefore(10) = %v", got) } if got := padAfter(10, 100, numContextLines); len(got) != 4 || got[0] != 11 || got[3] != 14 { t.Fatalf("padAfter(10,100) = %v", got) } // Clamp at start of file. if got := padBefore(2, numContextLines); len(got) != 2 || got[0] != 0 || got[1] != 1 { t.Fatalf("padBefore(2) = %v", got) } // Clamp at end of file. if got := padAfter(98, 100, numContextLines); len(got) != 2 || got[0] != 99 || got[1] != 100 { t.Fatalf("padAfter(98,100) = %v", got) }}
func TestMergeAdjacent_MergeClose(t *testing.T) { oppL, oppR := identityOpp(200) // Two deletions 2 lines apart on the lhs -> context windows overlap -> merge. got := mergeAdjacent([][]linePair{{{2, -1}}, {{4, -1}}}, oppL, oppR, 199, 199, numContextLines) if len(got) != 1 { t.Fatalf("want 1 merged hunk, got %d: %+v", len(got), got) }}
func TestMergeAdjacent_SplitFar(t *testing.T) { oppL, oppR := identityOpp(200) got := mergeAdjacent([][]linePair{{{2, -1}}, {{40, -1}}}, oppL, oppR, 199, 199, numContextLines) if len(got) != 2 { t.Fatalf("want 2 hunks, got %d: %+v", len(got), got) }}
func TestMergeAdjacent_LargeOneSidedInsertion(t *testing.T) { oppL, oppR := identityOpp(200) // Two modifications adjacent on the lhs (2 and 4) but far apart on the rhs (2 and 40), // as if 30+ lines were inserted between them. difftastic merges by per-side line // distance, so the lhs overlap keeps them one hunk — the case aligned-index windowing // would have split. got := mergeAdjacent([][]linePair{{{2, 2}}, {{4, 40}}}, oppL, oppR, 199, 199, numContextLines) if len(got) != 1 { t.Fatalf("want 1 merged hunk (lhs overlap), got %d: %+v", len(got), got) }}
func TestBuildOpposites_DeletionPairing(t *testing.T) { // Delete base line 4 of a 10-line file -> head has 9 lines. Unchanged base 5..9 must // pair with head 4..8 (the bijection difftastic's opposite_positions encodes). base := lines(10, nil) head := lines(9, nil) hunks := []*gitmirrorv1.Hunk{hunk(&gitmirrorv1.LinePair{Lhs: u32(4)})}
oppLhs, oppRhs, maxLhs, maxRhs := buildOpposites(base, head, hunks) if maxLhs != 9 || maxRhs != 8 { t.Fatalf("max lines = %d,%d want 9,8", maxLhs, maxRhs) } for l := 5; l <= 9; l++ { if oppLhs[l] != l-1 { t.Fatalf("oppLhs[%d] = %d, want %d", l, oppLhs[l], l-1) } } if oppRhs[4] != 5 { // head line 4 <-> base line 5 t.Fatalf("oppRhs[4] = %d, want 5", oppRhs[4]) } // The deleted base line 4 has no counterpart. if _, ok := oppLhs[4]; ok { t.Fatalf("deleted base line 4 should have no opposite") }}
func TestBuildHunks_MarksNovelAndAddsContext(t *testing.T) { base := lines(10, map[int]string{5: "old"}) head := lines(10, map[int]string{5: "new"}) built := buildHunks(base, head, []*gitmirrorv1.Hunk{hunk(&gitmirrorv1.LinePair{Lhs: u32(5), Rhs: u32(5)})}) if len(built) != 1 { t.Fatalf("want 1 hunk, got %d", len(built)) } var nChanged, nContext int for _, r := range built[0].rows { if r.changed { nChanged++ if r.lhs != 5 || r.rhs != 5 { t.Fatalf("changed row = %+v, want {5,5}", r) } } else { nContext++ } } if nChanged != 1 { t.Fatalf("want 1 changed row, got %d", nChanged) } // Side-by-side display shows numContextLines (3) context rows each side, index-based // (difftastic's matched_lines_indexes_for_hunk), both clamped inside the 10-line file. if nContext != 6 { t.Fatalf("want 6 context rows, got %d: %+v", nContext, built[0].rows) }}
func TestBuildHunks_MergedKeepsInteriorContext(t *testing.T) { // Regression: a deletion and a nearby addition, merged into one hunk, are separated by // unchanged lines. Those interior context rows must survive (reconstructing context via // extractLines/fillBetween dropped them across the deletion->addition boundary). // Base: 20 lines; delete base line 5. Head: base with line 5 removed and a new line // inserted at head index 8 (a few lines after the deletion point). base := lines(20, nil) head := make([]template.HTML, 0, 20) head = append(head, base[:5]...) // head 0..4 == base 0..4 head = append(head, base[6:9]...) // head 5..7 == base 6..8 head = append(head, template.HTML("INSERTED")) // head 8 (new) head = append(head, base[9:]...) // head 9.. == base 9..
hunks := []*gitmirrorv1.Hunk{ hunk(&gitmirrorv1.LinePair{Lhs: u32(5)}), // delete base line 5 hunk(&gitmirrorv1.LinePair{Rhs: u32(8)}), // insert head line 8 } built := buildHunks(base, head, hunks) if len(built) != 1 { t.Fatalf("want 1 merged hunk, got %d", len(built)) } // The unchanged lines between the deletion (base 5) and the insertion (head 8, i.e. base // 8) — base lines 6,7,8 — must appear as context rows (both sides present, unchanged). want := map[int]bool{6: false, 7: false, 8: false} got := map[int]bool{} for _, r := range built[0].rows { if !r.changed && r.lhs >= 0 { if _, ok := want[r.lhs]; ok { got[r.lhs] = true } } } for ln := range want { if !got[ln] { t.Fatalf("interior context base line %d missing from merged hunk: %+v", ln, built[0].rows) } }}
func TestIndexesForHunk_SliceAndClamp(t *testing.T) { base := lines(20, nil) head := lines(20, nil) // Single change at line 10. pairs, _ := alignFile(base, head, []*gitmirrorv1.Hunk{hunk(&gitmirrorv1.LinePair{Lhs: u32(10), Rhs: u32(10)})}) lo, hi := indexesForHunk(pairs, []linePair{{10, 10}}, numContextLines) if lo != 7 || hi != 14 { // [10-3 .. 10+3+1) t.Fatalf("slice = [%d,%d), want [7,14)", lo, hi) } // Clamp at start of file. pairs2, _ := alignFile(base, head, []*gitmirrorv1.Hunk{hunk(&gitmirrorv1.LinePair{Lhs: u32(1), Rhs: u32(1)})}) lo2, _ := indexesForHunk(pairs2, []linePair{{1, 1}}, numContextLines) if lo2 != 0 { t.Fatalf("start clamp = %d, want 0", lo2) }}