// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Benchmark results: // // BenchmarkMatcher-12 1000000 1615 ns/op 30.95 MB/s 0 B/op 0 allocs/op // package fuzzy_test import ( "bytes" "fmt" "math" "testing" "golang.org/x/tools/internal/lsp/fuzzy" ) func ExampleMatcher() { pattern := "TEdit" candidates := []string{"fuzzy.TextEdit", "ArtEdit", "TED talks about IT"} // Create a fuzzy matcher for the pattern. matcher := fuzzy.NewMatcher(pattern, fuzzy.Text) for _, candidate := range candidates { // Compute candidate's score against the matcher. score := matcher.Score(candidate) if score > -1 { // Get the substrings in the candidate matching the pattern. ranges := matcher.MatchedRanges() fmt.Println(ranges) // Do something with the ranges. } } } type comparator struct { f func(val, ref float32) bool descr string } var ( eq = comparator{ f: func(val, ref float32) bool { return val == ref }, descr: "==", } ge = comparator{ f: func(val, ref float32) bool { return val >= ref }, descr: ">=", } ) func (c comparator) eval(val, ref float32) bool { return c.f(val, ref) } func (c comparator) String() string { return c.descr } type scoreTest struct { candidate string comparator ref float32 } var matcherTests = []struct { pattern string input fuzzy.Input tests []scoreTest }{ { pattern: "", input: fuzzy.Text, tests: []scoreTest{ {"def", eq, 1}, {"Ab stuff c", eq, 1}, }, }, { pattern: "abc", input: fuzzy.Text, tests: []scoreTest{ {"def", eq, -1}, {"abd", eq, -1}, {"abc", ge, 0}, {"Abc", ge, 0}, {"Ab stuff c", ge, 0}, }, }, { pattern: "Abc", input: fuzzy.Text, tests: []scoreTest{ {"def", eq, -1}, {"abd", eq, -1}, {"abc", ge, 0}, {"Abc", ge, 0}, {"Ab stuff c", ge, 0}, }, }, { pattern: "subs", input: fuzzy.Filename, tests: []scoreTest{ {"sub/seq", ge, 0}, {"sub/seq/end", ge, 0}, {"sub/seq/base", ge, 0}, }, }, { pattern: "subs", input: fuzzy.Filename, tests: []scoreTest{ {"//sub/seq", ge, 0}, {"//sub/seq/end", ge, 0}, {"//sub/seq/base", ge, 0}, }, }, } func TestScore(t *testing.T) { for _, tc := range matcherTests { m := fuzzy.NewMatcher(tc.pattern, tc.input) for _, sct := range tc.tests { score := m.Score(sct.candidate) if !sct.comparator.eval(score, sct.ref) { t.Errorf("not true that m.Score(%s)[=%v] %s %v", sct.candidate, score, sct.comparator, sct.ref) } } } } type candidateCompTest struct { c1 string comparator comparator c2 string } var compareCandidatesTestCases = []struct { pattern string input fuzzy.Input orderedCandidates []string }{ { pattern: "aa", input: fuzzy.Filename, orderedCandidates: []string{ "baab", "bb_aa", "a/a/a", "aa_bb", "aa_b", "aabb", "aab", "b/aa", }, }, { pattern: "Foo", input: fuzzy.Text, orderedCandidates: []string{ "Barfoo", "F_o_o", "Faoo", "F__oo", "F_oo", "FaoFooa", "BarFoo", "FooA", "FooBar", "Foo", }, }, } func TestCompareCandidateScores(t *testing.T) { for _, tc := range compareCandidatesTestCases { m := fuzzy.NewMatcher(tc.pattern, tc.input) var prevScore float32 prevCand := "MIN_SCORE" for _, cand := range tc.orderedCandidates { score := m.Score(cand) if prevScore > score { t.Errorf("%s[=%v] is scored lower than %s[=%v]", cand, score, prevCand, prevScore) } if score < -1 || score > 1 { t.Errorf("%s score is %v; want value between [-1, 1]", cand, score) } prevScore = score prevCand = cand } } } var fuzzyMatcherTestCases = []struct { p string str string want string input fuzzy.Input }{ // fuzzy.Filename {p: "aa", str: "a_a/a_a", want: "[a]_a/[a]_a", input: fuzzy.Filename}, {p: "aaaa", str: "a_a/a_a", want: "[a]_[a]/[a]_[a]", input: fuzzy.Filename}, {p: "aaaa", str: "aaaa", want: "[aaaa]", input: fuzzy.Filename}, {p: "aaaa", str: "a_a/a_aaaa", want: "a_a/[a]_[aaa]a", input: fuzzy.Filename}, {p: "aaaa", str: "a_a/aaaaa", want: "a_a/[aaaa]a", input: fuzzy.Filename}, {p: "aaaa", str: "aabaaa", want: "[aa]b[aa]a", input: fuzzy.Filename}, {p: "aaaa", str: "a/baaa", want: "[a]/b[aaa]", input: fuzzy.Filename}, {p: "abcxz", str: "d/abc/abcd/oxz", want: "d/[abc]/abcd/o[xz]", input: fuzzy.Filename}, {p: "abcxz", str: "d/abcd/abc/oxz", want: "d/[abc]d/abc/o[xz]", input: fuzzy.Filename}, // fuzzy.Symbol {p: "foo", str: "abc::foo", want: "abc::[foo]", input: fuzzy.Symbol}, {p: "foo", str: "foo.foo", want: "foo.[foo]", input: fuzzy.Symbol}, {p: "foo", str: "fo_oo.o_oo", want: "[fo]_oo.[o]_oo", input: fuzzy.Symbol}, {p: "foo", str: "fo_oo.fo_oo", want: "fo_oo.[fo]_[o]o", input: fuzzy.Symbol}, {p: "fo_o", str: "fo_oo.o_oo", want: "[f]o_oo.[o_o]o", input: fuzzy.Symbol}, {p: "fOO", str: "fo_oo.o_oo", want: "[f]o_oo.[o]_[o]o", input: fuzzy.Symbol}, {p: "tedit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]", input: fuzzy.Symbol}, {p: "TEdit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]", input: fuzzy.Symbol}, {p: "Tedit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]", input: fuzzy.Symbol}, {p: "Tedit", str: "foo.Textedit", want: "foo.[Te]xte[dit]", input: fuzzy.Symbol}, {p: "TEdit", str: "foo.Textedit", want: "", input: fuzzy.Symbol}, {p: "te", str: "foo.Textedit", want: "foo.[Te]xtedit", input: fuzzy.Symbol}, {p: "ee", str: "foo.Textedit", want: "", input: fuzzy.Symbol}, // short middle of the word match {p: "ex", str: "foo.Textedit", want: "foo.T[ex]tedit", input: fuzzy.Symbol}, {p: "exdi", str: "foo.Textedit", want: "", input: fuzzy.Symbol}, // short middle of the word match {p: "exdit", str: "foo.Textedit", want: "", input: fuzzy.Symbol}, // short middle of the word match {p: "extdit", str: "foo.Textedit", want: "foo.T[ext]e[dit]", input: fuzzy.Symbol}, {p: "e", str: "foo.Textedit", want: "foo.T[e]xtedit", input: fuzzy.Symbol}, {p: "E", str: "foo.Textedit", want: "foo.T[e]xtedit", input: fuzzy.Symbol}, {p: "ed", str: "foo.Textedit", want: "foo.Text[ed]it", input: fuzzy.Symbol}, {p: "edt", str: "foo.Textedit", want: "", input: fuzzy.Symbol}, // short middle of the word match {p: "edit", str: "foo.Textedit", want: "foo.Text[edit]", input: fuzzy.Symbol}, {p: "edin", str: "foo.TexteditNum", want: "foo.Text[edi]t[N]um", input: fuzzy.Symbol}, {p: "n", str: "node.GoNodeMax", want: "[n]ode.GoNodeMax", input: fuzzy.Symbol}, {p: "N", str: "node.GoNodeMax", want: "[n]ode.GoNodeMax", input: fuzzy.Symbol}, {p: "completio", str: "completion", want: "[completio]n", input: fuzzy.Symbol}, {p: "completio", str: "completion.None", want: "[completio]n.None", input: fuzzy.Symbol}, } func TestFuzzyMatcherRanges(t *testing.T) { for _, tc := range fuzzyMatcherTestCases { matcher := fuzzy.NewMatcher(tc.p, tc.input) score := matcher.Score(tc.str) if tc.want == "" { if score >= 0 { t.Errorf("Score(%s, %s) = %v; want: <= 0", tc.p, tc.str, score) } continue } if score < 0 { t.Errorf("Score(%s, %s) = %v, want: > 0", tc.p, tc.str, score) continue } got := highlightMatches(tc.str, matcher) if tc.want != got { t.Errorf("highlightMatches(%s, %s) = %v, want: %v", tc.p, tc.str, got, tc.want) } } } var scoreTestCases = []struct { p string str string want float64 }{ // Score precision up to five digits. Modify if changing the score, but make sure the new values // are reasonable. {p: "abc", str: "abc", want: 1}, {p: "abc", str: "Abc", want: 1}, {p: "abc", str: "Abcdef", want: 1}, {p: "strc", str: "StrCat", want: 1}, {p: "abc_def", str: "abc_def_xyz", want: 1}, {p: "abcdef", str: "abc_def_xyz", want: 0.91667}, {p: "abcxyz", str: "abc_def_xyz", want: 0.875}, {p: "sc", str: "StrCat", want: 0.75}, {p: "abc", str: "AbstrBasicCtor", want: 0.75}, {p: "foo", str: "abc::foo", want: 1}, {p: "afoo", str: "abc::foo", want: 0.9375}, {p: "abr", str: "abc::bar", want: 0.5}, {p: "br", str: "abc::bar", want: 0.375}, {p: "aar", str: "abc::bar", want: 0.16667}, {p: "edin", str: "foo.TexteditNum", want: 0}, {p: "ediu", str: "foo.TexteditNum", want: 0}, // We want the next two items to have roughly similar scores. {p: "up", str: "unique_ptr", want: 0.75}, {p: "up", str: "upper_bound", want: 1}, } func TestScores(t *testing.T) { for _, tc := range scoreTestCases { matcher := fuzzy.NewMatcher(tc.p, fuzzy.Symbol) got := math.Round(float64(matcher.Score(tc.str))*1e5) / 1e5 if got != tc.want { t.Errorf("Score(%s, %s) = %v, want: %v", tc.p, tc.str, got, tc.want) } } } func highlightMatches(str string, matcher *fuzzy.Matcher) string { matches := matcher.MatchedRanges() var buf bytes.Buffer index := 0 for i := 0; i < len(matches)-1; i += 2 { s, e := matches[i], matches[i+1] fmt.Fprintf(&buf, "%s[%s]", str[index:s], str[s:e]) index = e } buf.WriteString(str[index:]) return buf.String() } func BenchmarkMatcher(b *testing.B) { pattern := "Foo" candidates := []string{ "F_o_o", "Barfoo", "Faoo", "F__oo", "F_oo", "FaoFooa", "BarFoo", "FooA", "FooBar", "Foo", } matcher := fuzzy.NewMatcher(pattern, fuzzy.Text) b.ResetTimer() for i := 0; i < b.N; i++ { for _, c := range candidates { matcher.Score(c) } } var numBytes int for _, c := range candidates { numBytes += len(c) } b.SetBytes(int64(numBytes)) }