Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

myersdiff_test.gno

9.83 Kb · 278 lines
  1package myersdiff
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	"gno.land/p/nt/uassert/v0"
  8)
  9
 10// render turns a script into one compact string per line, so a table case can
 11// state the whole expected script as text instead of a slice literal.
 12func render(s Script) string {
 13	var sb strings.Builder
 14	for i, e := range s.Edits {
 15		if i > 0 {
 16			sb.WriteString("|")
 17		}
 18		sb.WriteString(e.Op.String())
 19		sb.WriteString(e.Line)
 20	}
 21	return sb.String()
 22}
 23
 24func TestScript(t *testing.T) {
 25	cases := []struct {
 26		name string
 27		a    string
 28		b    string
 29		want string
 30	}{
 31		{"identical", "a\nb\nc", "a\nb\nc", " a| b| c"},
 32		{"both empty", "", "", ""},
 33		{"insert into empty", "", "a\nb", "+a|+b"},
 34		{"delete to empty", "a\nb", "", "-a|-b"},
 35		{"append one", "a", "a\nb", " a|+b"},
 36		{"prepend one", "b", "a\nb", "+a| b"},
 37		{"substitute middle", "a\nb\nc", "a\nx\nc", " a|-b|+x| c"},
 38		{"delete middle", "a\nb\nc", "a\nc", " a|-b| c"},
 39		{"nothing in common", "a\nb", "x\ny", "-a|-b|+x|+y"},
 40		{"trailing newline is a line", "a", "a\n", " a|+"},
 41		{"move one line down", "a\nb\nc", "b\nc\na", "-a| b| c|+a"},
 42	}
 43
 44	for _, tc := range cases {
 45		t.Run(tc.name, func(t *testing.T) {
 46			got := Lines(tc.a, tc.b)
 47			uassert.True(t, got.Minimal, "expected a minimal script")
 48			uassert.Equal(t, tc.want, render(got))
 49		})
 50	}
 51}
 52
 53// TestScriptIsShortest is the property the algorithm exists for. A correct but
 54// lazy implementation (delete everything, insert everything) passes every
 55// round-trip test, so the count of changed lines is asserted separately.
 56func TestScriptIsShortest(t *testing.T) {
 57	cases := []struct {
 58		name    string
 59		a       string
 60		b       string
 61		changed int
 62	}{
 63		{"one substitution", "a\nb\nc", "a\nx\nc", 2},
 64		{"one insertion", "a\nb\nc", "a\nb\nx\nc", 1},
 65		{"one deletion", "a\nb\nc", "a\nc", 1},
 66		{"two far apart", "a\nb\nc\nd\ne", "x\nb\nc\nd\ny", 4},
 67		{"classic abcabba to cbabac", "a\nb\nc\na\nb\nb\na", "c\nb\na\nb\na\nc", 5},
 68	}
 69
 70	for _, tc := range cases {
 71		t.Run(tc.name, func(t *testing.T) {
 72			s := Lines(tc.a, tc.b)
 73			changed := s.Added() + s.Deleted()
 74			uassert.Equal(t, tc.changed, changed)
 75		})
 76	}
 77}
 78
 79// TestApplyRoundTrips is the correctness half: whatever script comes out, it
 80// has to turn a back into b. Checked on the minimal path and on the budget
 81// fallback, because the fallback is the one nobody looks at.
 82func TestApplyRoundTrips(t *testing.T) {
 83	cases := []struct {
 84		name   string
 85		a      string
 86		b      string
 87		budget int
 88	}{
 89		{"typical patch", "one\ntwo\nthree\nfour", "one\n2\nthree\nfour\nfive", 0},
 90		{"full rewrite", "a\nb\nc\nd", "w\nx\ny\nz", 0},
 91		{"starved budget", "a\nb\nc\nd", "w\nx\ny\nz", 1},
 92		{"empty to empty", "", "", 0},
 93		{"repeated lines", "x\nx\nx\nx", "x\nx", 0},
 94	}
 95
 96	for _, tc := range cases {
 97		t.Run(tc.name, func(t *testing.T) {
 98			s := Budget(split(tc.a), split(tc.b), tc.budget)
 99			lines, ok := s.Apply(split(tc.a))
100			uassert.True(t, ok, "the script should describe its own source")
101			uassert.Equal(t, tc.b, strings.Join(lines, "\n"))
102		})
103	}
104}
105
106// TestBudgetFallback pins what a starved search returns. The script stops being
107// minimal and says so; it does not stop being correct and it does not panic.
108func TestBudgetFallback(t *testing.T) {
109	a := split("a\nb\nc\nd\ne\nf")
110	b := split("u\nv\nw\nx\ny\nz")
111
112	starved := Budget(a, b, 2)
113	uassert.False(t, starved.Minimal, "a two-round budget cannot reach a 12-edit script")
114	uassert.Equal(t, 6, starved.Deleted())
115	uassert.Equal(t, 6, starved.Added())
116	uassert.Equal(t, 0, starved.Unchanged())
117
118	full := Budget(a, b, 0)
119	uassert.True(t, full.Minimal, "the default budget is enough for six lines")
120	applied, ok := full.Apply(a)
121	uassert.True(t, ok, "the minimal script should describe its own source")
122	uassert.Equal(t, strings.Join(b, "\n"), strings.Join(applied, "\n"))
123
124	// A budget above what the script needs changes nothing, and a budget larger
125	// than any possible script is not an error.
126	wide := Budget(a, b, 10000)
127	uassert.True(t, wide.Minimal, "an oversized budget still terminates")
128	uassert.Equal(t, render(full), render(wide))
129}
130
131func TestCounts(t *testing.T) {
132	s := Lines("a\nb\nc", "a\nx\ny\nc")
133	uassert.Equal(t, 2, s.Added())
134	uassert.Equal(t, 1, s.Deleted())
135	uassert.Equal(t, 2, s.Unchanged())
136}
137
138func TestHunks(t *testing.T) {
139	// Eleven lines, two changes far enough apart to stay separate at context 1
140	// and to merge at context 3.
141	a := "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11"
142	b := "1\n2\nX\n4\n5\n6\n7\n8\nY\n10\n11"
143	s := Lines(a, b)
144
145	narrow := Hunks(s, 1)
146	uassert.Equal(t, 2, len(narrow))
147	uassert.Equal(t, "@@ -2,3 +2,3 @@", narrow[0].Header())
148	uassert.Equal(t, "@@ -8,3 +8,3 @@", narrow[1].Header())
149
150	wide := Hunks(s, 3)
151	uassert.Equal(t, 1, len(wide))
152	uassert.Equal(t, "@@ -1,11 +1,11 @@", wide[0].Header())
153
154	none := Hunks(Lines(a, a), 3)
155	uassert.Equal(t, 0, len(none))
156
157	zero := Hunks(s, 0)
158	uassert.Equal(t, 2, len(zero))
159	uassert.Equal(t, "@@ -3 +3 @@", zero[0].Header())
160}
161
162// TestHunkHeaderEmptySide pins the one header convention that is not obvious:
163// a side contributing no lines reports the line before the hunk and a length of
164// zero, which is what diff(1) prints and what patch(1) expects to read back.
165func TestHunkHeaderEmptySide(t *testing.T) {
166	into := Hunks(Lines("", "a\nb"), 0)
167	uassert.Equal(t, 1, len(into))
168	uassert.Equal(t, "@@ -0,0 +1,2 @@", into[0].Header())
169
170	outOf := Hunks(Lines("a\nb", ""), 0)
171	uassert.Equal(t, 1, len(outOf))
172	uassert.Equal(t, "@@ -1,2 +0,0 @@", outOf[0].Header())
173
174	negative := Hunks(Lines("a", "b"), -5)
175	uassert.Equal(t, 1, len(negative))
176	uassert.Equal(t, "@@ -1 +1 @@", negative[0].Header())
177}
178
179func TestUnified(t *testing.T) {
180	a := "the quick\nbrown fox\njumps over\nthe lazy dog"
181	b := "the quick\nred fox\njumps over\nthe lazy dog\nand runs"
182
183	want := "@@ -1,4 +1,5 @@\n the quick\n-brown fox\n+red fox\n jumps over\n the lazy dog\n+and runs\n"
184	uassert.Equal(t, want, Unified(a, b, 3))
185
186	uassert.Equal(t, "", Unified(a, a, 3))
187	uassert.Equal(t, Unified(a, b, 3), UnifiedScript(Lines(a, b), 3))
188}
189
190func TestOpString(t *testing.T) {
191	uassert.Equal(t, "-", Delete.String())
192	uassert.Equal(t, " ", Equal.String())
193	uassert.Equal(t, "+", Insert.String())
194}
195
196func TestMaxLines(cur realm, t *testing.T) {
197	big := make([]string, MaxLines+1)
198	ok := make([]string, MaxLines)
199	uassert.PanicsWithMessage(t, cur, "myersdiff: input exceeds MaxLines", func() { Budget(big, ok, 0) })
200	uassert.PanicsWithMessage(t, cur, "myersdiff: input exceeds MaxLines", func() { Budget(ok, big, 0) })
201	// MaxLines identical lines is the cheap case: d is zero, so the search ends
202	// in the first round whatever the size.
203	uassert.NotPanics(t, cur, func() { Budget(ok, ok, 0) })
204}
205
206// TestApplyChecksTheSourceIsWhatTheScriptDescribes closes the gap a review
207// found on the first pass: Apply used to ignore its argument entirely, so it
208// replayed any script into the script's own right-hand side and reported
209// nothing. The round trip then proved only that the script agreed with itself,
210// and the demo's applies() check was vacuous for the same reason.
211func TestApplyChecksTheSourceIsWhatTheScriptDescribes(t *testing.T) {
212	s := Lines("a\nb\nc", "a\nx\nc")
213
214	got, ok := s.Apply([]string{"a", "b", "c"})
215	uassert.True(t, ok, "the script's own source should be accepted")
216	uassert.Equal(t, "a\nx\nc", strings.Join(got, "\n"))
217
218	_, ok = s.Apply([]string{"q", "b", "c"})
219	uassert.False(t, ok, "an Equal line absent from the source is not a script for it")
220
221	_, ok = s.Apply([]string{"a", "q", "c"})
222	uassert.False(t, ok, "a Delete line absent from the source is not a script for it")
223
224	_, ok = s.Apply([]string{"a", "b"})
225	uassert.False(t, ok, "a source shorter than the script consumes")
226
227	_, ok = s.Apply([]string{"a", "b", "c", "d"})
228	uassert.False(t, ok, "a source with a line the script never consumed")
229
230	_, ok = s.Apply([]string{})
231	uassert.False(t, ok, "an empty source against a three-line script")
232}
233
234// TestLinesEnforcesTheLineCap covers the other half of the same review:
235// MaxLines guarded the comparison but not the strings.Split in front of it, so
236// a caller could force an arbitrarily large line slice and only then be
237// refused. Nothing here distinguishes the two orders, because the difference is
238// an allocation and gno test cannot observe one. What it does pin is that Lines
239// and Unified enforce the cap at all, which nothing asserted before.
240func TestLinesEnforcesTheLineCap(cur realm, t *testing.T) {
241	over := strings.Repeat("x\n", MaxLines)
242	atCap := strings.Repeat("x\n", MaxLines-1) + "x"
243
244	uassert.PanicsWithMessage(t, cur, "myersdiff: input exceeds MaxLines", func() { Lines(over, "") })
245	uassert.PanicsWithMessage(t, cur, "myersdiff: input exceeds MaxLines", func() { Lines("", over) })
246	uassert.PanicsWithMessage(t, cur, "myersdiff: input exceeds MaxLines", func() { Unified("", over, 1) })
247	uassert.NotPanics(t, cur, func() { Lines(atCap, atCap) })
248}
249
250// TestEffectiveRounds pins the one number that decides how much memory the
251// search can be made to allocate. The frontier is snapshotted once per round,
252// so the round count is the memory bound, and before MaxBudget existed a caller
253// could ask for 8,192 rounds by passing a large budget with two MaxLines
254// documents. This is asserted directly rather than through a diff, because the
255// inputs that would show it behaviourally take minutes to run.
256func TestEffectiveRounds(t *testing.T) {
257	cases := []struct {
258		name   string
259		budget int
260		n, m   int
261		want   int
262	}{
263		{"default applies when unset", 0, 1000, 1000, DefaultBudget},
264		{"default applies when negative", -7, 1000, 1000, DefaultBudget},
265		{"a small budget is kept", 5, 100, 100, 5},
266		{"no script is longer than n+m", 0, 10, 10, 20},
267		{"a large budget is capped at MaxBudget", 100000, MaxLines, MaxLines, MaxBudget},
268		{"MaxBudget never exceeds n+m either", 100000, 3, 4, 7},
269	}
270
271	for _, tc := range cases {
272		t.Run(tc.name, func(t *testing.T) {
273			uassert.Equal(t, tc.want, effectiveRounds(tc.budget, tc.n, tc.m))
274		})
275	}
276
277	uassert.True(t, MaxBudget >= DefaultBudget, "the cap must leave the default reachable")
278}