current commit
260810.1 · Clean up comments and tests
package algo

// Equivalence tests for the single- and two-character fast paths against the
// general FuzzyMatchV2 algorithm, which serves as the reference.
//
// Two complementary strategies:
//   - Exhaustive: every string up to a fixed length over an alphabet that
//     covers all ASCII character classes, so every local scoring branch is
//     exercised (not merely sampled).
//   - Fuzz: coverage-guided, arbitrary length, to reach cases the bounded
//     exhaustive sweep cannot (e.g. long gaps where a high score decays).

import (
	"testing"

	"github.com/junegunn/fzf/src/util"
)

// One char of each ASCII class that affects scoring: lower, upper, delimiter,
// number, non-word, whitespace.
var equivAlphabet = []byte{'a', 'B', '/', '1', '_', ' '}

func samePos(a, b *[]int) bool {
	if (a == nil) != (b == nil) {
		return false
	}
	if a == nil {
		return true
	}
	return equalInts(*a, *b)
}

// compareFastPath runs a single input/pattern/params pair through both the
// fast path and the general algorithm and fails on any difference.
// It toggles the package-global disable* hook, so tests that call it must
// not run in parallel.
func compareFastPath(t *testing.T, chars *util.Chars, pattern []rune, cs, norm, fwd, wp bool, disable *bool, slab *util.Slab) {
	t.Helper()
	*disable = true
	rg, pg := FuzzyMatchV2(cs, norm, fwd, chars, pattern, wp, slab)
	*disable = false
	rt, pt := FuzzyMatchV2(cs, norm, fwd, chars, pattern, wp, slab)
	if rg != rt || !samePos(pg, pt) {
		t.Fatalf("mismatch item=%q pat=%q cs=%v norm=%v fwd=%v wp=%v: general %v %v vs fast %v %v",
			chars.ToString(), string(pattern), cs, norm, fwd, wp, rg, pg, rt, pt)
	}
}

// lowerPattern lowercases the pattern for case-insensitive search, matching
// the Algo contract (the pattern is pre-lowercased by BuildPattern).
func lowerPattern(p []rune, cs bool) []rune {
	if cs {
		return p
	}
	out := make([]rune, len(p))
	for i, c := range p {
		if c >= 'A' && c <= 'Z' {
			c += 32
		}
		out[i] = c
	}
	return out
}

func runExhaustive(t *testing.T, patLen, maxLen int, disable *bool) {
	// All patterns of length patLen over the alphabet
	var pats [][]rune
	var genPat func(cur []rune)
	genPat = func(cur []rune) {
		if len(cur) == patLen {
			pats = append(pats, append([]rune(nil), cur...))
			return
		}
		for _, c := range equivAlphabet {
			genPat(append(cur, rune(c)))
		}
	}
	genPat(nil)

	slab := util.MakeSlab(100*1024, 2048)
	buf := make([]byte, 0, maxLen)
	var rec func(depth int)
	rec = func(depth int) {
		chars := util.ToChars(append([]byte(nil), buf...))
		// normalize is not varied: normalizeRune is the identity below 0xC0,
		// and the general algorithm skips normalization on its ASCII branch,
		// so it cannot change the result for the ASCII-only fast path. The
		// fuzz test exercises normalize=true as a guard against that changing.
		for _, cs := range []bool{false, true} {
			for _, fwd := range []bool{true, false} {
				for _, wp := range []bool{false, true} {
					for _, p := range pats {
						compareFastPath(t, &chars, lowerPattern(p, cs), cs, false, fwd, wp, disable, slab)
					}
				}
			}
		}
		if depth == maxLen {
			return
		}
		for _, c := range equivAlphabet {
			buf = append(buf, c)
			rec(depth + 1)
			buf = buf[:len(buf)-1]
		}
	}
	rec(0)
}

func TestFuzzyMatchV2SingleExhaustive(t *testing.T) {
	runExhaustive(t, 1, 6, &disableSingle)
}

func TestFuzzyMatchV2TwoExhaustive(t *testing.T) {
	runExhaustive(t, 2, 6, &disableTwo)
}

func fuzzFastPath(f *testing.F, patLen int, disable *bool) {
	inputs := []string{"core_color/view/server.txt", "a                        b",
		"XyZ/123_abc.def", "aaaaaaaaaaaaaaaaaaaaaaaa"}
	// Seed patterns of the right length; wrong-length seeds are rejected below
	// and would waste the corpus.
	seedPats := map[int][]string{1: {"c", "a", "/"}, 2: {"co", "ab", "z1", "aa"}}[patLen]
	for _, in := range inputs {
		for _, p := range seedPats {
			f.Add(in, p)
		}
	}
	slab := util.MakeSlab(200*1024, 4096)
	f.Fuzz(func(t *testing.T, input, pat string) {
		r := []rune(pat)
		if len(r) != patLen {
			return
		}
		for _, c := range r {
			if c >= 128 {
				return
			}
		}
		chars := util.ToChars([]byte(input))
		if !chars.IsBytes() {
			return
		}
		for _, cs := range []bool{false, true} {
			for _, norm := range []bool{false, true} {
				for _, fwd := range []bool{true, false} {
					for _, wp := range []bool{false, true} {
						compareFastPath(t, &chars, lowerPattern(r, cs), cs, norm, fwd, wp, disable, slab)
					}
				}
			}
		}
	})
}

func FuzzFuzzyMatchV2Single(f *testing.F) { fuzzFastPath(f, 1, &disableSingle) }
func FuzzFuzzyMatchV2Two(f *testing.F)    { fuzzFastPath(f, 2, &disableTwo) }