package prosediff
import (
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDiffIntsScripts(t *testing.T) {
tests := []struct {
name string
a, b []int
want []edit
}{
{"both empty", nil, nil, nil},
{"identical", []int{1, 2, 3}, []int{1, 2, 3}, []edit{{OpEqual, 3}}},
{"all inserted", nil, []int{1, 2}, []edit{{OpInsert, 2}}},
{"all deleted", []int{1, 2}, nil, []edit{{OpDelete, 2}}},
{"middle replaced", []int{1, 2, 3}, []int{1, 9, 3},
[]edit{{OpEqual, 1}, {OpDelete, 1}, {OpInsert, 1}, {OpEqual, 1}}},
{"suffix appended", []int{1, 2}, []int{1, 2, 3}, []edit{{OpEqual, 2}, {OpInsert, 1}}},
{"prefix removed", []int{0, 1, 2}, []int{1, 2}, []edit{{OpDelete, 1}, {OpEqual, 2}}},
{"nothing in common", []int{1, 2}, []int{3, 4},
[]edit{{OpDelete, 2}, {OpInsert, 2}}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := diffInts(tc.a, tc.b)
assert.Equal(t, tc.want, got)
assertScriptApplies(t, tc.a, tc.b, got)
})
}
}
// TestDiffIntsMinimalRandom is the real correctness check: on random inputs
// the script must both reconstruct b from a and keep exactly as many elements
// equal as the longest common subsequence.
func TestDiffIntsMinimalRandom(t *testing.T) {
rng := rand.New(rand.NewSource(1))
for iter := 0; iter < 400; iter++ {
a := randSeq(rng, rng.Intn(14), 5)
b := randSeq(rng, rng.Intn(14), 5)
script := diffInts(a, b)
assertScriptApplies(t, a, b, script)
require.Equal(t, lcsLen(a, b), commonCount(script),
"non-minimal script for a=%v b=%v: %v", a, b, script)
for i := 1; i < len(script); i++ {
require.NotEqual(t, script[i-1].op, script[i].op, "runs not coalesced: %v", script)
}
}
}
func TestRatio(t *testing.T) {
assert.Equal(t, 1.0, ratio(nil, nil))
assert.Equal(t, 1.0, ratio([]int{1, 2}, []int{1, 2}))
assert.Equal(t, 0.0, ratio([]int{1, 2}, []int{3, 4}))
assert.Equal(t, 0.5, ratio([]int{1, 2}, []int{1, 3}))
}
func randSeq(rng *rand.Rand, n, alphabet int) []int {
if n == 0 {
return nil
}
out := make([]int, n)
for i := range out {
out[i] = rng.Intn(alphabet)
}
return out
}
func assertScriptApplies(t *testing.T, a, b []int, script []edit) {
t.Helper()
var gotA, gotB []int
i, j := 0, 0
for _, e := range script {
switch e.op {
case OpEqual:
require.LessOrEqual(t, i+e.n, len(a))
require.LessOrEqual(t, j+e.n, len(b))
for n := 0; n < e.n; n++ {
require.Equal(t, a[i+n], b[j+n], "equal run over differing elements")
}
gotA = append(gotA, a[i:i+e.n]...)
gotB = append(gotB, b[j:j+e.n]...)
i += e.n
j += e.n
case OpDelete:
gotA = append(gotA, a[i:i+e.n]...)
i += e.n
case OpInsert:
gotB = append(gotB, b[j:j+e.n]...)
j += e.n
}
}
require.Equal(t, len(a), i)
require.Equal(t, len(b), j)
require.Equal(t, a, sliceOrNil(gotA))
require.Equal(t, b, sliceOrNil(gotB))
}
func sliceOrNil(s []int) []int {
if len(s) == 0 {
return nil
}
return s
}
// lcsLen is the textbook O(nm) LCS, used only as the oracle.
func lcsLen(a, b []int) int {
dp := make([][]int, len(a)+1)
for i := range dp {
dp[i] = make([]int, len(b)+1)
}
for i := 1; i <= len(a); i++ {
for j := 1; j <= len(b); j++ {
if a[i-1] == b[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
continue
}
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
return dp[len(a)][len(b)]
}