Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ jobs:

- name: Run benchmarks
run: go test -bench=. ./...

- name: Run fuzz tests
run: make fuzz
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ race:
bench:
@hack/do.sh bench

.PHONY: fuzz
fuzz:
@hack/do.sh fuzz

.PHONY: build-nolint
build-nolint:
@NOLINT=1 hack/do.sh build
Expand Down
108 changes: 108 additions & 0 deletions diff/myers/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package myers

import (
"strings"
"testing"

"github.com/neticdk/go-stdlib/diff"
"github.com/neticdk/go-stdlib/diff/internal/diffcore"
)

func FuzzMyersLinearSpace(f *testing.F) {
f.Add("hello", "world")
f.Add("abc", "def")
f.Add("A B C D E", "X Y A B C D E Z")
f.Add("X X X X X", "X X X X")
f.Add("", "")
f.Add("A", "")
f.Add("", "B")
f.Add("A B C D E F G H I J K", "X Y A B C D E F G H I J K Z")

f.Fuzz(func(t *testing.T, aStr, bStr string) {
a := []string{}
if aStr != "" {
a = strings.Split(aStr, "")
Comment thread
ChBLA marked this conversation as resolved.
}
b := []string{}
if bStr != "" {
b = strings.Split(bStr, "")
}

opts := options{
linearSpace: true,
linearRecursionMaxDepth: 100, // standard
}

// Test that it doesn't panic
script := computeEditScriptLinearSpace(a, b, opts)

// Also compute with LCS for comparison of edit distance length
lcsScript := diffcore.ComputeEditsLCS(a, b)

// Verify both scripts correctly transform a to b
verifyScript(t, a, b, script)
verifyScript(t, a, b, lcsScript)

// Count edits to ensure both found optimal paths (same number of edits)
editsMyers := 0
for _, l := range script {
if l.Kind != diff.Equal {
editsMyers++
}
}

editsLCS := 0
for _, l := range lcsScript {
if l.Kind != diff.Equal {
editsLCS++
}
}
Comment thread
KimNorgaard marked this conversation as resolved.

if editsMyers != editsLCS {
t.Errorf("Myers gave %d edits, LCS gave %d edits. A=%q, B=%q", editsMyers, editsLCS, aStr, bStr)
}
})
}

func verifyScript(t *testing.T, a, b []string, script []diff.Line) {
t.Helper()
aIdx := 0
Comment thread
KimNorgaard marked this conversation as resolved.
bIdx := 0

for _, l := range script {
switch l.Kind {
case diff.Equal:
if aIdx >= len(a) || bIdx >= len(b) {
t.Fatalf("Equal operation out of bounds: aIdx=%d, bIdx=%d, aLen=%d, bLen=%d", aIdx, bIdx, len(a), len(b))
}
if a[aIdx] != l.Text || b[bIdx] != l.Text {
t.Fatalf("Equal text mismatch: text=%q, a[%d]=%q, b[%d]=%q", l.Text, aIdx, a[aIdx], bIdx, b[bIdx])
}
aIdx++
bIdx++
case diff.Insert:
if bIdx >= len(b) {
t.Fatalf("Insert operation out of bounds: bIdx=%d, bLen=%d", bIdx, len(b))
}
if b[bIdx] != l.Text {
t.Fatalf("Insert text mismatch: text=%q, b[%d]=%q", l.Text, bIdx, b[bIdx])
}
bIdx++
case diff.Delete:
if aIdx >= len(a) {
t.Fatalf("Delete operation out of bounds: aIdx=%d, aLen=%d", aIdx, len(a))
}
if a[aIdx] != l.Text {
t.Fatalf("Delete text mismatch: text=%q, a[%d]=%q", l.Text, aIdx, a[aIdx])
}
aIdx++
}
}

if aIdx != len(a) {
t.Fatalf("Did not consume all of a. aIdx=%d, aLen=%d", aIdx, len(a))
}
if bIdx != len(b) {
t.Fatalf("Did not consume all of b. bIdx=%d, bLen=%d", bIdx, len(b))
}
}
10 changes: 10 additions & 0 deletions hack/do.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ bench() {
go test -bench=./... ./...
}

fuzz() {
packages=$(go list ./... | grep -v /vendor/)
for pkg in $packages; do
if go test -list "^Fuzz" "$pkg" | grep -q "^Fuzz"; then
echo "Fuzzing package: $pkg"
go test -fuzz=Fuzz -fuzztime=30s "$pkg" "$@"
fi
done
}

vet() {
go vet ./...
}
Expand Down