1 /*
2     This file is part of the KDE libraries
3 
4     SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 #include "kfuzzymatchertest.h"
9 
10 #include <QString>
11 #include <QStringList>
12 #include <QTest>
13 
14 #include <algorithm>
15 
16 #include "kfuzzymatcher.h"
17 
QTEST_MAIN(KFuzzyMatcherTest)18 QTEST_MAIN(KFuzzyMatcherTest)
19 
20 void KFuzzyMatcherTest::testMatchSimple_data()
21 {
22     QTest::addColumn<QString>("pattern");
23     QTest::addColumn<QString>("inputstr");
24     QTest::addColumn<bool>("expected");
25 
26     QTest::newRow("AbcD") << QStringLiteral("AbcD") << QStringLiteral("AbCdefg") << true;
27     QTest::newRow("WithSpace") << QStringLiteral("Wa qa") << QStringLiteral("Wa qar") << true;
28     QTest::newRow("RTL") << QStringLiteral("ارو") << QStringLiteral("اردو") << true;
29     QTest::newRow("WithSep") << QStringLiteral("tf") << QStringLiteral("the_file") << true;
30     QTest::newRow("Umlaut") << QStringLiteral("Häu") << QStringLiteral("Häuser") << true;
31     QTest::newRow("Unmatched") << QStringLiteral("Name") << QStringLiteral("Nam") << false;
32     QTest::newRow("Empty Pattern") << QStringLiteral("") << QStringLiteral("Nam") << true;
33 }
34 
testMatchSimple()35 void KFuzzyMatcherTest::testMatchSimple()
36 {
37     QFETCH(QString, pattern);
38     QFETCH(QString, inputstr);
39     QFETCH(bool, expected);
40 
41     QVERIFY(KFuzzyMatcher::matchSimple(pattern, inputstr) == expected);
42 }
43 
testMatch_data()44 void KFuzzyMatcherTest::testMatch_data()
45 {
46     QTest::addColumn<QString>("pattern");
47     QTest::addColumn<QStringList>("input");
48     QTest::addColumn<QStringList>("expected");
49     QTest::addColumn<int>("size");
50     // clang-format off
51     QTest::newRow("pattern=sort") << QStringLiteral("sort")
52                           << QStringList{
53                                 QStringLiteral("Sort"),
54                                 QStringLiteral("Some other right test"),
55                                 QStringLiteral("Soup rate"),
56                                 QStringLiteral("Someother"),
57                                 QStringLiteral("irrelevant"),
58                               }
59                           << QStringList{
60                                 QStringLiteral("Sort"),
61                                 QStringLiteral("Some other right test"),
62                                 QStringLiteral("Soup rate"),
63                               }
64                           << 3;
65 
66 
67     QTest::newRow("pattern=kateapp") << QStringLiteral("kaapp")
68                           << QStringList{
69                                 QStringLiteral("kateapp.cpp"),
70                                 QStringLiteral("kate_application"),
71                                 QStringLiteral("kateapp.h"),
72                                 QStringLiteral("katepap.c")
73                               }
74                           << QStringList{
75                                 QStringLiteral("kate_application"),
76                                 QStringLiteral("kateapp.h"),
77                                 QStringLiteral("kateapp.cpp")
78                              }
79                           << 3;
80 
81     QTest::newRow("pattern=this") << QStringLiteral("this")
82                           << QStringList{
83                                 QStringLiteral("th"),
84                                 QStringLiteral("ths"),
85                                 QStringLiteral("thsi")
86                               }
87                           << QStringList{
88                              }
89                           << 0;
90 
91     QTest::newRow("pattern=marath") << QStringLiteral("marath")
92                           << QStringList{
93                              QStringLiteral("Maralen of the Mornsong"),
94                              QStringLiteral("Silumgar, the Drifting Death"),
95                              QStringLiteral("Maralen of the Mornsong Avatar"),
96                              QStringLiteral("Marshaling the Troops"),
97                              QStringLiteral("Homeward Path"),
98                              QStringLiteral("Marath, Will of the Wild"),
99                              QStringLiteral("Marshal's Anthem"),
100                              QStringLiteral("Marchesa, the Black Rose"),
101                              QStringLiteral("Mark for Death"),
102                              QStringLiteral("Master Apothecary"),
103                              QStringLiteral("Mazirek, Kraul Death Priest"),
104                              QStringLiteral("Akroma, Angel of Wrath"),
105                              QStringLiteral("Akroma, Angel of Wrath Avatar"),
106                              QStringLiteral("Commander's Authority"),
107                              QStringLiteral("Shaman of the Great Hunt"),
108                              QStringLiteral("Halimar Wavewatch"),
109                              QStringLiteral("Pyromancer's Swath")
110                               }
111                           << QStringList{
112                              QStringLiteral("Marath, Will of the Wild"),
113                              QStringLiteral("Maralen of the Mornsong"),
114                              QStringLiteral("Maralen of the Mornsong Avatar"),
115                              QStringLiteral("Marshal's Anthem"),
116                              QStringLiteral("Marshaling the Troops"),
117                              QStringLiteral("Marchesa, the Black Rose"),
118                              QStringLiteral("Mark for Death"),
119                              QStringLiteral("Master Apothecary"),
120                              QStringLiteral("Mazirek, Kraul Death Priest"),
121                              QStringLiteral("Akroma, Angel of Wrath"),
122                              QStringLiteral("Akroma, Angel of Wrath Avatar"),
123                              QStringLiteral("Commander's Authority"),
124                              QStringLiteral("Homeward Path"),
125                              QStringLiteral("Shaman of the Great Hunt"),
126                              QStringLiteral("Halimar Wavewatch"),
127                              QStringLiteral("Pyromancer's Swath"),
128                              QStringLiteral("Silumgar, the Drifting Death")
129                              }
130                           << 17;
131 
132     // This tests our recursive best match
133     QTest::newRow("pattern=lll") << QStringLiteral("lll")
134                           << QStringList{
135                                 QStringLiteral("SVisualLoggerLogsList.h"),
136                                 QStringLiteral("SimpleFileLogger.cpp"),
137                                 QStringLiteral("StringHandlerLogList.txt"),
138                                 QStringLiteral("LeapFromLostAllan"),
139                                 QStringLiteral("BumpLLL"),
140                               }
141                           << QStringList{
142                              QStringLiteral("SVisualLoggerLogsList.h"),
143                              QStringLiteral("LeapFromLostAllan"),
144                              QStringLiteral("BumpLLL"),
145                              QStringLiteral("StringHandlerLogList.txt"),
146                              QStringLiteral("SimpleFileLogger.cpp"),
147                              }
148                           << 5;
149 
150     QTest::newRow("pattern=") << QStringLiteral("")
151                           << QStringList{
152                                 QStringLiteral("th"),
153                                 QStringLiteral("ths"),
154                                 QStringLiteral("thsi")
155                               }
156                           << QStringList{
157                              QStringLiteral("th"),
158                              QStringLiteral("ths"),
159                              QStringLiteral("thsi")
160                              }
161                           << 3;
162     // clang-format on
163 }
164 
matchHelper(const QString & pattern,const QStringList & input)165 static QStringList matchHelper(const QString &pattern, const QStringList &input)
166 {
167     QVector<QPair<QString, int>> actual;
168     for (int i = 0; i < input.size(); ++i) {
169         KFuzzyMatcher::Result res = KFuzzyMatcher::match(pattern, input.at(i));
170         if (res.matched) {
171             actual.push_back({input.at(i), res.score});
172         }
173     }
174 
175     // sort descending based on score
176     std::sort(actual.begin(), actual.end(), [](const QPair<QString, int> &l, const QPair<QString, int> &r) {
177         return l.second > r.second;
178     });
179 
180     QStringList actualOut;
181     for (const auto &s : actual) {
182         actualOut << s.first;
183     }
184     return actualOut;
185 }
186 
testMatch()187 void KFuzzyMatcherTest::testMatch()
188 {
189     QFETCH(QString, pattern);
190     QFETCH(QStringList, input);
191     QFETCH(QStringList, expected);
192     QFETCH(int, size);
193 
194     const QStringList actual = matchHelper(pattern, input);
195 
196     QCOMPARE(actual.size(), size);
197     QCOMPARE(actual, expected);
198 }
199 
testMatchedRanges_data()200 void KFuzzyMatcherTest::testMatchedRanges_data()
201 {
202     QTest::addColumn<QString>("pattern");
203     QTest::addColumn<QString>("string");
204 
205     using Range = QPair<int, int>;
206     QTest::addColumn<QVector<Range>>("expectedRanges");
207 
208     QTest::addColumn<bool>("matchingOnly");
209 
210     QTest::newRow("Emtpy") << QStringLiteral("") << QStringLiteral("") << QVector<Range>{} << true;
211     QTest::newRow("Hello") << QStringLiteral("Hlo") << QStringLiteral("Hello") << QVector<Range>{{0, 1}, {3, 2}} << true;
212     QTest::newRow("lll") << QStringLiteral("lll") << QStringLiteral("SVisualLoggerLogsList") << QVector<Range>{{7, 1}, {13, 1}, {17, 1}} << true;
213     QTest::newRow("Sort") << QStringLiteral("sort") << QStringLiteral("SorT") << QVector<Range>{{0, 4}} << true;
214     QTest::newRow("Unmatching") << QStringLiteral("git") << QStringLiteral("gti") << QVector<Range>{} << true;
215     QTest::newRow("UnmatchingWithAllMatches") << QStringLiteral("git") << QStringLiteral("gti") << QVector<Range>{{0, 1}, {2, 1}} << false;
216 }
217 
testMatchedRanges()218 void KFuzzyMatcherTest::testMatchedRanges()
219 {
220     QFETCH(QString, pattern);
221     QFETCH(QString, string);
222     QFETCH(bool, matchingOnly);
223     using Range = QPair<int, int>;
224     QFETCH(QVector<Range>, expectedRanges);
225 
226     const auto matchMode = matchingOnly ? KFuzzyMatcher::RangeType::FullyMatched : KFuzzyMatcher::RangeType::All;
227 
228     auto resultRanges = KFuzzyMatcher::matchedRanges(pattern, string, matchMode);
229     QCOMPARE(resultRanges.size(), expectedRanges.size());
230 
231     bool res = std::equal(expectedRanges.begin(), expectedRanges.end(), resultRanges.begin(), [](const Range &l, const KFuzzyMatcher::Range &r) {
232         return l.first == r.start && l.second == r.length;
233     });
234     QVERIFY(res);
235 }
236