blob: c03f8f3d88fa88b4f783b12b99f55ae6e169df0b [file] [log] [blame]
Sam McCallfad12f82017-12-01 17:08:02 +00001//===-- FuzzyMatchTests.cpp - String fuzzy matcher tests --------*- C++ -*-===//
2//
3// The LLVM Compiler Infrastructure
4//
5// This file is distributed under the University of Illinois Open Source
6// License. See LICENSE.TXT for details.
7//
8//===----------------------------------------------------------------------===//
9
10#include "FuzzyMatch.h"
11
12#include "llvm/ADT/StringExtras.h"
13#include "gmock/gmock.h"
14#include "gtest/gtest.h"
15
16namespace clang {
17namespace clangd {
18namespace {
19using namespace llvm;
20using testing::Not;
21
22struct ExpectedMatch {
Sam McCalleff7cf92018-03-05 17:34:33 +000023 // Annotations are optional, and will not be asserted if absent.
24 ExpectedMatch(StringRef Match) : Word(Match), Annotated(Match) {
Sam McCallfad12f82017-12-01 17:08:02 +000025 for (char C : "[]")
26 Word.erase(std::remove(Word.begin(), Word.end(), C), Word.end());
Sam McCalleff7cf92018-03-05 17:34:33 +000027 if (Word.size() == Annotated->size())
28 Annotated = None;
29 }
30 bool accepts(StringRef ActualAnnotated) const {
31 return !Annotated || ActualAnnotated == *Annotated;
Sam McCallfad12f82017-12-01 17:08:02 +000032 }
Sam McCalleff7cf92018-03-05 17:34:33 +000033
34 friend raw_ostream &operator<<(raw_ostream &OS, const ExpectedMatch &M) {
Sam McCall89f02bf2018-03-06 14:30:07 +000035 OS << "'" << M.Word;
Sam McCalleff7cf92018-03-05 17:34:33 +000036 if (M.Annotated)
37 OS << "' as " << *M.Annotated;
Sam McCall89f02bf2018-03-06 14:30:07 +000038 return OS;
Sam McCalleff7cf92018-03-05 17:34:33 +000039 }
40
Sam McCall89f02bf2018-03-06 14:30:07 +000041 std::string Word;
42
Sam McCalleff7cf92018-03-05 17:34:33 +000043private:
44 Optional<StringRef> Annotated;
Sam McCallfad12f82017-12-01 17:08:02 +000045};
Sam McCallfad12f82017-12-01 17:08:02 +000046
47struct MatchesMatcher : public testing::MatcherInterface<StringRef> {
48 ExpectedMatch Candidate;
Sam McCalla34ac6d2018-06-06 12:38:37 +000049 Optional<float> Score;
50 MatchesMatcher(ExpectedMatch Candidate, Optional<float> Score)
51 : Candidate(std::move(Candidate)), Score(Score) {}
Sam McCallfad12f82017-12-01 17:08:02 +000052
53 void DescribeTo(::std::ostream *OS) const override {
54 raw_os_ostream(*OS) << "Matches " << Candidate;
Sam McCalla34ac6d2018-06-06 12:38:37 +000055 if (Score)
56 *OS << " with score " << *Score;
Sam McCallfad12f82017-12-01 17:08:02 +000057 }
58
59 bool MatchAndExplain(StringRef Pattern,
60 testing::MatchResultListener *L) const override {
61 std::unique_ptr<raw_ostream> OS(
62 L->stream() ? (raw_ostream *)(new raw_os_ostream(*L->stream()))
63 : new raw_null_ostream());
64 FuzzyMatcher Matcher(Pattern);
65 auto Result = Matcher.match(Candidate.Word);
66 auto AnnotatedMatch = Matcher.dumpLast(*OS << "\n");
Sam McCalla34ac6d2018-06-06 12:38:37 +000067 return Result && Candidate.accepts(AnnotatedMatch) &&
68 (!Score || testing::Value(*Result, testing::FloatEq(*Score)));
Sam McCallfad12f82017-12-01 17:08:02 +000069 }
70};
71
Sam McCalla34ac6d2018-06-06 12:38:37 +000072// Accepts patterns that match a given word, optionally requiring a score.
Sam McCallfad12f82017-12-01 17:08:02 +000073// Dumps the debug tables on match failure.
Sam McCalla34ac6d2018-06-06 12:38:37 +000074testing::Matcher<StringRef> matches(StringRef M, Optional<float> Score = {}) {
75 return testing::MakeMatcher<StringRef>(new MatchesMatcher(M, Score));
Sam McCallfad12f82017-12-01 17:08:02 +000076}
77
78TEST(FuzzyMatch, Matches) {
Sam McCall7be1b562018-01-13 16:46:26 +000079 EXPECT_THAT("", matches("unique_ptr"));
Sam McCallfad12f82017-12-01 17:08:02 +000080 EXPECT_THAT("u_p", matches("[u]nique[_p]tr"));
81 EXPECT_THAT("up", matches("[u]nique_[p]tr"));
Sam McCall15034512018-06-14 13:50:30 +000082 EXPECT_THAT("uq", Not(matches("unique_ptr")));
Sam McCallfad12f82017-12-01 17:08:02 +000083 EXPECT_THAT("qp", Not(matches("unique_ptr")));
84 EXPECT_THAT("log", Not(matches("SVGFEMorphologyElement")));
85
86 EXPECT_THAT("tit", matches("win.[tit]"));
87 EXPECT_THAT("title", matches("win.[title]"));
88 EXPECT_THAT("WordCla", matches("[Word]Character[Cla]ssifier"));
89 EXPECT_THAT("WordCCla", matches("[WordC]haracter[Cla]ssifier"));
90
91 EXPECT_THAT("dete", Not(matches("editor.quickSuggestionsDelay")));
92
93 EXPECT_THAT("highlight", matches("editorHover[Highlight]"));
94 EXPECT_THAT("hhighlight", matches("editor[H]over[Highlight]"));
95 EXPECT_THAT("dhhighlight", Not(matches("editorHoverHighlight")));
96
97 EXPECT_THAT("-moz", matches("[-moz]-foo"));
98 EXPECT_THAT("moz", matches("-[moz]-foo"));
99 EXPECT_THAT("moza", matches("-[moz]-[a]nimation"));
100
101 EXPECT_THAT("ab", matches("[ab]A"));
Sam McCall15034512018-06-14 13:50:30 +0000102 EXPECT_THAT("ccm", Not(matches("cacmelCase")));
Sam McCallfad12f82017-12-01 17:08:02 +0000103 EXPECT_THAT("bti", Not(matches("the_black_knight")));
104 EXPECT_THAT("ccm", Not(matches("camelCase")));
105 EXPECT_THAT("cmcm", Not(matches("camelCase")));
106 EXPECT_THAT("BK", matches("the_[b]lack_[k]night"));
107 EXPECT_THAT("KeyboardLayout=", Not(matches("KeyboardLayout")));
108 EXPECT_THAT("LLL", matches("SVisual[L]ogger[L]ogs[L]ist"));
109 EXPECT_THAT("LLLL", Not(matches("SVilLoLosLi")));
110 EXPECT_THAT("LLLL", Not(matches("SVisualLoggerLogsList")));
111 EXPECT_THAT("TEdit", matches("[T]ext[Edit]"));
112 EXPECT_THAT("TEdit", matches("[T]ext[Edit]or"));
Sam McCall15034512018-06-14 13:50:30 +0000113 EXPECT_THAT("TEdit", Not(matches("[T]ext[edit]")));
Sam McCallfad12f82017-12-01 17:08:02 +0000114 EXPECT_THAT("TEdit", matches("[t]ext_[edit]"));
Sam McCall15034512018-06-14 13:50:30 +0000115 EXPECT_THAT("TEditDt", matches("[T]ext[Edit]or[D]ecoration[T]ype"));
Sam McCallfad12f82017-12-01 17:08:02 +0000116 EXPECT_THAT("TEdit", matches("[T]ext[Edit]orDecorationType"));
117 EXPECT_THAT("Tedit", matches("[T]ext[Edit]"));
118 EXPECT_THAT("ba", Not(matches("?AB?")));
119 EXPECT_THAT("bkn", matches("the_[b]lack_[kn]ight"));
Sam McCall15034512018-06-14 13:50:30 +0000120 EXPECT_THAT("bt", Not(matches("the_[b]lack_knigh[t]")));
121 EXPECT_THAT("ccm", Not(matches("[c]amelCase[cm]")));
122 EXPECT_THAT("fdm", Not(matches("[f]in[dM]odel")));
123 EXPECT_THAT("fob", Not(matches("[fo]o[b]ar")));
Sam McCallfad12f82017-12-01 17:08:02 +0000124 EXPECT_THAT("fobz", Not(matches("foobar")));
125 EXPECT_THAT("foobar", matches("[foobar]"));
126 EXPECT_THAT("form", matches("editor.[form]atOnSave"));
127 EXPECT_THAT("g p", matches("[G]it:[ P]ull"));
128 EXPECT_THAT("g p", matches("[G]it:[ P]ull"));
129 EXPECT_THAT("gip", matches("[Gi]t: [P]ull"));
130 EXPECT_THAT("gip", matches("[Gi]t: [P]ull"));
131 EXPECT_THAT("gp", matches("[G]it: [P]ull"));
132 EXPECT_THAT("gp", matches("[G]it_Git_[P]ull"));
133 EXPECT_THAT("is", matches("[I]mport[S]tatement"));
134 EXPECT_THAT("is", matches("[is]Valid"));
Sam McCall15034512018-06-14 13:50:30 +0000135 EXPECT_THAT("lowrd", Not(matches("[low]Wo[rd]")));
136 EXPECT_THAT("myvable", Not(matches("[myva]ria[ble]")));
Sam McCallfad12f82017-12-01 17:08:02 +0000137 EXPECT_THAT("no", Not(matches("")));
138 EXPECT_THAT("no", Not(matches("match")));
139 EXPECT_THAT("ob", Not(matches("foobar")));
140 EXPECT_THAT("sl", matches("[S]Visual[L]oggerLogsList"));
Sam McCall15034512018-06-14 13:50:30 +0000141 EXPECT_THAT("sllll", matches("[S]Visua[L]ogger[Ll]ama[L]ist"));
142 EXPECT_THAT("THRE", matches("H[T]ML[HRE]lement"));
Sam McCalleff7cf92018-03-05 17:34:33 +0000143 EXPECT_THAT("b", Not(matches("NDEBUG")));
Sam McCallfad12f82017-12-01 17:08:02 +0000144 EXPECT_THAT("Three", matches("[Three]"));
145 EXPECT_THAT("fo", Not(matches("barfoo")));
146 EXPECT_THAT("fo", matches("bar_[fo]o"));
147 EXPECT_THAT("fo", matches("bar_[Fo]o"));
148 EXPECT_THAT("fo", matches("bar [fo]o"));
149 EXPECT_THAT("fo", matches("bar.[fo]o"));
150 EXPECT_THAT("fo", matches("bar/[fo]o"));
151 EXPECT_THAT("fo", matches("bar\\[fo]o"));
152
153 EXPECT_THAT(
154 "aaaaaa",
155 matches("[aaaaaa]aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
156 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
157 EXPECT_THAT("baba", Not(matches("ababababab")));
158 EXPECT_THAT("fsfsfs", Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsa")));
159 EXPECT_THAT("fsfsfsfsfsfsfsf",
160 Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsafdsafdsafdsafdsfd"
161 "safdsfdfdfasdnfdsajfndsjnafjndsajlknfdsa")));
162
163 EXPECT_THAT(" g", matches("[ g]roup"));
164 EXPECT_THAT("g", matches(" [g]roup"));
165 EXPECT_THAT("g g", Not(matches(" groupGroup")));
166 EXPECT_THAT("g g", matches(" [g]roup[ G]roup"));
167 EXPECT_THAT(" g g", matches("[ ] [g]roup[ G]roup"));
168 EXPECT_THAT("zz", matches("[zz]Group"));
169 EXPECT_THAT("zzg", matches("[zzG]roup"));
170 EXPECT_THAT("g", matches("zz[G]roup"));
171
172 EXPECT_THAT("aaaa", matches("_a_[aaaa]")); // Prefer consecutive.
Sam McCalleff7cf92018-03-05 17:34:33 +0000173 // These would ideally match, but would need special segmentation rules.
174 EXPECT_THAT("printf", Not(matches("s[printf]")));
175 EXPECT_THAT("str", Not(matches("o[str]eam")));
Sam McCall15034512018-06-14 13:50:30 +0000176 EXPECT_THAT("strcpy", Not(matches("strncpy")));
177 EXPECT_THAT("std", Not(matches("PTHREAD_MUTEX_STALLED")));
178 EXPECT_THAT("std", Not(matches("pthread_condattr_setpshared")));
Sam McCallfad12f82017-12-01 17:08:02 +0000179}
180
181struct RankMatcher : public testing::MatcherInterface<StringRef> {
182 std::vector<ExpectedMatch> RankedStrings;
183 RankMatcher(std::initializer_list<ExpectedMatch> RankedStrings)
184 : RankedStrings(RankedStrings) {}
185
186 void DescribeTo(::std::ostream *OS) const override {
187 raw_os_ostream O(*OS);
188 O << "Ranks strings in order: [";
189 for (const auto &Str : RankedStrings)
190 O << "\n\t" << Str;
191 O << "\n]";
192 }
193
194 bool MatchAndExplain(StringRef Pattern,
195 testing::MatchResultListener *L) const override {
196 std::unique_ptr<raw_ostream> OS(
197 L->stream() ? (raw_ostream *)(new raw_os_ostream(*L->stream()))
198 : new raw_null_ostream());
199 FuzzyMatcher Matcher(Pattern);
200 const ExpectedMatch *LastMatch;
201 Optional<float> LastScore;
202 bool Ok = true;
203 for (const auto &Str : RankedStrings) {
204 auto Score = Matcher.match(Str.Word);
205 if (!Score) {
206 *OS << "\nDoesn't match '" << Str.Word << "'";
207 Matcher.dumpLast(*OS << "\n");
208 Ok = false;
209 } else {
210 std::string Buf;
211 llvm::raw_string_ostream Info(Buf);
212 auto AnnotatedMatch = Matcher.dumpLast(Info);
213
Sam McCalleff7cf92018-03-05 17:34:33 +0000214 if (!Str.accepts(AnnotatedMatch)) {
215 *OS << "\nDoesn't match " << Str << ", but " << AnnotatedMatch << "\n"
Sam McCallfad12f82017-12-01 17:08:02 +0000216 << Info.str();
217 Ok = false;
218 } else if (LastScore && *LastScore < *Score) {
219 *OS << "\nRanks '" << Str.Word << "'=" << *Score << " above '"
220 << LastMatch->Word << "'=" << *LastScore << "\n"
221 << Info.str();
222 Matcher.match(LastMatch->Word);
223 Matcher.dumpLast(*OS << "\n");
224 Ok = false;
225 }
226 }
227 LastMatch = &Str;
228 LastScore = Score;
229 }
230 return Ok;
231 }
232};
233
234// Accepts patterns that match all the strings and rank them in the given order.
235// Dumps the debug tables on match failure.
236template <typename... T> testing::Matcher<StringRef> ranks(T... RankedStrings) {
237 return testing::MakeMatcher<StringRef>(
238 new RankMatcher{ExpectedMatch(RankedStrings)...});
239}
240
241TEST(FuzzyMatch, Ranking) {
Sam McCallfad12f82017-12-01 17:08:02 +0000242 EXPECT_THAT("cons",
243 ranks("[cons]ole", "[Cons]ole", "ArrayBuffer[Cons]tructor"));
244 EXPECT_THAT("foo", ranks("[foo]", "[Foo]"));
Sam McCall15034512018-06-14 13:50:30 +0000245 EXPECT_THAT("onMes",
246 ranks("[onMes]sage", "[onmes]sage", "[on]This[M]ega[Es]capes"));
Sam McCallfad12f82017-12-01 17:08:02 +0000247 EXPECT_THAT("CC", ranks("[C]amel[C]ase", "[c]amel[C]ase"));
248 EXPECT_THAT("cC", ranks("[c]amel[C]ase", "[C]amel[C]ase"));
Sam McCalla34ac6d2018-06-06 12:38:37 +0000249 EXPECT_THAT("p", ranks("[p]", "[p]arse", "[p]osix", "[p]afdsa", "[p]ath"));
Sam McCallfad12f82017-12-01 17:08:02 +0000250 EXPECT_THAT("pa", ranks("[pa]rse", "[pa]th", "[pa]fdsa"));
251 EXPECT_THAT("log", ranks("[log]", "Scroll[Log]icalPosition"));
252 EXPECT_THAT("e", ranks("[e]lse", "Abstract[E]lement"));
253 EXPECT_THAT("workbench.sideb",
254 ranks("[workbench.sideB]ar.location",
255 "[workbench.]editor.default[SideB]ySideLayout"));
256 EXPECT_THAT("editor.r", ranks("[editor.r]enderControlCharacter",
257 "[editor.]overview[R]ulerlanes",
258 "diff[Editor.r]enderSideBySide"));
259 EXPECT_THAT("-mo", ranks("[-mo]z-columns", "[-]ms-ime-[mo]de"));
260 EXPECT_THAT("convertModelPosition",
261 ranks("[convertModelPosition]ToViewPosition",
262 "[convert]ViewTo[ModelPosition]"));
263 EXPECT_THAT("is", ranks("[is]ValidViewletId", "[i]mport [s]tatement"));
Sam McCall15034512018-06-14 13:50:30 +0000264 EXPECT_THAT("strcpy", ranks("[strcpy]", "[strcpy]_s"));
Sam McCallfad12f82017-12-01 17:08:02 +0000265}
266
Sam McCalla34ac6d2018-06-06 12:38:37 +0000267// Verify some bounds so we know scores fall in the right range.
268// Testing exact scores is fragile, so we prefer Ranking tests.
269TEST(FuzzyMatch, Scoring) {
Sam McCall15034512018-06-14 13:50:30 +0000270 EXPECT_THAT("abs", matches("[a]w[B]xYz[S]", 0.f));
Sam McCalla34ac6d2018-06-06 12:38:37 +0000271 EXPECT_THAT("abs", matches("[abs]l", 1.f));
272 EXPECT_THAT("abs", matches("[abs]", 2.f));
273 EXPECT_THAT("Abs", matches("[abs]", 2.f));
274}
275
Sam McCallb45d0732018-07-20 08:01:37 +0000276// Returns pretty-printed segmentation of Text.
277// e.g. std::basic_string --> +-- +---- +-----
278std::string segment(StringRef Text) {
279 std::vector<CharRole> Roles(Text.size());
280 calculateRoles(Text, Roles);
281 std::string Printed;
282 for (unsigned I = 0; I < Text.size(); ++I)
283 Printed.push_back("?-+ "[static_cast<unsigned>(Roles[I])]);
284 return Printed;
285}
286
287// this is a no-op hack so clang-format will vertically align our testcases.
288StringRef returns(StringRef Text) { return Text; }
289
290TEST(FuzzyMatch, Segmentation) {
291 EXPECT_THAT(segment("std::basic_string"), //
292 returns("+-- +---- +-----"));
293 EXPECT_THAT(segment("XMLHttpRequest"), //
294 returns("+--+---+------"));
295 EXPECT_THAT(segment("t3h PeNgU1N oF d00m!!!!!!!!"), //
296 returns("+-- +-+-+-+ ++ +--- "));
297}
298
Sam McCallfad12f82017-12-01 17:08:02 +0000299} // namespace
300} // namespace clangd
301} // namespace clang