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  }