package reconcile_test import ( "math/rand" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.d-ma.be/mathias/tapir/internal/reconcile" ) func words(s string) []string { return strings.Fields(s) } // apply rebuilds b from a using the ops, copying equal runs from a and the // replacement/insertion runs from b. If Diff is right this must equal b. func apply(a, b []string, ops []reconcile.Op) []string { var out []string for _, o := range ops { switch o.Kind { case reconcile.Equal: out = append(out, a[o.AStart:o.AEnd]...) default: out = append(out, b[o.BStart:o.BEnd]...) } } return out } func TestDiff_Cases(t *testing.T) { tests := []struct { name string a, b string want []reconcile.Op }{ {"identical", "a b c", "a b c", []reconcile.Op{{Kind: reconcile.Equal, AStart: 0, AEnd: 3, BStart: 0, BEnd: 3}}}, {"both empty", "", "", nil}, {"one word replaced", "a b c", "a x c", []reconcile.Op{ {Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1}, {Kind: reconcile.Replace, AStart: 1, AEnd: 2, BStart: 1, BEnd: 2}, {Kind: reconcile.Equal, AStart: 2, AEnd: 3, BStart: 2, BEnd: 3}, }}, {"word only in b", "a c", "a b c", []reconcile.Op{ {Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1}, {Kind: reconcile.Insert, AStart: 1, AEnd: 1, BStart: 1, BEnd: 2}, {Kind: reconcile.Equal, AStart: 1, AEnd: 2, BStart: 2, BEnd: 3}, }}, {"word only in a", "a b c", "a c", []reconcile.Op{ {Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1}, {Kind: reconcile.Delete, AStart: 1, AEnd: 2, BStart: 1, BEnd: 1}, {Kind: reconcile.Equal, AStart: 2, AEnd: 3, BStart: 1, BEnd: 2}, }}, {"nothing in common", "a b", "x y z", []reconcile.Op{ {Kind: reconcile.Replace, AStart: 0, AEnd: 2, BStart: 0, BEnd: 3}, }}, {"a is empty", "", "x y", []reconcile.Op{{Kind: reconcile.Insert, AStart: 0, AEnd: 0, BStart: 0, BEnd: 2}}}, {"b is empty", "x y", "", []reconcile.Op{{Kind: reconcile.Delete, AStart: 0, AEnd: 2, BStart: 0, BEnd: 0}}}, {"adjacent delete and insert read as one replace", "a b c d", "a x y d", []reconcile.Op{ {Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1}, {Kind: reconcile.Replace, AStart: 1, AEnd: 3, BStart: 1, BEnd: 3}, {Kind: reconcile.Equal, AStart: 3, AEnd: 4, BStart: 3, BEnd: 4}, }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, reconcile.Diff(words(tt.a), words(tt.b))) }) } } // lcsLen is a plain dynamic-programming reference. Myers must find an edit script // whose equal runs add up to exactly this: a diff that is merely valid but not // minimal would turn real agreement into disputes the adjudicator is then asked about. func lcsLen(a, b []string) int { prev := make([]int, len(b)+1) for i := 1; i <= len(a); i++ { cur := make([]int, len(b)+1) for j := 1; j <= len(b); j++ { if a[i-1] == b[j-1] { cur[j] = prev[j-1] + 1 } else { cur[j] = max(prev[j], cur[j-1]) } } prev = cur } return prev[len(b)] } func TestDiff_RebuildsBAndKeepsEveryCommonWordOnRandomInputs(t *testing.T) { rng := rand.New(rand.NewSource(7)) alphabet := []string{"a", "b", "c", "d", "e", "f"} gen := func(n int) []string { out := make([]string, n) for i := range out { out[i] = alphabet[rng.Intn(len(alphabet))] } return out } for i := 0; i < 400; i++ { a, b := gen(rng.Intn(25)), gen(rng.Intn(25)) ops := reconcile.Diff(a, b) require.Equal(t, append([]string{}, b...), append([]string{}, apply(a, b, ops)...), "a=%v b=%v ops=%v", a, b, ops) common := 0 prevA, prevB := 0, 0 for _, o := range ops { require.Equal(t, prevA, o.AStart, "ops must tile a without gaps: %v", ops) require.Equal(t, prevB, o.BStart, "ops must tile b without gaps: %v", ops) prevA, prevB = o.AEnd, o.BEnd if o.Kind == reconcile.Equal { common += o.AEnd - o.AStart require.Equal(t, a[o.AStart:o.AEnd], b[o.BStart:o.BEnd], "an Equal op must really be equal") } } require.Equal(t, len(a), prevA) require.Equal(t, len(b), prevB) require.Equal(t, lcsLen(a, b), common, "the diff must be minimal: a=%v b=%v", a, b) } } func TestDiff_HandlesAnHoursWorthOfWordsFast(t *testing.T) { rng := rand.New(rand.NewSource(3)) a := make([]string, 6500) for i := range a { a[i] = string(rune('a'+rng.Intn(26))) + string(rune('a'+rng.Intn(26))) } b := append([]string{}, a...) for i := 0; i < 380; i++ { // ~380 disputes, as measured on a real hour b[rng.Intn(len(b))] = "zz" + string(rune('a'+rng.Intn(26))) } ops := reconcile.Diff(a, b) assert.NotEmpty(t, ops) assert.Equal(t, append([]string{}, b...), append([]string{}, apply(a, b, ops)...)) }