Sam McCall | 8749641 | 2017-12-01 17:08:02 +0000 | [diff] [blame] | 1 | //===-- 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 | |
| 16 | namespace clang { |
| 17 | namespace clangd { |
| 18 | namespace { |
| 19 | using namespace llvm; |
| 20 | using testing::Not; |
| 21 | |
| 22 | struct ExpectedMatch { |
| 23 | ExpectedMatch(StringRef Annotated) : Word(Annotated), Annotated(Annotated) { |
| 24 | for (char C : "[]") |
| 25 | Word.erase(std::remove(Word.begin(), Word.end(), C), Word.end()); |
| 26 | } |
| 27 | std::string Word; |
| 28 | StringRef Annotated; |
| 29 | }; |
| 30 | raw_ostream &operator<<(raw_ostream &OS, const ExpectedMatch &M) { |
| 31 | return OS << "'" << M.Word << "' as " << M.Annotated; |
| 32 | } |
| 33 | |
| 34 | struct MatchesMatcher : public testing::MatcherInterface<StringRef> { |
| 35 | ExpectedMatch Candidate; |
| 36 | MatchesMatcher(ExpectedMatch Candidate) : Candidate(std::move(Candidate)) {} |
| 37 | |
| 38 | void DescribeTo(::std::ostream *OS) const override { |
| 39 | raw_os_ostream(*OS) << "Matches " << Candidate; |
| 40 | } |
| 41 | |
| 42 | bool MatchAndExplain(StringRef Pattern, |
| 43 | testing::MatchResultListener *L) const override { |
| 44 | std::unique_ptr<raw_ostream> OS( |
| 45 | L->stream() ? (raw_ostream *)(new raw_os_ostream(*L->stream())) |
| 46 | : new raw_null_ostream()); |
| 47 | FuzzyMatcher Matcher(Pattern); |
| 48 | auto Result = Matcher.match(Candidate.Word); |
| 49 | auto AnnotatedMatch = Matcher.dumpLast(*OS << "\n"); |
| 50 | return Result && AnnotatedMatch == Candidate.Annotated; |
| 51 | } |
| 52 | }; |
| 53 | |
| 54 | // Accepts patterns that match a given word. |
| 55 | // Dumps the debug tables on match failure. |
| 56 | testing::Matcher<StringRef> matches(StringRef M) { |
| 57 | return testing::MakeMatcher<StringRef>(new MatchesMatcher(M)); |
| 58 | } |
| 59 | |
| 60 | TEST(FuzzyMatch, Matches) { |
Sam McCall | 09b4be5 | 2018-01-13 16:46:26 +0000 | [diff] [blame^] | 61 | EXPECT_THAT("", matches("unique_ptr")); |
Sam McCall | 8749641 | 2017-12-01 17:08:02 +0000 | [diff] [blame] | 62 | EXPECT_THAT("u_p", matches("[u]nique[_p]tr")); |
| 63 | EXPECT_THAT("up", matches("[u]nique_[p]tr")); |
| 64 | EXPECT_THAT("uq", matches("[u]ni[q]ue_ptr")); |
| 65 | EXPECT_THAT("qp", Not(matches("unique_ptr"))); |
| 66 | EXPECT_THAT("log", Not(matches("SVGFEMorphologyElement"))); |
| 67 | |
| 68 | EXPECT_THAT("tit", matches("win.[tit]")); |
| 69 | EXPECT_THAT("title", matches("win.[title]")); |
| 70 | EXPECT_THAT("WordCla", matches("[Word]Character[Cla]ssifier")); |
| 71 | EXPECT_THAT("WordCCla", matches("[WordC]haracter[Cla]ssifier")); |
| 72 | |
| 73 | EXPECT_THAT("dete", Not(matches("editor.quickSuggestionsDelay"))); |
| 74 | |
| 75 | EXPECT_THAT("highlight", matches("editorHover[Highlight]")); |
| 76 | EXPECT_THAT("hhighlight", matches("editor[H]over[Highlight]")); |
| 77 | EXPECT_THAT("dhhighlight", Not(matches("editorHoverHighlight"))); |
| 78 | |
| 79 | EXPECT_THAT("-moz", matches("[-moz]-foo")); |
| 80 | EXPECT_THAT("moz", matches("-[moz]-foo")); |
| 81 | EXPECT_THAT("moza", matches("-[moz]-[a]nimation")); |
| 82 | |
| 83 | EXPECT_THAT("ab", matches("[ab]A")); |
| 84 | EXPECT_THAT("ccm", matches("[c]a[cm]elCase")); |
| 85 | EXPECT_THAT("bti", Not(matches("the_black_knight"))); |
| 86 | EXPECT_THAT("ccm", Not(matches("camelCase"))); |
| 87 | EXPECT_THAT("cmcm", Not(matches("camelCase"))); |
| 88 | EXPECT_THAT("BK", matches("the_[b]lack_[k]night")); |
| 89 | EXPECT_THAT("KeyboardLayout=", Not(matches("KeyboardLayout"))); |
| 90 | EXPECT_THAT("LLL", matches("SVisual[L]ogger[L]ogs[L]ist")); |
| 91 | EXPECT_THAT("LLLL", Not(matches("SVilLoLosLi"))); |
| 92 | EXPECT_THAT("LLLL", Not(matches("SVisualLoggerLogsList"))); |
| 93 | EXPECT_THAT("TEdit", matches("[T]ext[Edit]")); |
| 94 | EXPECT_THAT("TEdit", matches("[T]ext[Edit]or")); |
| 95 | EXPECT_THAT("TEdit", matches("[Te]xte[dit]")); |
| 96 | EXPECT_THAT("TEdit", matches("[t]ext_[edit]")); |
| 97 | EXPECT_THAT("TEditDit", matches("[T]ext[Edit]or[D]ecorat[i]on[T]ype")); |
| 98 | EXPECT_THAT("TEdit", matches("[T]ext[Edit]orDecorationType")); |
| 99 | EXPECT_THAT("Tedit", matches("[T]ext[Edit]")); |
| 100 | EXPECT_THAT("ba", Not(matches("?AB?"))); |
| 101 | EXPECT_THAT("bkn", matches("the_[b]lack_[kn]ight")); |
| 102 | EXPECT_THAT("bt", matches("the_[b]lack_knigh[t]")); |
| 103 | EXPECT_THAT("ccm", matches("[c]amelCase[cm]")); |
| 104 | EXPECT_THAT("fdm", matches("[f]in[dM]odel")); |
| 105 | EXPECT_THAT("fob", matches("[fo]o[b]ar")); |
| 106 | EXPECT_THAT("fobz", Not(matches("foobar"))); |
| 107 | EXPECT_THAT("foobar", matches("[foobar]")); |
| 108 | EXPECT_THAT("form", matches("editor.[form]atOnSave")); |
| 109 | EXPECT_THAT("g p", matches("[G]it:[ P]ull")); |
| 110 | EXPECT_THAT("g p", matches("[G]it:[ P]ull")); |
| 111 | EXPECT_THAT("gip", matches("[Gi]t: [P]ull")); |
| 112 | EXPECT_THAT("gip", matches("[Gi]t: [P]ull")); |
| 113 | EXPECT_THAT("gp", matches("[G]it: [P]ull")); |
| 114 | EXPECT_THAT("gp", matches("[G]it_Git_[P]ull")); |
| 115 | EXPECT_THAT("is", matches("[I]mport[S]tatement")); |
| 116 | EXPECT_THAT("is", matches("[is]Valid")); |
| 117 | EXPECT_THAT("lowrd", matches("[low]Wo[rd]")); |
| 118 | EXPECT_THAT("myvable", matches("[myva]ria[ble]")); |
| 119 | EXPECT_THAT("no", Not(matches(""))); |
| 120 | EXPECT_THAT("no", Not(matches("match"))); |
| 121 | EXPECT_THAT("ob", Not(matches("foobar"))); |
| 122 | EXPECT_THAT("sl", matches("[S]Visual[L]oggerLogsList")); |
| 123 | EXPECT_THAT("sllll", matches("[S]Visua[lL]ogger[L]ogs[L]ist")); |
| 124 | EXPECT_THAT("Three", matches("H[T]ML[HRE]l[e]ment")); |
| 125 | EXPECT_THAT("Three", matches("[Three]")); |
| 126 | EXPECT_THAT("fo", Not(matches("barfoo"))); |
| 127 | EXPECT_THAT("fo", matches("bar_[fo]o")); |
| 128 | EXPECT_THAT("fo", matches("bar_[Fo]o")); |
| 129 | EXPECT_THAT("fo", matches("bar [fo]o")); |
| 130 | EXPECT_THAT("fo", matches("bar.[fo]o")); |
| 131 | EXPECT_THAT("fo", matches("bar/[fo]o")); |
| 132 | EXPECT_THAT("fo", matches("bar\\[fo]o")); |
| 133 | |
| 134 | EXPECT_THAT( |
| 135 | "aaaaaa", |
| 136 | matches("[aaaaaa]aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" |
| 137 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); |
| 138 | EXPECT_THAT("baba", Not(matches("ababababab"))); |
| 139 | EXPECT_THAT("fsfsfs", Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsa"))); |
| 140 | EXPECT_THAT("fsfsfsfsfsfsfsf", |
| 141 | Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsafdsafdsafdsafdsfd" |
| 142 | "safdsfdfdfasdnfdsajfndsjnafjndsajlknfdsa"))); |
| 143 | |
| 144 | EXPECT_THAT(" g", matches("[ g]roup")); |
| 145 | EXPECT_THAT("g", matches(" [g]roup")); |
| 146 | EXPECT_THAT("g g", Not(matches(" groupGroup"))); |
| 147 | EXPECT_THAT("g g", matches(" [g]roup[ G]roup")); |
| 148 | EXPECT_THAT(" g g", matches("[ ] [g]roup[ G]roup")); |
| 149 | EXPECT_THAT("zz", matches("[zz]Group")); |
| 150 | EXPECT_THAT("zzg", matches("[zzG]roup")); |
| 151 | EXPECT_THAT("g", matches("zz[G]roup")); |
| 152 | |
| 153 | EXPECT_THAT("aaaa", matches("_a_[aaaa]")); // Prefer consecutive. |
| 154 | EXPECT_THAT("printf", matches("s[printf]")); |
| 155 | EXPECT_THAT("str", matches("o[str]eam")); |
| 156 | } |
| 157 | |
| 158 | struct RankMatcher : public testing::MatcherInterface<StringRef> { |
| 159 | std::vector<ExpectedMatch> RankedStrings; |
| 160 | RankMatcher(std::initializer_list<ExpectedMatch> RankedStrings) |
| 161 | : RankedStrings(RankedStrings) {} |
| 162 | |
| 163 | void DescribeTo(::std::ostream *OS) const override { |
| 164 | raw_os_ostream O(*OS); |
| 165 | O << "Ranks strings in order: ["; |
| 166 | for (const auto &Str : RankedStrings) |
| 167 | O << "\n\t" << Str; |
| 168 | O << "\n]"; |
| 169 | } |
| 170 | |
| 171 | bool MatchAndExplain(StringRef Pattern, |
| 172 | testing::MatchResultListener *L) const override { |
| 173 | std::unique_ptr<raw_ostream> OS( |
| 174 | L->stream() ? (raw_ostream *)(new raw_os_ostream(*L->stream())) |
| 175 | : new raw_null_ostream()); |
| 176 | FuzzyMatcher Matcher(Pattern); |
| 177 | const ExpectedMatch *LastMatch; |
| 178 | Optional<float> LastScore; |
| 179 | bool Ok = true; |
| 180 | for (const auto &Str : RankedStrings) { |
| 181 | auto Score = Matcher.match(Str.Word); |
| 182 | if (!Score) { |
| 183 | *OS << "\nDoesn't match '" << Str.Word << "'"; |
| 184 | Matcher.dumpLast(*OS << "\n"); |
| 185 | Ok = false; |
| 186 | } else { |
| 187 | std::string Buf; |
| 188 | llvm::raw_string_ostream Info(Buf); |
| 189 | auto AnnotatedMatch = Matcher.dumpLast(Info); |
| 190 | |
| 191 | if (AnnotatedMatch != Str.Annotated) { |
| 192 | *OS << "\nMatched " << Str.Word << " as " << AnnotatedMatch |
| 193 | << " instead of " << Str.Annotated << "\n" |
| 194 | << Info.str(); |
| 195 | Ok = false; |
| 196 | } else if (LastScore && *LastScore < *Score) { |
| 197 | *OS << "\nRanks '" << Str.Word << "'=" << *Score << " above '" |
| 198 | << LastMatch->Word << "'=" << *LastScore << "\n" |
| 199 | << Info.str(); |
| 200 | Matcher.match(LastMatch->Word); |
| 201 | Matcher.dumpLast(*OS << "\n"); |
| 202 | Ok = false; |
| 203 | } |
| 204 | } |
| 205 | LastMatch = &Str; |
| 206 | LastScore = Score; |
| 207 | } |
| 208 | return Ok; |
| 209 | } |
| 210 | }; |
| 211 | |
| 212 | // Accepts patterns that match all the strings and rank them in the given order. |
| 213 | // Dumps the debug tables on match failure. |
| 214 | template <typename... T> testing::Matcher<StringRef> ranks(T... RankedStrings) { |
| 215 | return testing::MakeMatcher<StringRef>( |
| 216 | new RankMatcher{ExpectedMatch(RankedStrings)...}); |
| 217 | } |
| 218 | |
| 219 | TEST(FuzzyMatch, Ranking) { |
| 220 | EXPECT_THAT("eb", ranks("[e]mplace_[b]ack", "[e]m[b]ed")); |
| 221 | EXPECT_THAT("cons", |
| 222 | ranks("[cons]ole", "[Cons]ole", "ArrayBuffer[Cons]tructor")); |
| 223 | EXPECT_THAT("foo", ranks("[foo]", "[Foo]")); |
| 224 | EXPECT_THAT("onMess", |
| 225 | ranks("[onMess]age", "[onmess]age", "[on]This[M]ega[Es]cape[s]")); |
| 226 | EXPECT_THAT("CC", ranks("[C]amel[C]ase", "[c]amel[C]ase")); |
| 227 | EXPECT_THAT("cC", ranks("[c]amel[C]ase", "[C]amel[C]ase")); |
| 228 | EXPECT_THAT("p", ranks("[p]arse", "[p]osix", "[p]afdsa", "[p]ath", "[p]")); |
| 229 | EXPECT_THAT("pa", ranks("[pa]rse", "[pa]th", "[pa]fdsa")); |
| 230 | EXPECT_THAT("log", ranks("[log]", "Scroll[Log]icalPosition")); |
| 231 | EXPECT_THAT("e", ranks("[e]lse", "Abstract[E]lement")); |
| 232 | EXPECT_THAT("workbench.sideb", |
| 233 | ranks("[workbench.sideB]ar.location", |
| 234 | "[workbench.]editor.default[SideB]ySideLayout")); |
| 235 | EXPECT_THAT("editor.r", ranks("[editor.r]enderControlCharacter", |
| 236 | "[editor.]overview[R]ulerlanes", |
| 237 | "diff[Editor.r]enderSideBySide")); |
| 238 | EXPECT_THAT("-mo", ranks("[-mo]z-columns", "[-]ms-ime-[mo]de")); |
| 239 | EXPECT_THAT("convertModelPosition", |
| 240 | ranks("[convertModelPosition]ToViewPosition", |
| 241 | "[convert]ViewTo[ModelPosition]")); |
| 242 | EXPECT_THAT("is", ranks("[is]ValidViewletId", "[i]mport [s]tatement")); |
| 243 | EXPECT_THAT("title", ranks("window.[title]", |
| 244 | "files.[t]r[i]m[T]rai[l]ingWhit[e]space")); |
| 245 | EXPECT_THAT("strcpy", ranks("[strcpy]", "[strcpy]_s", "[str]n[cpy]")); |
| 246 | EXPECT_THAT("close", ranks("workbench.quickOpen.[close]OnFocusOut", |
| 247 | "[c]ss.[l]int.imp[o]rt[S]tat[e]ment", |
| 248 | "[c]ss.co[lo]rDecorator[s].[e]nable")); |
| 249 | } |
| 250 | |
| 251 | } // namespace |
| 252 | } // namespace clangd |
| 253 | } // namespace clang |