aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--diffbytes/diffbytes_test.go326
-rw-r--r--difflines/difflines.go (renamed from diffbytes/diffbytes.go)42
-rw-r--r--difflines/difflines_test.go326
-rw-r--r--difflines/unsafe.go (renamed from diffbytes/unsafe.go)2
4 files changed, 348 insertions, 348 deletions
diff --git a/diffbytes/diffbytes_test.go b/diffbytes/diffbytes_test.go
deleted file mode 100644
index 00af1881..00000000
--- a/diffbytes/diffbytes_test.go
+++ /dev/null
@@ -1,326 +0,0 @@
-package diffbytes
-
-import (
- "bytes"
- "strconv"
- "strings"
- "testing"
-)
-
-func TestDiffBytes(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- oldInput string
- newInput string
- expected []BytesDiffChunk
- }{
- {
- name: "empty inputs produce no chunks",
- oldInput: "",
- newInput: "",
- expected: []BytesDiffChunk{},
- },
- {
- name: "only additions",
- oldInput: "",
- newInput: "alpha\nbeta\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindAdded, Data: []byte("alpha\nbeta\n")},
- },
- },
- {
- name: "only deletions",
- oldInput: "alpha\nbeta\n",
- newInput: "",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("alpha\nbeta\n")},
- },
- },
- {
- name: "unchanged content is grouped",
- oldInput: "same\nlines\n",
- newInput: "same\nlines\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("same\nlines\n")},
- },
- },
- {
- name: "insertion in the middle",
- oldInput: "a\nb\nc\n",
- newInput: "a\nb\nX\nc\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("a\nb\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("X\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("c\n")},
- },
- },
- {
- name: "replacement without trailing newline",
- oldInput: "first\nsecond",
- newInput: "first\nsecond\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("first\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("second")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("second\n")},
- },
- },
- {
- name: "line replacement",
- oldInput: "a\nb\nc\n",
- newInput: "a\nB\nc\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("a\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("b\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("B\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("c\n")},
- },
- },
- {
- name: "swap adjacent lines",
- oldInput: "A\nB\n",
- newInput: "B\nA\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("A\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("B\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("A\n")},
- },
- },
- {
- name: "indentation change is a full line replacement",
- oldInput: "func main() {\n\treturn\n}\n",
- newInput: "func main() {\n return\n}\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("func main() {\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("\treturn\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte(" return\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("}\n")},
- },
- },
- {
- name: "commenting out lines",
- oldInput: "code\n",
- newInput: "// code\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("code\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("// code\n")},
- },
- },
- {
- name: "reducing repeating lines",
- oldInput: "log\nlog\nlog\n",
- newInput: "log\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("log\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("log\nlog\n")},
- },
- },
- {
- name: "expanding repeating lines",
- oldInput: "tick\n",
- newInput: "tick\ntick\ntick\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("tick\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("tick\ntick\n")},
- },
- },
- {
- name: "interleaved modifications",
- oldInput: "keep\nchange\nkeep\nchange\n",
- newInput: "keep\nfixed\nkeep\nfixed\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("keep\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("change\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("fixed\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("keep\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("change\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("fixed\n")},
- },
- },
- {
- name: "large common header and footer",
- oldInput: "header\nheader\nheader\nOLD\nfooter\nfooter\n",
- newInput: "header\nheader\nheader\nNEW\nfooter\nfooter\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("header\nheader\nheader\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("OLD\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("NEW\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("footer\nfooter\n")},
- },
- },
- {
- name: "completely different content",
- oldInput: "apple\nbanana\n",
- newInput: "cherry\ndate\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("apple\nbanana\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("cherry\ndate\n")},
- },
- },
- {
- name: "unicode and emoji changes",
- oldInput: "Hello 🌍\nYay\n",
- newInput: "Hello 🌎\nYay\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("Hello 🌍\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("Hello 🌎\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("Yay\n")},
- },
- },
- {
- name: "binary data with embedded newlines",
- oldInput: "\x00\x01\n\x02\x03\n",
- newInput: "\x00\x01\n\x02\xFF\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("\x00\x01\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("\x02\x03\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("\x02\xFF\n")},
- },
- },
- {
- name: "adding trailing newline to last line",
- oldInput: "Line 1\nLine 2",
- newInput: "Line 1\nLine 2\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("Line 1\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("Line 2")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("Line 2\n")},
- },
- },
- {
- name: "removing trailing newline",
- oldInput: "A\nB\n",
- newInput: "A\nB",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("B\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("B")},
- },
- },
- {
- name: "inserting blank lines",
- oldInput: "A\nB\n",
- newInput: "A\n\n\nB\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("\n\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("B\n")},
- },
- },
- {
- name: "collapsing blank lines",
- oldInput: "A\n\n\n\nB\n",
- newInput: "A\nB\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("\n\n\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("B\n")},
- },
- },
- {
- name: "case sensitivity check",
- oldInput: "FOO\nbar\n",
- newInput: "foo\nbar\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("FOO\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("foo\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("bar\n")},
- },
- },
- {
- name: "partial line match is full mismatch",
- oldInput: "The quick brown fox\n",
- newInput: "The quick brown fox jumps\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("The quick brown fox\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("The quick brown fox jumps\n")},
- },
- },
- {
- name: "inserting middle content",
- oldInput: "Top\nBottom\n",
- newInput: "Top\nMiddle\nBottom\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("Top\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("Middle\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("Bottom\n")},
- },
- },
- {
- name: "block move simulated",
- oldInput: "BlockA\nBlockB\nBlockC\n",
- newInput: "BlockA\nBlockC\nBlockB\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("BlockA\n")},
- {Kind: BytesDiffChunkKindDeleted, Data: []byte("BlockB\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("BlockC\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("BlockB\n")},
- },
- },
- {
- name: "alternating additions",
- oldInput: "A\nB\nC\n",
- newInput: "A\n1\nB\n2\nC\n",
- expected: []BytesDiffChunk{
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("A\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("1\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("B\n")},
- {Kind: BytesDiffChunkKindAdded, Data: []byte("2\n")},
- {Kind: BytesDiffChunkKindUnchanged, Data: []byte("C\n")},
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
- chunks, err := DiffBytes([]byte(tt.oldInput), []byte(tt.newInput))
- if err != nil {
- t.Fatalf("DiffBytes returned error: %v", err)
- }
-
- if len(chunks) != len(tt.expected) {
- t.Fatalf("expected %d chunks, got %d: %s", len(tt.expected), len(chunks), formatChunks(chunks))
- }
-
- for i := range tt.expected {
- if chunks[i].Kind != tt.expected[i].Kind {
- t.Fatalf("chunk %d kind mismatch: got %v, want %v; chunks: %s", i, chunks[i].Kind, tt.expected[i].Kind, formatChunks(chunks))
- }
- if !bytes.Equal(chunks[i].Data, tt.expected[i].Data) {
- t.Fatalf("chunk %d data mismatch: got %q, want %q; chunks: %s", i, string(chunks[i].Data), string(tt.expected[i].Data), formatChunks(chunks))
- }
- }
- })
- }
-}
-
-func formatChunks(chunks []BytesDiffChunk) string {
- var b strings.Builder
- b.WriteByte('[')
- for i, chunk := range chunks {
- if i > 0 {
- b.WriteString(", ")
- }
- b.WriteString(chunkKindName(chunk.Kind))
- b.WriteByte(':')
- b.WriteString(strconv.Quote(string(chunk.Data)))
- }
- b.WriteByte(']')
- return b.String()
-}
-
-func chunkKindName(kind BytesDiffChunkKind) string {
- switch kind {
- case BytesDiffChunkKindUnchanged:
- return "U"
- case BytesDiffChunkKindDeleted:
- return "D"
- case BytesDiffChunkKindAdded:
- return "A"
- default:
- return "?"
- }
-}
diff --git a/diffbytes/diffbytes.go b/difflines/difflines.go
index ab2ee849..db4c1a03 100644
--- a/diffbytes/diffbytes.go
+++ b/difflines/difflines.go
@@ -1,11 +1,11 @@
-// Package diffbytes provides routines to perform line-based diffs.
-package diffbytes
+// Package difflines provides routines to perform line-based diffs.
+package difflines
import "bytes"
-// DiffBytes performs a line-based diff.
+// DiffLines performs a line-based diff.
// Lines are bytes up to and including '\n' (final line may lack '\n').
-func DiffBytes(oldB, newB []byte) ([]BytesDiffChunk, error) {
+func DiffLines(oldB, newB []byte) ([]LinesDiffChunk, error) {
type lineRef struct {
base []byte
start int
@@ -119,7 +119,7 @@ func DiffBytes(oldB, newB []byte) ([]BytesDiffChunk, error) {
}
type edit struct {
- kind BytesDiffChunkKind
+ kind LinesDiffChunkKind
lineref lineRef
}
revEdits := make([]edit, 0, n+m)
@@ -148,7 +148,7 @@ func DiffBytes(oldB, newB []byte) ([]BytesDiffChunk, error) {
for x > prevX && y > prevY {
x--
y--
- revEdits = append(revEdits, edit{kind: BytesDiffChunkKindUnchanged, lineref: oldLines[x]})
+ revEdits = append(revEdits, edit{kind: LinesDiffChunkKindUnchanged, lineref: oldLines[x]})
}
if D == 0 {
@@ -157,10 +157,10 @@ func DiffBytes(oldB, newB []byte) ([]BytesDiffChunk, error) {
if x == prevX {
y--
- revEdits = append(revEdits, edit{kind: BytesDiffChunkKindAdded, lineref: newLines[y]})
+ revEdits = append(revEdits, edit{kind: LinesDiffChunkKindAdded, lineref: newLines[y]})
} else {
x--
- revEdits = append(revEdits, edit{kind: BytesDiffChunkKindDeleted, lineref: oldLines[x]})
+ revEdits = append(revEdits, edit{kind: LinesDiffChunkKindDeleted, lineref: oldLines[x]})
}
}
@@ -168,7 +168,7 @@ func DiffBytes(oldB, newB []byte) ([]BytesDiffChunk, error) {
revEdits[i], revEdits[j] = revEdits[j], revEdits[i]
}
- var out []BytesDiffChunk
+ var out []LinesDiffChunk
type meta struct {
base []byte
start int
@@ -182,7 +182,7 @@ func DiffBytes(oldB, newB []byte) ([]BytesDiffChunk, error) {
curEnd := e.lineref.end
if len(out) == 0 || out[len(out)-1].Kind != e.kind {
- out = append(out, BytesDiffChunk{Kind: e.kind, Data: curBase[curStart:curEnd]})
+ out = append(out, LinesDiffChunk{Kind: e.kind, Data: curBase[curStart:curEnd]})
metas = append(metas, meta{base: curBase, start: curStart, end: curEnd})
continue
}
@@ -203,21 +203,21 @@ func DiffBytes(oldB, newB []byte) ([]BytesDiffChunk, error) {
return out, nil
}
-// BytesDiffChunk represents a contiguous region of bytes categorized
+// LinesDiffChunk represents a contiguous region of lines categorized
// as unchanged, deleted, or added.
-type BytesDiffChunk struct {
- Kind BytesDiffChunkKind
+type LinesDiffChunk struct {
+ Kind LinesDiffChunkKind
Data []byte
}
-// BytesDiffChunkKind enumerates the type of diff chunk.
-type BytesDiffChunkKind int
+// LinesDiffChunkKind enumerates the type of diff chunk.
+type LinesDiffChunkKind int
const (
- // BytesDiffChunkKindUnchanged represents an unchanged diff chunk.
- BytesDiffChunkKindUnchanged BytesDiffChunkKind = iota
- // BytesDiffChunkKindDeleted represents a deleted diff chunk.
- BytesDiffChunkKindDeleted
- // BytesDiffChunkKindAdded represents an added diff chunk.
- BytesDiffChunkKindAdded
+ // LinesDiffChunkKindUnchanged represents an unchanged diff chunk.
+ LinesDiffChunkKindUnchanged LinesDiffChunkKind = iota
+ // LinesDiffChunkKindDeleted represents a deleted diff chunk.
+ LinesDiffChunkKindDeleted
+ // LinesDiffChunkKindAdded represents an added diff chunk.
+ LinesDiffChunkKindAdded
)
diff --git a/difflines/difflines_test.go b/difflines/difflines_test.go
new file mode 100644
index 00000000..783c2d6e
--- /dev/null
+++ b/difflines/difflines_test.go
@@ -0,0 +1,326 @@
+package difflines
+
+import (
+ "bytes"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+func TestDiffLines(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ oldInput string
+ newInput string
+ expected []LinesDiffChunk
+ }{
+ {
+ name: "empty inputs produce no chunks",
+ oldInput: "",
+ newInput: "",
+ expected: []LinesDiffChunk{},
+ },
+ {
+ name: "only additions",
+ oldInput: "",
+ newInput: "alpha\nbeta\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("alpha\nbeta\n")},
+ },
+ },
+ {
+ name: "only deletions",
+ oldInput: "alpha\nbeta\n",
+ newInput: "",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("alpha\nbeta\n")},
+ },
+ },
+ {
+ name: "unchanged content is grouped",
+ oldInput: "same\nlines\n",
+ newInput: "same\nlines\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("same\nlines\n")},
+ },
+ },
+ {
+ name: "insertion in the middle",
+ oldInput: "a\nb\nc\n",
+ newInput: "a\nb\nX\nc\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("a\nb\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("X\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("c\n")},
+ },
+ },
+ {
+ name: "replacement without trailing newline",
+ oldInput: "first\nsecond",
+ newInput: "first\nsecond\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("first\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("second")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("second\n")},
+ },
+ },
+ {
+ name: "line replacement",
+ oldInput: "a\nb\nc\n",
+ newInput: "a\nB\nc\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("a\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("b\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("B\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("c\n")},
+ },
+ },
+ {
+ name: "swap adjacent lines",
+ oldInput: "A\nB\n",
+ newInput: "B\nA\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("A\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("B\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("A\n")},
+ },
+ },
+ {
+ name: "indentation change is a full line replacement",
+ oldInput: "func main() {\n\treturn\n}\n",
+ newInput: "func main() {\n return\n}\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("func main() {\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("\treturn\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte(" return\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("}\n")},
+ },
+ },
+ {
+ name: "commenting out lines",
+ oldInput: "code\n",
+ newInput: "// code\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("code\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("// code\n")},
+ },
+ },
+ {
+ name: "reducing repeating lines",
+ oldInput: "log\nlog\nlog\n",
+ newInput: "log\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("log\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("log\nlog\n")},
+ },
+ },
+ {
+ name: "expanding repeating lines",
+ oldInput: "tick\n",
+ newInput: "tick\ntick\ntick\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("tick\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("tick\ntick\n")},
+ },
+ },
+ {
+ name: "interleaved modifications",
+ oldInput: "keep\nchange\nkeep\nchange\n",
+ newInput: "keep\nfixed\nkeep\nfixed\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("keep\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("change\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("fixed\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("keep\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("change\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("fixed\n")},
+ },
+ },
+ {
+ name: "large common header and footer",
+ oldInput: "header\nheader\nheader\nOLD\nfooter\nfooter\n",
+ newInput: "header\nheader\nheader\nNEW\nfooter\nfooter\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("header\nheader\nheader\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("OLD\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("NEW\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("footer\nfooter\n")},
+ },
+ },
+ {
+ name: "completely different content",
+ oldInput: "apple\nbanana\n",
+ newInput: "cherry\ndate\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("apple\nbanana\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("cherry\ndate\n")},
+ },
+ },
+ {
+ name: "unicode and emoji changes",
+ oldInput: "Hello 🌍\nYay\n",
+ newInput: "Hello 🌎\nYay\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("Hello 🌍\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("Hello 🌎\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("Yay\n")},
+ },
+ },
+ {
+ name: "binary data with embedded newlines",
+ oldInput: "\x00\x01\n\x02\x03\n",
+ newInput: "\x00\x01\n\x02\xFF\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("\x00\x01\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("\x02\x03\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("\x02\xFF\n")},
+ },
+ },
+ {
+ name: "adding trailing newline to last line",
+ oldInput: "Line 1\nLine 2",
+ newInput: "Line 1\nLine 2\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("Line 1\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("Line 2")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("Line 2\n")},
+ },
+ },
+ {
+ name: "removing trailing newline",
+ oldInput: "A\nB\n",
+ newInput: "A\nB",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("A\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("B\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("B")},
+ },
+ },
+ {
+ name: "inserting blank lines",
+ oldInput: "A\nB\n",
+ newInput: "A\n\n\nB\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("A\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("\n\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("B\n")},
+ },
+ },
+ {
+ name: "collapsing blank lines",
+ oldInput: "A\n\n\n\nB\n",
+ newInput: "A\nB\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("A\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("\n\n\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("B\n")},
+ },
+ },
+ {
+ name: "case sensitivity check",
+ oldInput: "FOO\nbar\n",
+ newInput: "foo\nbar\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("FOO\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("foo\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("bar\n")},
+ },
+ },
+ {
+ name: "partial line match is full mismatch",
+ oldInput: "The quick brown fox\n",
+ newInput: "The quick brown fox jumps\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("The quick brown fox\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("The quick brown fox jumps\n")},
+ },
+ },
+ {
+ name: "inserting middle content",
+ oldInput: "Top\nBottom\n",
+ newInput: "Top\nMiddle\nBottom\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("Top\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("Middle\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("Bottom\n")},
+ },
+ },
+ {
+ name: "block move simulated",
+ oldInput: "BlockA\nBlockB\nBlockC\n",
+ newInput: "BlockA\nBlockC\nBlockB\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("BlockA\n")},
+ {Kind: LinesDiffChunkKindDeleted, Data: []byte("BlockB\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("BlockC\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("BlockB\n")},
+ },
+ },
+ {
+ name: "alternating additions",
+ oldInput: "A\nB\nC\n",
+ newInput: "A\n1\nB\n2\nC\n",
+ expected: []LinesDiffChunk{
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("A\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("1\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("B\n")},
+ {Kind: LinesDiffChunkKindAdded, Data: []byte("2\n")},
+ {Kind: LinesDiffChunkKindUnchanged, Data: []byte("C\n")},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ chunks, err := DiffLines([]byte(tt.oldInput), []byte(tt.newInput))
+ if err != nil {
+ t.Fatalf("DiffLines returned error: %v", err)
+ }
+
+ if len(chunks) != len(tt.expected) {
+ t.Fatalf("expected %d chunks, got %d: %s", len(tt.expected), len(chunks), formatChunks(chunks))
+ }
+
+ for i := range tt.expected {
+ if chunks[i].Kind != tt.expected[i].Kind {
+ t.Fatalf("chunk %d kind mismatch: got %v, want %v; chunks: %s", i, chunks[i].Kind, tt.expected[i].Kind, formatChunks(chunks))
+ }
+ if !bytes.Equal(chunks[i].Data, tt.expected[i].Data) {
+ t.Fatalf("chunk %d data mismatch: got %q, want %q; chunks: %s", i, string(chunks[i].Data), string(tt.expected[i].Data), formatChunks(chunks))
+ }
+ }
+ })
+ }
+}
+
+func formatChunks(chunks []LinesDiffChunk) string {
+ var b strings.Builder
+ b.WriteByte('[')
+ for i, chunk := range chunks {
+ if i > 0 {
+ b.WriteString(", ")
+ }
+ b.WriteString(chunkKindName(chunk.Kind))
+ b.WriteByte(':')
+ b.WriteString(strconv.Quote(string(chunk.Data)))
+ }
+ b.WriteByte(']')
+ return b.String()
+}
+
+func chunkKindName(kind LinesDiffChunkKind) string {
+ switch kind {
+ case LinesDiffChunkKindUnchanged:
+ return "U"
+ case LinesDiffChunkKindDeleted:
+ return "D"
+ case LinesDiffChunkKindAdded:
+ return "A"
+ default:
+ return "?"
+ }
+}
diff --git a/diffbytes/unsafe.go b/difflines/unsafe.go
index 79c215a2..6e7ac5fd 100644
--- a/diffbytes/unsafe.go
+++ b/difflines/unsafe.go
@@ -1,4 +1,4 @@
-package diffbytes
+package difflines
import "unsafe"