Artifact 4fa6c564e2ec9220b5ce25a7159389c14b15a6ce48f63d6d900d0fde8e57c8fc:
- File
internal/diff/diff_test.go
— part of check-in
[f5744359d3]
at
2026-04-19 16:30:54
on branch trunk
— Initial open source release
libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil.
This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil.
Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ (user: [email protected], size: 10554) [annotate] [blame] [check-ins using]
package diff import ( "fmt" "strings" "testing" ) func TestSplitLines(t *testing.T) { tests := []struct { name string input string want []string }{ {"empty", "", nil}, {"single no newline", "hello", []string{"hello"}}, {"single with newline", "hello\n", []string{"hello"}}, {"two lines", "a\nb\n", []string{"a", "b"}}, {"no trailing newline", "a\nb", []string{"a", "b"}}, {"crlf normalized", "a\r\nb\r\n", []string{"a", "b"}}, {"mixed eol", "a\nb\r\nc\n", []string{"a", "b", "c"}}, {"blank lines", "a\n\nb\n", []string{"a", "", "b"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := splitLines([]byte(tt.input)) if len(got) != len(tt.want) { t.Fatalf("splitLines(%q) = %v (len %d), want %v (len %d)", tt.input, got, len(got), tt.want, len(tt.want)) } for i := range got { if got[i] != tt.want[i] { t.Errorf("line %d: got %q, want %q", i, got[i], tt.want[i]) } } }) } } func TestIsBinary(t *testing.T) { if isBinary([]byte("hello world")) { t.Fatal("text should not be binary") } if !isBinary([]byte("hello\x00world")) { t.Fatal("null byte should be binary") } if isBinary(nil) { t.Fatal("nil should not be binary") } if isBinary([]byte{}) { t.Fatal("empty should not be binary") } } func TestMyersIdentical(t *testing.T) { ops := myers([]string{"a", "b", "c"}, []string{"a", "b", "c"}) for _, op := range ops { if op.kind != opEqual { t.Fatalf("identical inputs should produce only opEqual, got %v", op.kind) } } if len(ops) != 3 { t.Fatalf("got %d ops, want 3", len(ops)) } } func TestMyersInsert(t *testing.T) { ops := myers([]string{"a", "c"}, []string{"a", "b", "c"}) var inserts int for _, op := range ops { if op.kind == opInsert { inserts++ if op.text != "b" { t.Fatalf("inserted text = %q, want %q", op.text, "b") } } } if inserts != 1 { t.Fatalf("got %d inserts, want 1", inserts) } } func TestMyersDelete(t *testing.T) { ops := myers([]string{"a", "b", "c"}, []string{"a", "c"}) var deletes int for _, op := range ops { if op.kind == opDelete { deletes++ if op.text != "b" { t.Fatalf("deleted text = %q, want %q", op.text, "b") } } } if deletes != 1 { t.Fatalf("got %d deletes, want 1", deletes) } } func TestMyersEmpty(t *testing.T) { ops := myers(nil, []string{"a", "b"}) var inserts int for _, op := range ops { if op.kind == opInsert { inserts++ } } if inserts != 2 { t.Fatalf("got %d inserts, want 2", inserts) } ops = myers([]string{"a", "b"}, nil) var deletes int for _, op := range ops { if op.kind == opDelete { deletes++ } } if deletes != 2 { t.Fatalf("got %d deletes, want 2", deletes) } } func TestMyersMixed(t *testing.T) { src := []string{"a", "b", "c", "d", "e"} dst := []string{"a", "x", "c", "e", "f"} ops := myers(src, dst) // Verify we get a valid edit script: applying ops to src produces dst. var result []string for _, op := range ops { switch op.kind { case opEqual, opInsert: result = append(result, op.text) } } if len(result) != len(dst) { t.Fatalf("applying ops: got %v, want %v", result, dst) } for i := range result { if result[i] != dst[i] { t.Fatalf("line %d: got %q, want %q", i, result[i], dst[i]) } } } func TestUnifiedIdentical(t *testing.T) { a := []byte("hello\nworld\n") got := Unified(a, a, Options{ContextLines: 3}) if got != "" { t.Fatalf("identical inputs should return empty string, got:\n%s", got) } } func TestUnifiedSimpleChange(t *testing.T) { a := []byte("line1\nline2\nline3\n") b := []byte("line1\nchanged\nline3\n") got := Unified(a, b, Options{ ContextLines: 3, SrcName: "a/file.txt", DstName: "b/file.txt", }) if got == "" { t.Fatal("expected non-empty diff") } if !strings.Contains(got, "--- a/file.txt") { t.Fatalf("missing src header in:\n%s", got) } if !strings.Contains(got, "+++ b/file.txt") { t.Fatalf("missing dst header in:\n%s", got) } if !strings.Contains(got, "-line2") { t.Fatalf("missing deletion in:\n%s", got) } if !strings.Contains(got, "+changed") { t.Fatalf("missing insertion in:\n%s", got) } if !strings.Contains(got, " line1") { t.Fatalf("missing context in:\n%s", got) } if !strings.Contains(got, "@@") { t.Fatalf("missing hunk header in:\n%s", got) } } func TestUnifiedInsertOnly(t *testing.T) { a := []byte("a\nc\n") b := []byte("a\nb\nc\n") got := Unified(a, b, Options{ContextLines: 3}) if !strings.Contains(got, "+b") { t.Fatalf("expected +b in:\n%s", got) } } func TestUnifiedDeleteOnly(t *testing.T) { a := []byte("a\nb\nc\n") b := []byte("a\nc\n") got := Unified(a, b, Options{ContextLines: 3}) if !strings.Contains(got, "-b") { t.Fatalf("expected -b in:\n%s", got) } } func TestUnifiedZeroContext(t *testing.T) { a := []byte("a\nb\nc\nd\ne\n") b := []byte("a\nB\nc\nd\nE\n") got := Unified(a, b, Options{ContextLines: 0}) // Two changes separated by 2 unchanged lines -- with 0 context, must be 2 hunks. hunkCount := strings.Count(got, "@@ -") if !strings.HasPrefix(got, "---") { t.Fatalf("expected diff header, got:\n%s", got) } if hunkCount != 2 { t.Fatalf("expected 2 hunks with 0 context, got %d:\n%s", hunkCount, got) } // No context lines should appear (lines starting with a space). for _, line := range strings.Split(got, "\n") { if len(line) > 0 && line[0] == ' ' { t.Fatalf("zero-context diff should not have context lines, found %q in:\n%s", line, got) } } } func TestUnifiedBinary(t *testing.T) { a := []byte("hello\x00world") b := []byte("different") got := Unified(a, b, Options{SrcName: "bin.dat", DstName: "bin.dat"}) if !strings.Contains(got, "cannot compute difference between binary files") { t.Fatalf("binary input should report binary message, got:\n%s", got) } if !strings.Contains(got, "--- bin.dat") { t.Fatalf("binary diff should include headers, got:\n%s", got) } } func TestUnifiedBothEmpty(t *testing.T) { got := Unified(nil, nil, Options{}) if got != "" { t.Fatalf("both nil should return empty, got: %q", got) } } func TestUnifiedOneEmpty(t *testing.T) { a := []byte("hello\n") got := Unified(nil, a, Options{SrcName: "old", DstName: "new"}) if !strings.Contains(got, "+hello") { t.Fatalf("expected insertion in:\n%s", got) } got2 := Unified(a, nil, Options{SrcName: "old", DstName: "new"}) if !strings.Contains(got2, "-hello") { t.Fatalf("expected deletion in:\n%s", got2) } } func TestStatSimple(t *testing.T) { a := []byte("a\nb\nc\n") b := []byte("a\nB\nc\nd\n") stat := Stat(a, b) // b changed -> 1 deletion + 1 insertion, d added -> 1 insertion if stat.Insertions < 1 { t.Fatalf("Insertions = %d, want >= 1", stat.Insertions) } if stat.Deletions < 1 { t.Fatalf("Deletions = %d, want >= 1", stat.Deletions) } if stat.Binary { t.Fatal("should not be binary") } } func TestStatBinary(t *testing.T) { stat := Stat([]byte("a\x00b"), []byte("c")) if !stat.Binary { t.Fatal("should detect binary") } } func TestStatIdentical(t *testing.T) { a := []byte("same\n") stat := Stat(a, a) if stat.Insertions != 0 || stat.Deletions != 0 { t.Fatalf("identical: got %+v", stat) } } func TestUnifiedLargeFile(t *testing.T) { // 10K lines, change every 100th line. var a, b strings.Builder for i := 0; i < 10000; i++ { fmt.Fprintf(&a, "line %d original\n", i) if i%100 == 50 { fmt.Fprintf(&b, "line %d CHANGED\n", i) } else { fmt.Fprintf(&b, "line %d original\n", i) } } got := Unified([]byte(a.String()), []byte(b.String()), Options{ContextLines: 3}) if got == "" { t.Fatal("expected non-empty diff for large file") } stat := Stat([]byte(a.String()), []byte(b.String())) // 100 lines changed = 100 deletions + 100 insertions if stat.Insertions != 100 { t.Fatalf("Insertions = %d, want 100", stat.Insertions) } if stat.Deletions != 100 { t.Fatalf("Deletions = %d, want 100", stat.Deletions) } } func TestUnifiedWhitespaceOnly(t *testing.T) { a := []byte(" hello \n world \n") b := []byte(" hello\n world \n") got := Unified(a, b, Options{ContextLines: 3}) if got == "" { t.Fatal("whitespace change should produce diff") } } func TestUnifiedEmptyLinesOnly(t *testing.T) { a := []byte("\n\n\n") b := []byte("\n\n\n\n") got := Unified(a, b, Options{ContextLines: 3}) if got == "" { t.Fatal("added empty line should produce diff") } } func TestUnifiedSingleChar(t *testing.T) { a := []byte("x") b := []byte("y") got := Unified(a, b, Options{ContextLines: 3}) if !strings.Contains(got, "-x") || !strings.Contains(got, "+y") { t.Fatalf("single char diff:\n%s", got) } } func TestUnifiedLongLine(t *testing.T) { long := strings.Repeat("x", 10000) a := []byte(long + "\n") b := []byte(long + "y\n") got := Unified(a, b, Options{ContextLines: 3}) if got == "" { t.Fatal("long line change should produce diff") } } func TestUnifiedUnicode(t *testing.T) { a := []byte("hello δΈη\n") b := []byte("hello π\n") got := Unified(a, b, Options{ContextLines: 3}) if got == "" { t.Fatal("unicode change should produce diff") } if !strings.Contains(got, "-hello δΈη") { t.Fatalf("missing unicode deletion:\n%s", got) } } func TestUnifiedNoTrailingNewlineBoth(t *testing.T) { a := []byte("line1\nline2") b := []byte("line1\nchanged") got := Unified(a, b, Options{ContextLines: 3}) if got == "" { t.Fatal("expected diff for no-trailing-newline") } } func TestUnifiedAllDifferent(t *testing.T) { // Worst case for Myers: every line differs. var a, b strings.Builder for i := 0; i < 100; i++ { fmt.Fprintf(&a, "old line %d\n", i) fmt.Fprintf(&b, "new line %d\n", i) } got := Unified([]byte(a.String()), []byte(b.String()), Options{ContextLines: 3}) if got == "" { t.Fatal("all-different should produce diff") } stat := Stat([]byte(a.String()), []byte(b.String())) if stat.Insertions != 100 || stat.Deletions != 100 { t.Fatalf("all-different stat: %+v", stat) } } func TestUnifiedNegativeContextPanics(t *testing.T) { defer func() { if r := recover(); r == nil { t.Fatal("expected panic for negative ContextLines") } }() Unified([]byte("a\n"), []byte("b\n"), Options{ContextLines: -1}) } func BenchmarkMyers(b *testing.B) { var src, dst []string for i := 0; i < 1000; i++ { src = append(src, fmt.Sprintf("line %d", i)) if i%10 == 5 { dst = append(dst, fmt.Sprintf("line %d changed", i)) } else { dst = append(dst, fmt.Sprintf("line %d", i)) } } b.ResetTimer() for i := 0; i < b.N; i++ { myers(src, dst) } }