diff --git a/go.mod b/go.mod index 7982aca..0d0fe57 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.26 require ( go.followtheprocess.codes/hue v1.1.0 go.followtheprocess.codes/snapshot v0.9.1 - golang.org/x/tools v0.44.0 ) require ( + go.followtheprocess.codes/diff v0.1.1 go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/term v0.42.0 // indirect diff --git a/go.sum b/go.sum index 743f8b4..27394b3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +go.followtheprocess.codes/diff v0.1.1 h1:pQpnow+Uj39S4xxNC6FLLfsJnolVM5+a90EIo5QZN6s= +go.followtheprocess.codes/diff v0.1.1/go.mod h1:bDSZPC9CvkRr8HlOwjE1bl/8qFAmiA3LVtkThRnniis= go.followtheprocess.codes/hue v1.1.0 h1:bPq21YLdWxQ0ki4lIvXCYtgutaGaDUYaSIENDdrrlNQ= go.followtheprocess.codes/hue v1.1.0/go.mod h1:VnCeVmYESGmX7fZJSFs59u8G+5zseCwGdFiJGHCFg4o= go.followtheprocess.codes/snapshot v0.9.1 h1:q90k4ZsV4WNrJkAXo6gLqYLgE3RipnzSOXU5o5Moyts= diff --git a/internal/diff/chardiff_test.go b/internal/diff/chardiff_test.go deleted file mode 100644 index 852fcba..0000000 --- a/internal/diff/chardiff_test.go +++ /dev/null @@ -1,310 +0,0 @@ -package diff_test - -import ( - "strings" - "testing" - - "go.followtheprocess.codes/test/internal/diff" -) - -func TestCharDiff(t *testing.T) { - tests := []struct { - name string - removed []byte - added []byte - wantAllUnchanged bool // if true, expect all segments Changed:false (identical inputs) - wantHasChanged bool // if true, expect at least one Changed:true segment on either side - }{ - { - name: "identical lines - all segments unchanged", - removed: []byte("hello world\n"), - added: []byte("hello world\n"), - wantAllUnchanged: true, - }, - { - name: "completely different", - removed: []byte("abc\n"), - added: []byte("xyz\n"), - wantHasChanged: true, - }, - { - name: "prefix change", - removed: []byte("foobar\n"), - added: []byte("bazbar\n"), - wantHasChanged: true, - }, - { - name: "suffix change", - removed: []byte("hello world\n"), - added: []byte("hello earth\n"), - wantHasChanged: true, - }, - { - name: "middle change", - removed: []byte("hello world bye\n"), - added: []byte("hello earth bye\n"), - wantHasChanged: true, - }, - { - name: "unicode", - removed: []byte("héllo wörld\n"), - added: []byte("héllo earth\n"), - wantHasChanged: true, - }, - { - name: "empty removed", - removed: []byte(""), - added: []byte("new content\n"), - wantHasChanged: true, - }, - { - name: "empty added", - removed: []byte("old content\n"), - added: []byte(""), - wantHasChanged: true, - }, - { - name: "both empty", - removed: []byte(""), - added: []byte(""), - wantAllUnchanged: true, - }, - { - name: "trailing newline preserved", - removed: []byte("line\n"), - added: []byte("changed\n"), - wantHasChanged: true, - }, - { - // Regression: invalid UTF-8 on the added side must not corrupt the join invariant. - // \xe2 is the start of a 3-byte sequence with no continuation bytes. - name: "invalid UTF-8 in added side", - removed: []byte("0"), - added: []byte("\xe2"), - wantHasChanged: true, - }, - { - // Regression: invalid UTF-8 on the removed side. - name: "invalid UTF-8 in removed side", - removed: []byte("\xe2"), - added: []byte("0"), - wantHasChanged: true, - }, - { - // Regression: identical invalid UTF-8 bytes must produce all-unchanged segments. - // \x80 is a bare continuation byte — invalid UTF-8 on its own. - name: "identical invalid UTF-8", - removed: []byte("\x80"), - added: []byte("\x80"), - wantAllUnchanged: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := diff.CharDiff(tt.removed, tt.added) - - assertJoinInvariant(t, result, tt.removed, tt.added) - - if tt.wantAllUnchanged { - assertNoneChanged(t, result.Removed, "removed") - assertNoneChanged(t, result.Added, "added") - } - - if tt.wantHasChanged { - if !anyChanged(result.Removed) && !anyChanged(result.Added) { - t.Error("CharDiff on differing inputs should produce at least one Changed segment") - } - } - }) - } -} - -// TestCharDiffCapTriggers verifies the > 500 rune safety cap produces a single-segment fallback. -func TestCharDiffCapTriggers(t *testing.T) { - long := []byte(strings.Repeat("a", 501) + "\n") - other := []byte(strings.Repeat("b", 501) + "\n") - - result := diff.CharDiff(long, other) - - if len(result.Removed) != 1 { - t.Errorf("expected 1 removed segment for >500 rune input, got %d", len(result.Removed)) - } - - if len(result.Added) != 1 { - t.Errorf("expected 1 added segment for >500 rune input, got %d", len(result.Added)) - } - - if !result.Removed[0].Changed { - t.Error("expected removed fallback segment to be Changed:true") - } - - if !result.Added[0].Changed { - t.Error("expected added fallback segment to be Changed:true") - } -} - -// TestCharDiffNewlineNotHighlighted asserts that the trailing newline is never included in a -// Changed segment. A highlighted \n causes the ANSI background colour to bleed onto the next -// terminal line when rendered. -func TestCharDiffNewlineNotHighlighted(t *testing.T) { - tests := []struct { - name string - removed []byte - added []byte - }{ - { - name: "suffix added", - removed: []byte("hello\n"), - added: []byte("hello world\n"), - }, - { - name: "inline change with trailing newline", - removed: []byte("\treturn \"Hello, \" + name\n"), - added: []byte("\treturn \"Hello, \" + name + \"!\"\n"), - }, - { - name: "completely different lines", - removed: []byte("abc\n"), - added: []byte("xyz\n"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := diff.CharDiff(tt.removed, tt.added) - - for i, seg := range result.Removed { - if seg.Changed && len(seg.Text) > 0 && seg.Text[len(seg.Text)-1] == '\n' { - t.Errorf("removed segment[%d] is Changed=true but ends with \\n; highlight would bleed onto next terminal line", i) - } - } - - for i, seg := range result.Added { - if seg.Changed && len(seg.Text) > 0 && seg.Text[len(seg.Text)-1] == '\n' { - t.Errorf("added segment[%d] is Changed=true but ends with \\n; highlight would bleed onto next terminal line", i) - } - } - }) - } -} - -// TestCharDiffNotSameForDifferent verifies that differing inputs produce at least one Changed segment. -func TestCharDiffNotSameForDifferent(t *testing.T) { - result := diff.CharDiff([]byte("hello\n"), []byte("world\n")) - - hasChanged := false - - for _, seg := range result.Removed { - if seg.Changed { - hasChanged = true - break - } - } - - if !hasChanged { - for _, seg := range result.Added { - if seg.Changed { - hasChanged = true - break - } - } - } - - if !hasChanged { - t.Error("CharDiff on different inputs should produce at least one Changed segment") - } -} - -// BenchmarkCharDiff benchmarks the CharDiff function. -func BenchmarkCharDiff(b *testing.B) { - removed := []byte("the quick brown fox jumps over the lazy dog\n") - added := []byte("the quick brown cat jumps over the lazy frog\n") - - b.ResetTimer() - - for b.Loop() { - diff.CharDiff(removed, added) - } -} - -// FuzzCharDiff verifies CharDiff never panics, terminates, and produces segments -// whose concatenation equals the original input on each side. -func FuzzCharDiff(f *testing.F) { - f.Add([]byte("hello\n"), []byte("hello\n")) - f.Add([]byte("a\n"), []byte("b\n")) - f.Add([]byte("the quick brown fox\n"), []byte("the quick brown cat\n")) - f.Add([]byte(strings.Repeat("a", 10)+"\n"), []byte(strings.Repeat("b", 10)+"\n")) - f.Add([]byte("héllo\n"), []byte("wörld\n")) - f.Add([]byte(""), []byte("content\n")) - f.Add([]byte("content\n"), []byte("")) - - f.Fuzz(func(t *testing.T, removed, added []byte) { - result := diff.CharDiff(removed, added) - - // Invariant: segments must join back to original input. - if joinSegments(result.Removed) != string(removed) { - t.Fatalf("removed segments join = %q, want %q", joinSegments(result.Removed), string(removed)) - } - - if joinSegments(result.Added) != string(added) { - t.Fatalf("added segments join = %q, want %q", joinSegments(result.Added), string(added)) - } - - // Invariant: identical inputs → all segments Changed:false. - if string(removed) == string(added) { - for i, seg := range result.Removed { - if seg.Changed { - t.Fatalf("removed segment[%d] Changed=true for identical inputs", i) - } - } - - for i, seg := range result.Added { - if seg.Changed { - t.Fatalf("added segment[%d] Changed=true for identical inputs", i) - } - } - } - }) -} - -func assertJoinInvariant(t *testing.T, result diff.InlineChange, removed, added []byte) { - t.Helper() - - if got := joinSegments(result.Removed); got != string(removed) { - t.Errorf("removed segments join = %q, want %q", got, string(removed)) - } - - if got := joinSegments(result.Added); got != string(added) { - t.Errorf("added segments join = %q, want %q", got, string(added)) - } -} - -func assertNoneChanged(t *testing.T, segs []diff.Segment, side string) { - t.Helper() - - for i, seg := range segs { - if seg.Changed { - t.Errorf("%s segment[%d] Changed=true, want false for identical inputs", side, i) - } - } -} - -func anyChanged(segs []diff.Segment) bool { - for _, s := range segs { - if s.Changed { - return true - } - } - - return false -} - -func joinSegments(segs []diff.Segment) string { - var sb strings.Builder - for _, s := range segs { - sb.Write(s.Text) - } - - return sb.String() -} diff --git a/internal/diff/chars.go b/internal/diff/chars.go deleted file mode 100644 index c3f0498..0000000 --- a/internal/diff/chars.go +++ /dev/null @@ -1,235 +0,0 @@ -package diff - -import ( - "bytes" - "unicode/utf8" -) - -// Segment is a contiguous run of text tagged as equal or changed. -type Segment struct { - Text []byte - Changed bool -} - -// InlineChange holds the char-level breakdown for a removed/added line pair. -type InlineChange struct { - Removed []Segment - Added []Segment -} - -// CharDiff computes character-level segments for a removed/added line pair. -// Uses O(mn) LCS on []rune — acceptable since individual lines are short. -// Strips and restores trailing \n before diffing. -// If either side exceeds 500 runes, returns a single-segment fallback (whole-line). -func CharDiff(removed, added []byte) InlineChange { - // Identical inputs: short-circuit before any UTF-8 handling. This also - // covers identical invalid UTF-8, where fallback would wrongly return - // Changed:true segments. - if bytes.Equal(removed, added) { - seg := Segment{Text: append([]byte(nil), removed...), Changed: false} - - return InlineChange{ - Removed: []Segment{seg}, - Added: []Segment{{Text: append([]byte(nil), added...), Changed: false}}, - } - } - - // Strip trailing newline, remember whether each side had one. - removedNL := len(removed) > 0 && removed[len(removed)-1] == '\n' - addedNL := len(added) > 0 && added[len(added)-1] == '\n' - - removedCore := removed - if removedNL { - removedCore = removed[:len(removed)-1] - } - - addedCore := added - if addedNL { - addedCore = added[:len(added)-1] - } - - // Fall back to whole-line diff for invalid UTF-8: converting invalid bytes - // to runes replaces them with U+FFFD, making it impossible to reconstruct - // the original bytes from the segments. - if !utf8.Valid(removedCore) || !utf8.Valid(addedCore) { - return fallback(removed, added) - } - - oldRunes := []rune(string(removedCore)) - newRunes := []rune(string(addedCore)) - - // Safety cap: avoid O(mn) on large inputs (minified/generated content). - if len(oldRunes) > 500 || len(newRunes) > 500 { - return fallback(removed, added) - } - - segs := lcsSegments(oldRunes, newRunes) - - removedSegs := reattachNL(segs.removed, removedNL) - addedSegs := reattachNL(segs.added, addedNL) - - return InlineChange{Removed: removedSegs, Added: addedSegs} -} - -// fallback returns a single Changed segment per side (whole-line fallback). -// Copies the input slices to avoid aliasing the caller's memory. -func fallback(removed, added []byte) InlineChange { - result := InlineChange{} - - if len(removed) > 0 { - cp := make([]byte, len(removed)) - copy(cp, removed) - result.Removed = []Segment{{Text: cp, Changed: true}} - } - - if len(added) > 0 { - cp := make([]byte, len(added)) - copy(cp, added) - result.Added = []Segment{{Text: cp, Changed: true}} - } - - return result -} - -// reattachNL returns a new segment slice with a newline reattached after the -// last segment. The input slice is not modified. -// -// If the last segment is Changed, the newline is appended as a separate -// unchanged segment rather than being included in the highlight span — a -// highlighted \n causes the ANSI background colour to bleed onto the next -// terminal line. -func reattachNL(segs []Segment, hadNL bool) []Segment { - if !hadNL || len(segs) == 0 { - return segs - } - - last := segs[len(segs)-1] - - if last.Changed { - result := make([]Segment, 0, len(segs)+1) - result = append(result, segs...) - - return append(result, Segment{Text: []byte{'\n'}, Changed: false}) - } - - result := make([]Segment, len(segs)) - copy(result, segs) - - last.Text = append(append([]byte(nil), last.Text...), '\n') - result[len(result)-1] = last - - return result -} - -// sides holds parallel removed/added segment lists built during backtracking. -type sides struct { - removed []Segment - added []Segment -} - -// op is a single edit operation produced during LCS backtracking. -type op struct { - r rune - kind byte // 'e' equal, 'd' delete, 'i' insert -} - -// lcsSegments builds the LCS table and backtracks to produce Equal/Delete/Insert -// edit operations, then merges consecutive same-kind ops into Segment runs. -func lcsSegments(old, newText []rune) sides { - m, n := len(old), len(newText) - - // Build LCS DP table using a flat slice to avoid m+1 separate allocations. - // dp[i*(n+1)+j] = length of LCS of old[:i] and newText[:j]. - dp := make([]int, (m+1)*(n+1)) - - stride := n + 1 - for i := 1; i <= m; i++ { - for j := 1; j <= n; j++ { - if old[i-1] == newText[j-1] { - dp[i*stride+j] = dp[(i-1)*stride+(j-1)] + 1 - } else { - dp[i*stride+j] = max(dp[(i-1)*stride+j], dp[i*stride+(j-1)]) - } - } - } - - // Backtrack to build edit ops, then reverse. - ops := make([]op, 0, m+n) - - i, j := m, n - for i > 0 || j > 0 { - switch { - case i > 0 && j > 0 && old[i-1] == newText[j-1]: - ops = append(ops, op{r: old[i-1], kind: 'e'}) - i-- - j-- - case j > 0 && (i == 0 || dp[i*stride+(j-1)] >= dp[(i-1)*stride+j]): - ops = append(ops, op{r: newText[j-1], kind: 'i'}) - j-- - default: - ops = append(ops, op{r: old[i-1], kind: 'd'}) - i-- - } - } - - // Reverse ops (they were built backwards). - for l, r := 0, len(ops)-1; l < r; l, r = l+1, r-1 { - ops[l], ops[r] = ops[r], ops[l] - } - - return mergeSegments(ops) -} - -// mergeSegments merges consecutive same-kind ops into Segment runs. -func mergeSegments(ops []op) sides { - // Pre-allocate with a reasonable capacity to reduce re-allocations. - removedSegs := make([]Segment, 0, 8) - addedSegs := make([]Segment, 0, 8) - - var buf [utf8.UTFMax]byte - for _, o := range ops { - nb := utf8.EncodeRune(buf[:], o.r) - r := buf[:nb] - - switch o.kind { - case 'e': - if len(removedSegs) > 0 && !removedSegs[len(removedSegs)-1].Changed { - removedSegs[len(removedSegs)-1].Text = append(removedSegs[len(removedSegs)-1].Text, r...) - } else { - removedSegs = append(removedSegs, Segment{Text: append([]byte(nil), r...), Changed: false}) - } - - if len(addedSegs) > 0 && !addedSegs[len(addedSegs)-1].Changed { - addedSegs[len(addedSegs)-1].Text = append(addedSegs[len(addedSegs)-1].Text, r...) - } else { - addedSegs = append(addedSegs, Segment{Text: append([]byte(nil), r...), Changed: false}) - } - case 'd': - if len(removedSegs) > 0 && removedSegs[len(removedSegs)-1].Changed { - removedSegs[len(removedSegs)-1].Text = append(removedSegs[len(removedSegs)-1].Text, r...) - } else { - removedSegs = append(removedSegs, Segment{Text: append([]byte(nil), r...), Changed: true}) - } - case 'i': - if len(addedSegs) > 0 && addedSegs[len(addedSegs)-1].Changed { - addedSegs[len(addedSegs)-1].Text = append(addedSegs[len(addedSegs)-1].Text, r...) - } else { - addedSegs = append(addedSegs, Segment{Text: append([]byte(nil), r...), Changed: true}) - } - default: - // op.kind is always 'e', 'd', or 'i' — lcsSegments is the only producer. - } - } - - // Handle empty inputs: produce a single unchanged empty segment so callers - // always have at least one segment to work with. - if len(removedSegs) == 0 { - removedSegs = append(removedSegs, Segment{Text: []byte{}, Changed: false}) - } - - if len(addedSegs) == 0 { - addedSegs = append(addedSegs, Segment{Text: []byte{}, Changed: false}) - } - - return sides{removed: removedSegs, added: addedSegs} -} diff --git a/internal/diff/diff.go b/internal/diff/diff.go deleted file mode 100644 index 13ffbd0..0000000 --- a/internal/diff/diff.go +++ /dev/null @@ -1,370 +0,0 @@ -// Package diff originally derived from Go's internal/diff, but has since been -// substantially extended to support structured line types, character-level inline -// diff highlighting, and colourised terminal rendering. -package diff - -import ( - "bytes" - "fmt" - "sort" - "strings" -) - -// LineKind identifies the role of a line in a diff. -type LineKind int - -const ( - KindContext LineKind = iota // unchanged context line - KindRemoved // line present only in old - KindAdded // line present only in new - KindHeader // "diff …", "--- …", "+++ …", "@@ … @@" -) - -// String returns the name of the LineKind constant, suitable for use in test failure messages. -func (k LineKind) String() string { - switch k { - case KindContext: - return "KindContext" - case KindRemoved: - return "KindRemoved" - case KindAdded: - return "KindAdded" - case KindHeader: - return "KindHeader" - default: - return fmt.Sprintf("LineKind(%d)", int(k)) - } -} - -// Line is a single structured line from a diff. -// Content holds the line text WITHOUT the leading diff prefix ("- "/"+ "/" "). -// For KindHeader lines, Content holds the full raw line (including its newline). -type Line struct { - Content []byte - Kind LineKind -} - -// A pair is a pair of values tracked for both the x and y side of a diff. -// It is typically a pair of line indexes. -type pair struct{ x, y int } - -// Lines returns the structured diff lines for old and newText. -// Returns nil if old and newText are identical. -// Uses the same anchored diff algorithm as [Diff]. -func Lines(oldName string, old []byte, newName string, newText []byte) []Line { - if bytes.Equal(old, newText) { - return nil - } - - return computeLines(oldName, old, newName, newText) -} - -// Diff returns an anchored diff of the two texts old and new -// in the "unified diff" format. If old and new are identical, -// Diff returns a nil slice (no output). -// -// Unix diff implementations typically look for a diff with -// the smallest number of lines inserted and removed, -// which can in the worst case take time quadratic in the -// number of lines in the texts. As a result, many implementations -// either can be made to run for a long time or cut off the search -// after a predetermined amount of work. -// -// In contrast, this implementation looks for a diff with the -// smallest number of "unique" lines inserted and removed, -// where unique means a line that appears just once in both old and new. -// We call this an "anchored diff" because the unique lines anchor -// the chosen matching regions. An anchored diff is usually clearer -// than a standard diff, because the algorithm does not try to -// reuse unrelated blank lines or closing braces. -// The algorithm also guarantees to run in O(n log n) time -// instead of the standard O(n²) time. -// -// Some systems call this approach a "patience diff," named for -// the "patience sorting" algorithm, itself named for a solitaire card game. -// We avoid that name for two reasons. First, the name has been used -// for a few different variants of the algorithm, so it is imprecise. -// Second, the name is frequently interpreted as meaning that you have -// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm, -// when in fact the algorithm is faster than the standard one. -func Diff( - oldName string, - old []byte, - newName string, - newText []byte, -) []byte { - if bytes.Equal(old, newText) { - return nil - } - - structured := computeLines(oldName, old, newName, newText) - - var out bytes.Buffer - - for _, line := range structured { - switch line.Kind { - case KindHeader: - out.Write(line.Content) - case KindRemoved: - out.WriteString("- ") - out.Write(line.Content) - case KindAdded: - out.WriteString("+ ") - out.Write(line.Content) - case KindContext: - out.WriteString(" ") - out.Write(line.Content) - default: - // no action for unknown line kinds - } - } - - return out.Bytes() -} - -// computeLines computes structured diff lines for old and newText (assumed non-equal). -func computeLines(oldName string, old []byte, newName string, newText []byte) []Line { - x := splitLines(old) - y := splitLines(newText) - - var result []Line - - result = append(result, - Line{Kind: KindHeader, Content: fmt.Appendf(nil, "diff %s %s\n", oldName, newName)}, - Line{Kind: KindHeader, Content: fmt.Appendf(nil, "--- %s\n", oldName)}, - Line{Kind: KindHeader, Content: fmt.Appendf(nil, "+++ %s\n", newName)}, - ) - - // Loop over matches to consider, - // expanding each match to include surrounding lines, - // and then printing diff chunks. - // To avoid setup/teardown cases outside the loop, - // tgs returns a leading {0,0} and trailing {len(x), len(y)} pair - // in the sequence of matches. - var ( - done pair // printed up to x[:done.x] and y[:done.y] - chunk pair // start lines of current chunk - count pair // number of lines from each side in current chunk - ctext []Line // lines for current chunk - ) - - // contextLines is the number of unchanged lines to show around each diff hunk. - const contextLines = 3 - - for _, m := range tgs(x, y) { - if m.x < done.x { - // Already handled scanning forward from earlier match. - continue - } - - start, end := expandMatch(m, done, x, y) - - // Emit the mismatched lines before start into this chunk. - // (No effect on first sentinel iteration, when start = {0,0}.) - for _, s := range x[done.x:start.x] { - ctext = append(ctext, Line{Kind: KindRemoved, Content: []byte(s)}) - count.x++ - } - - for _, s := range y[done.y:start.y] { - ctext = append(ctext, Line{Kind: KindAdded, Content: []byte(s)}) - count.y++ - } - - // If we're not at EOF and have too few common lines, - // the chunk includes all the common lines and continues. - if (end.x < len(x) || end.y < len(y)) && - (end.x-start.x < contextLines || (len(ctext) > 0 && end.x-start.x < 2*contextLines)) { - for _, s := range x[start.x:end.x] { - ctext = append(ctext, Line{Kind: KindContext, Content: []byte(s)}) - count.x++ - count.y++ - } - - done = end - - continue - } - - // End chunk with common lines for context. - if len(ctext) > 0 { - n := min(end.x-start.x, contextLines) - - for _, s := range x[start.x : start.x+n] { - ctext = append(ctext, Line{Kind: KindContext, Content: []byte(s)}) - count.x++ - count.y++ - } - - done = pair{start.x + n, start.y + n} - - result = append(result, chunkHeader(chunk, count)) - result = append(result, ctext...) - - count.x = 0 - count.y = 0 - ctext = ctext[:0] - } - - // If we reached EOF, we're done. - if end.x >= len(x) && end.y >= len(y) { - break - } - - // Otherwise start a new chunk. - chunk = pair{end.x - contextLines, end.y - contextLines} - for _, s := range x[chunk.x:end.x] { - ctext = append(ctext, Line{Kind: KindContext, Content: []byte(s)}) - count.x++ - count.y++ - } - - done = end - } - - return result -} - -// expandMatch expands a match region backward to start and forward to end -// while adjacent lines in x and y also match. -func expandMatch(m, done pair, x, y []string) (start, end pair) { - start = m - for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] { - start.x-- - start.y-- - } - - end = m - for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] { - end.x++ - end.y++ - } - - return start, end -} - -// chunkHeader formats the @@ header line for a diff chunk. -// chunk is the 0-indexed start of the chunk; count is the number of lines on each side. -func chunkHeader(chunk, count pair) Line { - x, y := chunk.x, chunk.y - if count.x > 0 { - x++ - } - - if count.y > 0 { - y++ - } - - return Line{ - Kind: KindHeader, - Content: fmt.Appendf(nil, "@@ -%d,%d +%d,%d @@\n", x, count.x, y, count.y), - } -} - -// splitLines returns the lines in the file x, including newlines. -// If the file does not end in a newline, one is supplied -// along with a warning about the missing newline. -func splitLines(x []byte) []string { - l := strings.SplitAfter(string(x), "\n") - if l[len(l)-1] == "" { - l = l[:len(l)-1] - } else { - // Treat last line as having a message about the missing newline attached, - // using the same text as BSD/GNU diff (including the leading backslash). - l[len(l)-1] += "\n\\ No newline at end of file\n" - } - - return l -} - -// tgs returns the pairs of indexes of the longest common subsequence -// of unique lines in x and y, where a unique line is one that appears -// once in x and once in y. -// -// The longest common subsequence algorithm is as described in -// Thomas G. Szymanski, "A Special Case of the Maximal Common -// Subsequence Problem," Princeton TR #170 (January 1975), -// available at https://research.swtch.com/tgs170.pdf. -func tgs(x, y []string) []pair { - // Count the number of times each string appears in a and b. - // We only care about 0, 1, many, counted as 0, -1, -2 - // for the x side and 0, -4, -8 for the y side. - // Using negative numbers now lets us distinguish positive line numbers later. - m := make(map[string]int) - for _, s := range x { - if c := m[s]; c > -2 { - m[s] = c - 1 - } - } - - for _, s := range y { - if c := m[s]; c > -8 { - m[s] = c - 4 - } - } - - // Now unique strings can be identified by m[s] = -1+-4. - // - // Gather the indexes of those strings in x and y, building: - // xi[i] = increasing indexes of unique strings in x. - // yi[i] = increasing indexes of unique strings in y. - // inv[i] = index j such that x[xi[i]] = y[yi[j]]. - var xi, yi, inv []int - - for i, s := range y { - if m[s] == -1+-4 { - m[s] = len(yi) - yi = append(yi, i) - } - } - - for i, s := range x { - if j, ok := m[s]; ok && j >= 0 { - xi = append(xi, i) - inv = append(inv, j) - } - } - - // Apply Algorithm A from Szymanski's paper. - // In those terms, A = J = inv and B = [0, n). - // We add sentinel pairs {0,0}, and {len(x),len(y)} - // to the returned sequence, to help the processing loop. - j := inv - n := len(xi) - tails := make([]int, n) - lengths := make([]int, n) - - for i := range tails { - tails[i] = n + 1 - } - - for i := range n { - k := sort.Search(n, func(k int) bool { - return tails[k] >= j[i] - }) - tails[k] = j[i] - lengths[i] = k + 1 - } - - k := 0 - for _, v := range lengths { - if k < v { - k = v - } - } - - seq := make([]pair, 2+k) - seq[1+k] = pair{len(x), len(y)} // sentinel at end - - lastj := n - for i := n - 1; i >= 0; i-- { - if lengths[i] == k && j[i] < lastj { - seq[k] = pair{xi[i], yi[j[i]]} - k-- - } - } - - seq[0] = pair{0, 0} // sentinel at start - - return seq -} diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go deleted file mode 100644 index 10bed06..0000000 --- a/internal/diff/diff_test.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package diff_test - -import ( - "bytes" - "os" - "path/filepath" - "testing" - - "go.followtheprocess.codes/test/internal/diff" - "golang.org/x/tools/txtar" -) - -// TestLines verifies the Lines function returns structured diff lines. -func TestLines(t *testing.T) { - tests := []struct { - name string - old []byte - newText []byte - oldName string - newName string - want []diff.Line // nil means we expect nil (inputs equal) - }{ - { - name: "nil on equal inputs", - oldName: "a", newName: "b", - old: []byte("same\n"), - newText: []byte("same\n"), - want: nil, - }, - { - name: "basic add and remove", - oldName: "want", newName: "got", - old: []byte("hello\nworld\n"), - newText: []byte("hello\nearth\n"), - want: []diff.Line{ - {Kind: diff.KindHeader, Content: []byte("diff want got\n")}, - {Kind: diff.KindHeader, Content: []byte("--- want\n")}, - {Kind: diff.KindHeader, Content: []byte("+++ got\n")}, - {Kind: diff.KindHeader, Content: []byte("@@ -1,2 +1,2 @@\n")}, - {Kind: diff.KindContext, Content: []byte("hello\n")}, - {Kind: diff.KindRemoved, Content: []byte("world\n")}, - {Kind: diff.KindAdded, Content: []byte("earth\n")}, - }, - }, - { - name: "all added", - oldName: "want", newName: "got", - old: []byte(""), - newText: []byte("new line\n"), - want: []diff.Line{ - {Kind: diff.KindHeader, Content: []byte("diff want got\n")}, - {Kind: diff.KindHeader, Content: []byte("--- want\n")}, - {Kind: diff.KindHeader, Content: []byte("+++ got\n")}, - {Kind: diff.KindHeader, Content: []byte("@@ -0,0 +1,1 @@\n")}, - {Kind: diff.KindAdded, Content: []byte("new line\n")}, - }, - }, - { - name: "all removed", - oldName: "want", newName: "got", - old: []byte("old line\n"), - newText: []byte(""), - want: []diff.Line{ - {Kind: diff.KindHeader, Content: []byte("diff want got\n")}, - {Kind: diff.KindHeader, Content: []byte("--- want\n")}, - {Kind: diff.KindHeader, Content: []byte("+++ got\n")}, - {Kind: diff.KindHeader, Content: []byte("@@ -1,1 +0,0 @@\n")}, - {Kind: diff.KindRemoved, Content: []byte("old line\n")}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := diff.Lines(tt.oldName, tt.old, tt.newName, tt.newText) - if tt.want == nil { - if got != nil { - t.Fatalf("Lines() = %v, want nil", got) - } - - return - } - - if len(got) != len(tt.want) { - t.Fatalf("Lines() returned %d lines, want %d\ngot: %#v\nwant: %#v", len(got), len(tt.want), got, tt.want) - } - - for i, line := range got { - if line.Kind != tt.want[i].Kind { - t.Errorf("line[%d].Kind = %v, want %v", i, line.Kind, tt.want[i].Kind) - } - - if !bytes.Equal(line.Content, tt.want[i].Content) { - t.Errorf("line[%d].Content = %q, want %q", i, line.Content, tt.want[i].Content) - } - } - }) - } -} - -// BenchmarkLines benchmarks the Lines function using long.txtar as realistic input. -func BenchmarkLines(b *testing.B) { - contents, err := os.ReadFile(filepath.Join("testdata", "long.txtar")) - if err != nil { - b.Fatalf("could not read long.txtar: %v", err) - } - - archive := txtar.Parse(contents) - old := clean(archive.Files[0].Data) - newContent := clean(archive.Files[1].Data) - - b.ResetTimer() - - for b.Loop() { - diff.Lines(archive.Files[0].Name, old, archive.Files[1].Name, newContent) - } -} - -// BenchmarkDiff benchmarks the Diff function using long.txtar as realistic input. -func BenchmarkDiff(b *testing.B) { - contents, err := os.ReadFile(filepath.Join("testdata", "long.txtar")) - if err != nil { - b.Fatalf("could not read long.txtar: %v", err) - } - - archive := txtar.Parse(contents) - old := clean(archive.Files[0].Data) - newContent := clean(archive.Files[1].Data) - - b.ResetTimer() - - for b.Loop() { - diff.Diff(archive.Files[0].Name, old, archive.Files[1].Name, newContent) - } -} - -// FuzzLines verifies Lines() never panics and returns nil iff inputs are equal. -func FuzzLines(f *testing.F) { - f.Add([]byte(""), []byte("")) - f.Add([]byte("same\n"), []byte("same\n")) - f.Add([]byte("hello\nworld\n"), []byte("hello\nearth\n")) - f.Add([]byte("completely different\n"), []byte("nothing in common\n")) - f.Add([]byte("a\nb\nc\n"), []byte("a\nd\nc\n")) - f.Add([]byte("unicode: héllo\n"), []byte("unicode: wörld\n")) - f.Add([]byte(" \n\t\n"), []byte(" \n\t\n")) - - f.Fuzz(func(t *testing.T, old, newContent []byte) { - result := diff.Lines("a", old, "b", newContent) - if bytes.Equal(old, newContent) { - if result != nil { - t.Fatal("Lines() = non-nil for equal inputs") - } - } else { - if result == nil { - t.Fatal("Lines() = nil for non-equal inputs") - } - } - }) -} - -func clean(text []byte) []byte { - text = bytes.ReplaceAll(text, []byte("$\n"), []byte("\n")) - text = bytes.TrimSuffix(text, []byte("^D\n")) - - return text -} - -func Test(t *testing.T) { - files, err := filepath.Glob(filepath.Join("testdata", "*.txtar")) - if err != nil { - t.Fatalf("could not glob txtar files: %v", err) - } - - if len(files) == 0 { - t.Fatal("no testdata") - } - - for _, file := range files { - t.Run(filepath.Base(file), func(t *testing.T) { - contents, err := os.ReadFile(file) - if err != nil { - t.Fatalf("could not read %s: %v", file, err) - } - // Stupid windows - contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n")) - - archive := txtar.Parse(contents) - if len(archive.Files) != 3 || archive.Files[2].Name != "diff" { - t.Fatalf("%s: want three files, third named \"diff\", got: %v", file, archive.Files) - } - - diffs := diff.Diff( - archive.Files[0].Name, - clean(archive.Files[0].Data), - archive.Files[1].Name, - clean(archive.Files[1].Data), - ) - want := clean(archive.Files[2].Data) - - if !bytes.Equal(diffs, want) { - t.Fatalf("%s: have:\n%s\nwant:\n%s\n%s", file, - diffs, want, diff.Diff("have", diffs, "want", want)) - } - }) - } -} diff --git a/internal/diff/render.go b/internal/diff/render.go deleted file mode 100644 index b94eae3..0000000 --- a/internal/diff/render.go +++ /dev/null @@ -1,129 +0,0 @@ -package diff - -import ( - "go.followtheprocess.codes/hue" -) - -const ( - styleHeaderBold = hue.Bold - styleRemovedHeader = hue.Red - styleAddedHeader = hue.Green - styleRemovedLine = hue.Red - styleAddedLine = hue.Green - styleRemovedHighlight = hue.Black | hue.Bold | hue.RedBackground - styleAddedHighlight = hue.Black | hue.Bold | hue.GreenBackground -) - -// Render formats a []Line as a colourised string suitable for terminal output. -// Returns an empty string if lines is nil or empty. -func Render(lines []Line) string { - if len(lines) == 0 { - return "" - } - - var buf []byte - - i := 0 - for i < len(lines) { - line := lines[i] - - switch line.Kind { - case KindHeader: - switch { - case len(line.Content) >= 3 && string(line.Content[:3]) == "---": - buf = styleRemovedHeader.AppendText(buf, line.Content) - case len(line.Content) >= 3 && string(line.Content[:3]) == "+++": - buf = styleAddedHeader.AppendText(buf, line.Content) - default: - buf = styleHeaderBold.AppendText(buf, line.Content) - } - - i++ - - case KindContext: - buf = append(buf, ' ', ' ') - buf = append(buf, line.Content...) - i++ - - case KindRemoved: - // Collect the full consecutive run of removed lines, then any trailing added lines. - start := i - for i < len(lines) && lines[i].Kind == KindRemoved { - i++ - } - - removedEnd := i - for i < len(lines) && lines[i].Kind == KindAdded { - i++ - } - - removed := lines[start:removedEnd] - added := lines[removedEnd:i] - - if len(removed) == len(added) { - buf = renderInlinePairs(buf, removed, added) - } else { - buf = renderWholeLine(buf, removed, added) - } - - case KindAdded: - // Standalone added block — no preceding removed lines in this hunk. - start := i - for i < len(lines) && lines[i].Kind == KindAdded { - i++ - } - - buf = renderWholeLine(buf, nil, lines[start:i]) - - default: - // no action for unknown line kinds - i++ - } - } - - return string(buf) -} - -// renderInlinePairs renders 1:1 paired removed/added lines with character-level inline diff. -func renderInlinePairs(buf []byte, removed, added []Line) []byte { - for k := range removed { - ic := CharDiff(removed[k].Content, added[k].Content) - - buf = styleRemovedLine.AppendText(buf, []byte("- ")) - - for _, seg := range ic.Removed { - if seg.Changed { - buf = styleRemovedHighlight.AppendText(buf, seg.Text) - } else { - buf = styleRemovedLine.AppendText(buf, seg.Text) - } - } - - buf = styleAddedLine.AppendText(buf, []byte("+ ")) - - for _, seg := range ic.Added { - if seg.Changed { - buf = styleAddedHighlight.AppendText(buf, seg.Text) - } else { - buf = styleAddedLine.AppendText(buf, seg.Text) - } - } - } - - return buf -} - -// renderWholeLine renders removed/added lines with whole-line colour (no inline diff). -func renderWholeLine(buf []byte, removed, added []Line) []byte { - for _, r := range removed { - buf = styleRemovedLine.AppendText(buf, []byte("- ")) - buf = styleRemovedLine.AppendText(buf, r.Content) - } - - for _, a := range added { - buf = styleAddedLine.AppendText(buf, []byte("+ ")) - buf = styleAddedLine.AppendText(buf, a.Content) - } - - return buf -} diff --git a/internal/diff/render_test.go b/internal/diff/render_test.go deleted file mode 100644 index fdd59c8..0000000 --- a/internal/diff/render_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package diff_test - -import ( - "flag" - "os" - "path/filepath" - "testing" - - "go.followtheprocess.codes/hue" - "go.followtheprocess.codes/test/internal/diff" -) - -var update = flag.Bool("update", false, "update golden files") - -func TestMain(m *testing.M) { - // Force colour on so rendered output is predictable in tests. - hue.Enabled(true) - m.Run() -} - -func TestRender(t *testing.T) { - tests := []struct { - name string - lines []diff.Line - }{ - { - name: "nil input returns empty string", - lines: nil, - }, - { - name: "empty slice returns empty string", - lines: []diff.Line{}, - }, - { - name: "context line has no colour and double-space prefix", - lines: []diff.Line{ - {Kind: diff.KindContext, Content: []byte("unchanged\n")}, - }, - }, - { - name: "diff header line is bold no colour", - lines: []diff.Line{ - {Kind: diff.KindHeader, Content: []byte("diff want got\n")}, - }, - }, - { - name: "removed header line is red", - lines: []diff.Line{ - {Kind: diff.KindHeader, Content: []byte("--- want\n")}, - }, - }, - { - name: "added header line is green", - lines: []diff.Line{ - {Kind: diff.KindHeader, Content: []byte("+++ got\n")}, - }, - }, - { - name: "hunk header is bold no colour", - lines: []diff.Line{ - {Kind: diff.KindHeader, Content: []byte("@@ -1,1 +1,1 @@\n")}, - }, - }, - { - name: "standalone removed line has red prefix and content", - lines: []diff.Line{ - {Kind: diff.KindRemoved, Content: []byte("old line\n")}, - }, - }, - { - name: "standalone added line has green prefix and content", - lines: []diff.Line{ - {Kind: diff.KindAdded, Content: []byte("new line\n")}, - }, - }, - { - // "old" and "new" share no characters, so CharDiff produces a single - // Changed segment per side with the \n reattached as a separate unchanged segment. - name: "matched removed added pair uses inline char diff with coloured prefixes", - lines: []diff.Line{ - {Kind: diff.KindRemoved, Content: []byte("old\n")}, - {Kind: diff.KindAdded, Content: []byte("new\n")}, - }, - }, - { - name: "mismatched count uses whole-line colour with coloured prefixes", - lines: []diff.Line{ - {Kind: diff.KindRemoved, Content: []byte("line one\n")}, - {Kind: diff.KindRemoved, Content: []byte("line two\n")}, - {Kind: diff.KindAdded, Content: []byte("replacement\n")}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := diff.Render(tt.lines) - golden := filepath.Join("testdata", filepath.FromSlash(t.Name())+".txt") - - if *update { - err := os.MkdirAll(filepath.Dir(golden), 0o755) - if err != nil { - t.Fatalf("create golden dir: %v", err) - } - - err = os.WriteFile(golden, []byte(got), 0o644) - if err != nil { - t.Fatalf("update golden: %v", err) - } - - return - } - - want, err := os.ReadFile(golden) - if err != nil { - t.Fatalf("read golden: %v", err) - } - - if got != string(want) { - t.Errorf("Render() =\n%q\nwant\n%q", got, string(want)) - } - }) - } -} - -// TestVisualDiff is a manual smoke-check for the diff renderer. -// Run with go test -v to see the colourised output in your terminal. -func TestVisualDiff(t *testing.T) { - // TestMain enables colour, so all rendering below is colourised. - scenarios := []struct { - name string - old string - new string - }{ - { - // Single changed line: char-level inline highlighting should show - // exactly which characters differ. - name: "single line change (inline char diff)", - old: `func greet(name string) string { - return "Hello, " + name -} -`, - new: `func greet(name string) string { - return "Hello, " + name + "!" -} -`, - }, - { - // Two changed lines paired 1:1: each pair gets its own inline diff. - name: "multi-line paired change", - old: `func (s *Server) Start(port int) error { - addr := fmt.Sprintf("0.0.0.0:%d", port) - return http.ListenAndServe(addr, s.mux) -} -`, - new: `func (s *Server) Start(ctx context.Context, port int) error { - addr := fmt.Sprintf(":%d", port) - return s.httpServer.ListenAndServeContext(ctx, addr) -} -`, - }, - { - // More removed than added: mismatched counts fall back to whole-line colour. - name: "mismatched counts (whole-line fallback)", - old: `case "json": - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - return enc.Encode(v) -`, - new: `case "json": - return json.NewEncoder(w).Encode(v) -`, - }, - { - // Unicode content: char diff should handle multi-byte runes correctly. - name: "unicode content", - old: "Héllo, wörld! Ünïcödé is fün.\n", - new: "Héllo, wörld! Ünïcödé is grëat.\n", - }, - } - - for _, sc := range scenarios { - t.Run(sc.name, func(t *testing.T) { - lines := diff.Lines("want", []byte(sc.old), "got", []byte(sc.new)) - t.Logf("\n=== %s ===\n%s\n", sc.name, diff.Render(lines)) - }) - } -} - -// BenchmarkRender benchmarks Render using a realistic diff. -func BenchmarkRender(b *testing.B) { - old := []byte("the quick brown fox\njumps over the lazy dog\nsome context\nmore context\n") - newContent := []byte("the quick brown cat\njumps over the lazy frog\nsome context\nmore context\n") - lines := diff.Lines("want", old, "got", newContent) - - b.ResetTimer() - - for b.Loop() { - diff.Render(lines) - } -} diff --git a/internal/diff/testdata/TestRender/added_header_line_is_green.txt b/internal/diff/testdata/TestRender/added_header_line_is_green.txt deleted file mode 100644 index ab3cdde..0000000 --- a/internal/diff/testdata/TestRender/added_header_line_is_green.txt +++ /dev/null @@ -1,2 +0,0 @@ -+++ got - \ No newline at end of file diff --git a/internal/diff/testdata/TestRender/context_line_has_no_colour_and_double-space_prefix.txt b/internal/diff/testdata/TestRender/context_line_has_no_colour_and_double-space_prefix.txt deleted file mode 100644 index bf47b77..0000000 --- a/internal/diff/testdata/TestRender/context_line_has_no_colour_and_double-space_prefix.txt +++ /dev/null @@ -1 +0,0 @@ - unchanged diff --git a/internal/diff/testdata/TestRender/diff_header_line_is_bold_no_colour.txt b/internal/diff/testdata/TestRender/diff_header_line_is_bold_no_colour.txt deleted file mode 100644 index c03a4d1..0000000 --- a/internal/diff/testdata/TestRender/diff_header_line_is_bold_no_colour.txt +++ /dev/null @@ -1,2 +0,0 @@ -diff want got - \ No newline at end of file diff --git a/internal/diff/testdata/TestRender/empty_slice_returns_empty_string.txt b/internal/diff/testdata/TestRender/empty_slice_returns_empty_string.txt deleted file mode 100644 index e69de29..0000000 diff --git a/internal/diff/testdata/TestRender/hunk_header_is_bold_no_colour.txt b/internal/diff/testdata/TestRender/hunk_header_is_bold_no_colour.txt deleted file mode 100644 index 02d9dab..0000000 --- a/internal/diff/testdata/TestRender/hunk_header_is_bold_no_colour.txt +++ /dev/null @@ -1,2 +0,0 @@ -@@ -1,1 +1,1 @@ - \ No newline at end of file diff --git a/internal/diff/testdata/TestRender/matched_removed_added_pair_uses_inline_char_diff_with_coloured_prefixes.txt b/internal/diff/testdata/TestRender/matched_removed_added_pair_uses_inline_char_diff_with_coloured_prefixes.txt deleted file mode 100644 index 79759f3..0000000 --- a/internal/diff/testdata/TestRender/matched_removed_added_pair_uses_inline_char_diff_with_coloured_prefixes.txt +++ /dev/null @@ -1,3 +0,0 @@ -- old -+ new - \ No newline at end of file diff --git a/internal/diff/testdata/TestRender/mismatched_count_uses_whole-line_colour_with_coloured_prefixes.txt b/internal/diff/testdata/TestRender/mismatched_count_uses_whole-line_colour_with_coloured_prefixes.txt deleted file mode 100644 index ecbdf6f..0000000 --- a/internal/diff/testdata/TestRender/mismatched_count_uses_whole-line_colour_with_coloured_prefixes.txt +++ /dev/null @@ -1,4 +0,0 @@ -- line one -- line two -+ replacement - \ No newline at end of file diff --git a/internal/diff/testdata/TestRender/nil_input_returns_empty_string.txt b/internal/diff/testdata/TestRender/nil_input_returns_empty_string.txt deleted file mode 100644 index e69de29..0000000 diff --git a/internal/diff/testdata/TestRender/removed_header_line_is_red.txt b/internal/diff/testdata/TestRender/removed_header_line_is_red.txt deleted file mode 100644 index c5c3732..0000000 --- a/internal/diff/testdata/TestRender/removed_header_line_is_red.txt +++ /dev/null @@ -1,2 +0,0 @@ ---- want - \ No newline at end of file diff --git a/internal/diff/testdata/TestRender/standalone_added_line_has_green_prefix_and_content.txt b/internal/diff/testdata/TestRender/standalone_added_line_has_green_prefix_and_content.txt deleted file mode 100644 index 751f4d7..0000000 --- a/internal/diff/testdata/TestRender/standalone_added_line_has_green_prefix_and_content.txt +++ /dev/null @@ -1,2 +0,0 @@ -+ new line - \ No newline at end of file diff --git a/internal/diff/testdata/TestRender/standalone_removed_line_has_red_prefix_and_content.txt b/internal/diff/testdata/TestRender/standalone_removed_line_has_red_prefix_and_content.txt deleted file mode 100644 index 395e3de..0000000 --- a/internal/diff/testdata/TestRender/standalone_removed_line_has_red_prefix_and_content.txt +++ /dev/null @@ -1,2 +0,0 @@ -- old line - \ No newline at end of file diff --git a/internal/diff/testdata/allnew.txtar b/internal/diff/testdata/allnew.txtar deleted file mode 100644 index 0828b55..0000000 --- a/internal/diff/testdata/allnew.txtar +++ /dev/null @@ -1,13 +0,0 @@ --- old -- --- new -- -a -b -c --- diff -- -diff old new ---- old -+++ new -@@ -0,0 +1,3 @@ -+ a -+ b -+ c diff --git a/internal/diff/testdata/allold.txtar b/internal/diff/testdata/allold.txtar deleted file mode 100644 index 020cedf..0000000 --- a/internal/diff/testdata/allold.txtar +++ /dev/null @@ -1,13 +0,0 @@ --- old -- -a -b -c --- new -- --- diff -- -diff old new ---- old -+++ new -@@ -1,3 +0,0 @@ -- a -- b -- c diff --git a/internal/diff/testdata/basic.txtar b/internal/diff/testdata/basic.txtar deleted file mode 100644 index f12c77d..0000000 --- a/internal/diff/testdata/basic.txtar +++ /dev/null @@ -1,35 +0,0 @@ -# Example from Hunt and McIlroy, “An Algorithm for Differential File Comparison.” -# https://www.cs.dartmouth.edu/~doug/diff.pdf - --- old -- -a -b -c -d -e -f -g --- new -- -w -a -b -x -y -z -e --- diff -- -diff old new ---- old -+++ new -@@ -1,7 +1,7 @@ -+ w - a - b -- c -- d -+ x -+ y -+ z - e -- f -- g diff --git a/internal/diff/testdata/dups.txtar b/internal/diff/testdata/dups.txtar deleted file mode 100644 index f69f6ac..0000000 --- a/internal/diff/testdata/dups.txtar +++ /dev/null @@ -1,40 +0,0 @@ --- old -- -a - -b - -c - -d - -e - -f --- new -- -a - -B - -C - -d - -e - -f --- diff -- -diff old new ---- old -+++ new -@@ -1,8 +1,8 @@ - a - $ -- b -- -- c -+ B -+ -+ C - $ - d - $ diff --git a/internal/diff/testdata/end.txtar b/internal/diff/testdata/end.txtar deleted file mode 100644 index 1c1ef5f..0000000 --- a/internal/diff/testdata/end.txtar +++ /dev/null @@ -1,38 +0,0 @@ --- old -- -1 -2 -3 -4 -5 -6 -7 -eight -nine -ten -eleven --- new -- -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 --- diff -- -diff old new ---- old -+++ new -@@ -5,7 +5,6 @@ - 5 - 6 - 7 -- eight -- nine -- ten -- eleven -+ 8 -+ 9 -+ 10 diff --git a/internal/diff/testdata/eof.txtar b/internal/diff/testdata/eof.txtar deleted file mode 100644 index 5dc145c..0000000 --- a/internal/diff/testdata/eof.txtar +++ /dev/null @@ -1,9 +0,0 @@ --- old -- -a -b -c^D --- new -- -a -b -c^D --- diff -- diff --git a/internal/diff/testdata/eof1.txtar b/internal/diff/testdata/eof1.txtar deleted file mode 100644 index fa9e11f..0000000 --- a/internal/diff/testdata/eof1.txtar +++ /dev/null @@ -1,18 +0,0 @@ --- old -- -a -b -c --- new -- -a -b -c^D --- diff -- -diff old new ---- old -+++ new -@@ -1,3 +1,3 @@ - a - b -- c -+ c -\ No newline at end of file diff --git a/internal/diff/testdata/eof2.txtar b/internal/diff/testdata/eof2.txtar deleted file mode 100644 index 2a3e2d6..0000000 --- a/internal/diff/testdata/eof2.txtar +++ /dev/null @@ -1,18 +0,0 @@ --- old -- -a -b -c^D --- new -- -a -b -c --- diff -- -diff old new ---- old -+++ new -@@ -1,3 +1,3 @@ - a - b -- c -\ No newline at end of file -+ c diff --git a/internal/diff/testdata/fuzz/FuzzCharDiff/10ab2b7964808a78 b/internal/diff/testdata/fuzz/FuzzCharDiff/10ab2b7964808a78 deleted file mode 100644 index 7207ac5..0000000 --- a/internal/diff/testdata/fuzz/FuzzCharDiff/10ab2b7964808a78 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -[]byte("\x80") -[]byte("\x80") diff --git a/internal/diff/testdata/fuzz/FuzzCharDiff/959c728e1ba9028d b/internal/diff/testdata/fuzz/FuzzCharDiff/959c728e1ba9028d deleted file mode 100644 index acd79d4..0000000 --- a/internal/diff/testdata/fuzz/FuzzCharDiff/959c728e1ba9028d +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -[]byte("0") -[]byte("\xe2") diff --git a/internal/diff/testdata/long.txtar b/internal/diff/testdata/long.txtar deleted file mode 100644 index 1ab33fb..0000000 --- a/internal/diff/testdata/long.txtar +++ /dev/null @@ -1,62 +0,0 @@ --- old -- -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -14½ -15 -16 -17 -18 -19 -20 --- new -- -1 -2 -3 -4 -5 -6 -8 -9 -10 -11 -12 -13 -14 -17 -18 -19 -20 --- diff -- -diff old new ---- old -+++ new -@@ -4,7 +4,6 @@ - 4 - 5 - 6 -- 7 - 8 - 9 - 10 -@@ -12,9 +11,6 @@ - 12 - 13 - 14 -- 14½ -- 15 -- 16 - 17 - 18 - 19 diff --git a/internal/diff/testdata/same.txtar b/internal/diff/testdata/same.txtar deleted file mode 100644 index 86b1100..0000000 --- a/internal/diff/testdata/same.txtar +++ /dev/null @@ -1,5 +0,0 @@ --- old -- -hello world --- new -- -hello world --- diff -- diff --git a/internal/diff/testdata/start.txtar b/internal/diff/testdata/start.txtar deleted file mode 100644 index 3842583..0000000 --- a/internal/diff/testdata/start.txtar +++ /dev/null @@ -1,34 +0,0 @@ --- old -- -e -pi -4 -5 -6 -7 -8 -9 -10 --- new -- -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 --- diff -- -diff old new ---- old -+++ new -@@ -1,5 +1,6 @@ -- e -- pi -+ 1 -+ 2 -+ 3 - 4 - 5 - 6 diff --git a/internal/diff/testdata/triv.txtar b/internal/diff/testdata/triv.txtar deleted file mode 100644 index a1fea60..0000000 --- a/internal/diff/testdata/triv.txtar +++ /dev/null @@ -1,40 +0,0 @@ -# Another example from Hunt and McIlroy, -# “An Algorithm for Differential File Comparison.” -# https://www.cs.dartmouth.edu/~doug/diff.pdf - -# Anchored diff gives up on finding anything, -# since there are no unique lines. - --- old -- -a -b -c -a -b -b -a --- new -- -c -a -b -a -b -c --- diff -- -diff old new ---- old -+++ new -@@ -1,7 +1,6 @@ -- a -- b -- c -- a -- b -- b -- a -+ c -+ a -+ b -+ a -+ b -+ c diff --git a/test.go b/test.go index 56a00ec..fa28c61 100644 --- a/test.go +++ b/test.go @@ -14,8 +14,9 @@ import ( "sync" "testing" + "go.followtheprocess.codes/diff" + "go.followtheprocess.codes/diff/render" "go.followtheprocess.codes/hue" - "go.followtheprocess.codes/test/internal/diff" ) // ColorEnabled sets whether the output from this package is colourised. @@ -380,7 +381,7 @@ func DiffBytes(tb testing.TB, got, want []byte) { want = fixNL(want) if lines := diff.Lines("want", want, "got", got); lines != nil { - tb.Fatalf("\nDiff\n----\n%s\n", diff.Render(lines)) + tb.Fatalf("\nDiff\n----\n%s\n", render.Render(lines)) } } @@ -402,9 +403,6 @@ func DiffReader(tb testing.TB, got, want io.Reader) { tb.Fatalf("DiffReader: could not read from want: %v\n", err) } - gotData = fixNL(gotData) - wantData = fixNL(wantData) - DiffBytes(tb, gotData, wantData) }