github.com/v2fly/tools@v0.100.0/internal/lsp/fuzzy/matcher_test.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Benchmark results: 6 // 7 // BenchmarkMatcher-12 1000000 1615 ns/op 30.95 MB/s 0 B/op 0 allocs/op 8 // 9 package fuzzy_test 10 11 import ( 12 "bytes" 13 "fmt" 14 "math" 15 "testing" 16 17 "github.com/v2fly/tools/internal/lsp/fuzzy" 18 ) 19 20 type comparator struct { 21 f func(val, ref float32) bool 22 descr string 23 } 24 25 var ( 26 eq = comparator{ 27 f: func(val, ref float32) bool { 28 return val == ref 29 }, 30 descr: "==", 31 } 32 ge = comparator{ 33 f: func(val, ref float32) bool { 34 return val >= ref 35 }, 36 descr: ">=", 37 } 38 gt = comparator{ 39 f: func(val, ref float32) bool { 40 return val > ref 41 }, 42 descr: ">", 43 } 44 ) 45 46 func (c comparator) eval(val, ref float32) bool { 47 return c.f(val, ref) 48 } 49 50 func (c comparator) String() string { 51 return c.descr 52 } 53 54 type scoreTest struct { 55 candidate string 56 comparator 57 ref float32 58 } 59 60 var matcherTests = []struct { 61 pattern string 62 tests []scoreTest 63 }{ 64 { 65 pattern: "", 66 tests: []scoreTest{ 67 {"def", eq, 1}, 68 {"Ab stuff c", eq, 1}, 69 }, 70 }, 71 { 72 pattern: "abc", 73 tests: []scoreTest{ 74 {"def", eq, 0}, 75 {"abd", eq, 0}, 76 {"abc", ge, 0}, 77 {"Abc", ge, 0}, 78 {"Ab stuff c", ge, 0}, 79 }, 80 }, 81 { 82 pattern: "Abc", 83 tests: []scoreTest{ 84 {"def", eq, 0}, 85 {"abd", eq, 0}, 86 {"abc", ge, 0}, 87 {"Abc", ge, 0}, 88 {"Ab stuff c", ge, 0}, 89 }, 90 }, 91 { 92 pattern: "U", 93 tests: []scoreTest{ 94 {"ErrUnexpectedEOF", gt, 0}, 95 {"ErrUnexpectedEOF.Error", eq, 0}, 96 }, 97 }, 98 } 99 100 func TestScore(t *testing.T) { 101 for _, tc := range matcherTests { 102 m := fuzzy.NewMatcher(tc.pattern) 103 for _, sct := range tc.tests { 104 score := m.Score(sct.candidate) 105 if !sct.comparator.eval(score, sct.ref) { 106 t.Errorf("m.Score(%q) = %.2g, want %s %v", sct.candidate, score, sct.comparator, sct.ref) 107 } 108 } 109 } 110 } 111 112 var compareCandidatesTestCases = []struct { 113 pattern string 114 orderedCandidates []string 115 }{ 116 { 117 pattern: "Foo", 118 orderedCandidates: []string{ 119 "Barfoo", 120 "Faoo", 121 "F_o_o", 122 "FaoFooa", 123 "BarFoo", 124 "F__oo", 125 "F_oo", 126 "FooA", 127 "FooBar", 128 "Foo", 129 }, 130 }, 131 { 132 pattern: "U", 133 orderedCandidates: []string{ 134 "ErrUnexpectedEOF.Error", 135 "ErrUnexpectedEOF", 136 }, 137 }, 138 } 139 140 func TestCompareCandidateScores(t *testing.T) { 141 for _, tc := range compareCandidatesTestCases { 142 m := fuzzy.NewMatcher(tc.pattern) 143 144 var prevScore float32 145 prevCand := "MIN_SCORE" 146 for _, cand := range tc.orderedCandidates { 147 score := m.Score(cand) 148 if prevScore > score { 149 t.Errorf("%s[=%v] is scored lower than %s[=%v]", cand, score, prevCand, prevScore) 150 } 151 if score < -1 || score > 1 { 152 t.Errorf("%s score is %v; want value between [-1, 1]", cand, score) 153 } 154 prevScore = score 155 prevCand = cand 156 } 157 } 158 } 159 160 var fuzzyMatcherTestCases = []struct { 161 p string 162 str string 163 want string 164 }{ 165 {p: "foo", str: "abc::foo", want: "abc::[foo]"}, 166 {p: "foo", str: "foo.foo", want: "foo.[foo]"}, 167 {p: "foo", str: "fo_oo.o_oo", want: "[fo]_oo.[o]_oo"}, 168 {p: "foo", str: "fo_oo.fo_oo", want: "fo_oo.[fo]_[o]o"}, 169 {p: "fo_o", str: "fo_oo.o_oo", want: "[f]o_oo.[o_o]o"}, 170 {p: "fOO", str: "fo_oo.o_oo", want: "[f]o_oo.[o]_[o]o"}, 171 {p: "tedit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]"}, 172 {p: "TEdit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]"}, 173 {p: "Tedit", str: "foo.TextEdit", want: "foo.[T]ext[Edit]"}, 174 {p: "Tedit", str: "foo.Textedit", want: "foo.[Te]xte[dit]"}, 175 {p: "TEdit", str: "foo.Textedit", want: ""}, 176 {p: "te", str: "foo.Textedit", want: "foo.[Te]xtedit"}, 177 {p: "ee", str: "foo.Textedit", want: ""}, // short middle of the word match 178 {p: "ex", str: "foo.Textedit", want: "foo.T[ex]tedit"}, 179 {p: "exdi", str: "foo.Textedit", want: ""}, // short middle of the word match 180 {p: "exdit", str: "foo.Textedit", want: ""}, // short middle of the word match 181 {p: "extdit", str: "foo.Textedit", want: "foo.T[ext]e[dit]"}, 182 {p: "e", str: "foo.Textedit", want: "foo.T[e]xtedit"}, 183 {p: "E", str: "foo.Textedit", want: "foo.T[e]xtedit"}, 184 {p: "ed", str: "foo.Textedit", want: "foo.Text[ed]it"}, 185 {p: "edt", str: "foo.Textedit", want: ""}, // short middle of the word match 186 {p: "edit", str: "foo.Textedit", want: "foo.Text[edit]"}, 187 {p: "edin", str: "foo.TexteditNum", want: "foo.Text[edi]t[N]um"}, 188 {p: "n", str: "node.GoNodeMax", want: "[n]ode.GoNodeMax"}, 189 {p: "N", str: "node.GoNodeMax", want: "[n]ode.GoNodeMax"}, 190 {p: "completio", str: "completion", want: "[completio]n"}, 191 {p: "completio", str: "completion.None", want: "[completio]n.None"}, 192 } 193 194 func TestFuzzyMatcherRanges(t *testing.T) { 195 for _, tc := range fuzzyMatcherTestCases { 196 matcher := fuzzy.NewMatcher(tc.p) 197 score := matcher.Score(tc.str) 198 if tc.want == "" { 199 if score > 0 { 200 t.Errorf("Score(%s, %s) = %v; want: <= 0", tc.p, tc.str, score) 201 } 202 continue 203 } 204 if score < 0 { 205 t.Errorf("Score(%s, %s) = %v, want: > 0", tc.p, tc.str, score) 206 continue 207 } 208 got := highlightMatches(tc.str, matcher) 209 if tc.want != got { 210 t.Errorf("highlightMatches(%s, %s) = %v, want: %v", tc.p, tc.str, got, tc.want) 211 } 212 } 213 } 214 215 var scoreTestCases = []struct { 216 p string 217 str string 218 want float64 219 }{ 220 // Score precision up to five digits. Modify if changing the score, but make sure the new values 221 // are reasonable. 222 {p: "abc", str: "abc", want: 1}, 223 {p: "abc", str: "Abc", want: 1}, 224 {p: "abc", str: "Abcdef", want: 1}, 225 {p: "strc", str: "StrCat", want: 1}, 226 {p: "abc_def", str: "abc_def_xyz", want: 1}, 227 {p: "abcdef", str: "abc_def_xyz", want: 0.91667}, 228 {p: "abcxyz", str: "abc_def_xyz", want: 0.91667}, 229 {p: "sc", str: "StrCat", want: 0.75}, 230 {p: "abc", str: "AbstrBasicCtor", want: 0.83333}, 231 {p: "foo", str: "abc::foo", want: 0.91667}, 232 {p: "afoo", str: "abc::foo", want: 0.9375}, 233 {p: "abr", str: "abc::bar", want: 0.5}, 234 {p: "br", str: "abc::bar", want: 0.25}, 235 {p: "aar", str: "abc::bar", want: 0.41667}, 236 {p: "edin", str: "foo.TexteditNum", want: 0.125}, 237 {p: "ediu", str: "foo.TexteditNum", want: 0}, 238 // We want the next two items to have roughly similar scores. 239 {p: "up", str: "unique_ptr", want: 0.75}, 240 {p: "up", str: "upper_bound", want: 1}, 241 } 242 243 func TestScores(t *testing.T) { 244 for _, tc := range scoreTestCases { 245 matcher := fuzzy.NewMatcher(tc.p) 246 got := math.Round(float64(matcher.Score(tc.str))*1e5) / 1e5 247 if got != tc.want { 248 t.Errorf("Score(%s, %s) = %v, want: %v", tc.p, tc.str, got, tc.want) 249 } 250 } 251 } 252 253 func highlightMatches(str string, matcher *fuzzy.Matcher) string { 254 matches := matcher.MatchedRanges() 255 256 var buf bytes.Buffer 257 index := 0 258 for i := 0; i < len(matches)-1; i += 2 { 259 s, e := matches[i], matches[i+1] 260 fmt.Fprintf(&buf, "%s[%s]", str[index:s], str[s:e]) 261 index = e 262 } 263 buf.WriteString(str[index:]) 264 return buf.String() 265 } 266 267 func BenchmarkMatcher(b *testing.B) { 268 pattern := "Foo" 269 candidates := []string{ 270 "F_o_o", 271 "Barfoo", 272 "Faoo", 273 "F__oo", 274 "F_oo", 275 "FaoFooa", 276 "BarFoo", 277 "FooA", 278 "FooBar", 279 "Foo", 280 } 281 282 matcher := fuzzy.NewMatcher(pattern) 283 284 b.ResetTimer() 285 for i := 0; i < b.N; i++ { 286 for _, c := range candidates { 287 matcher.Score(c) 288 } 289 } 290 var numBytes int 291 for _, c := range candidates { 292 numBytes += len(c) 293 } 294 b.SetBytes(int64(numBytes)) 295 }