2 SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
3 SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
5 SPDX-License-Identifier: GPL-2.0-or-later
8 #include "search/dolphinquery.h"
13 #include <QJsonDocument>
14 #include <QJsonObject>
15 #include <QStandardPaths>
16 #include <QStringList>
20 class DolphinQueryTest
: public QObject
26 void testBalooSearchParsing_data();
27 void testBalooSearchParsing();
28 void testExportImport();
32 * Helper function to compose the baloo query URL used for searching
34 QUrl
balooQueryUrl(const QString
&searchString
)
36 const QJsonObject jsonObject
{{"searchString", searchString
}};
38 const QJsonDocument
doc(jsonObject
);
39 const QString queryString
= QString::fromUtf8(doc
.toJson(QJsonDocument::Compact
));
42 urlQuery
.addQueryItem(QStringLiteral("json"), queryString
);
45 searchUrl
.setScheme(QLatin1String("baloosearch"));
46 searchUrl
.setQuery(urlQuery
);
51 void DolphinQueryTest::initTestCase()
53 QStandardPaths::setTestModeEnabled(true);
54 Search::setTestMode();
58 * Defines the parameters for the test cases in testBalooSearchParsing()
60 void DolphinQueryTest::testBalooSearchParsing_data()
62 QTest::addColumn
<QUrl
>("searchUrl");
63 QTest::addColumn
<QString
>("expectedSearchTerm");
64 QTest::addColumn
<QDate
>("expectedModifiedSinceDate");
65 QTest::addColumn
<int>("expectedMinimumRating");
66 QTest::addColumn
<QStringList
>("expectedTags");
67 QTest::addColumn
<bool>("hasContent");
68 QTest::addColumn
<bool>("hasFileName");
70 const QString text
= QStringLiteral("abc");
71 const QString textS
= QStringLiteral("abc xyz");
72 const QString textQ
= QStringLiteral("\"abc xyz\"");
73 const QString textM
= QStringLiteral("\"abc xyz\" tuv");
75 const QString filename
= QStringLiteral("filename:\"%1\"").arg(text
);
76 const QString filenameS
= QStringLiteral("filename:\"%1\"").arg(textS
);
77 const QString filenameQ
= QStringLiteral("filename:\"%1\"").arg(textQ
);
78 const QString filenameM
= QStringLiteral("filename:\"%1\"").arg(textM
);
80 const QString rating
= QStringLiteral("rating>=2");
81 const QString modified
= QStringLiteral("modified>=2019-08-07");
83 modifiedDate
.setDate(2019, 8, 7);
85 const QString tag
= QStringLiteral("tag:tagA");
86 const QString tagS
= QStringLiteral("tag:\"tagB with spaces\""); // in search url
87 const QString tagR
= QStringLiteral("tag:tagB with spaces"); // in result term
89 const QLatin1String tagA
{"tagA"};
90 const QLatin1String tagBWithSpaces
{"tagB with spaces"};
93 QTest::newRow("content") << balooQueryUrl(text
) << text
<< QDate
{} << 0 << QStringList() << true << true;
94 QTest::newRow("content/space") << balooQueryUrl(textS
) << textS
<< QDate
{} << 0 << QStringList() << true << true;
95 QTest::newRow("content/quoted") << balooQueryUrl(textQ
) << textS
<< QDate
{} << 0 << QStringList() << true << true;
96 QTest::newRow("content/empty") << balooQueryUrl("") << "" << QDate
{} << 0 << QStringList() << false << false;
97 QTest::newRow("content/single_quote") << balooQueryUrl("\"") << "\"" << QDate
{} << 0 << QStringList() << true << true;
98 QTest::newRow("content/double_quote") << balooQueryUrl("\"\"") << "" << QDate
{} << 0 << QStringList() << false << false;
100 // Test for "FileName"
101 QTest::newRow("filename") << balooQueryUrl(filename
) << text
<< QDate
{} << 0 << QStringList() << false << true;
102 QTest::newRow("filename/space") << balooQueryUrl(filenameS
) << textS
<< QDate
{} << 0 << QStringList() << false << true;
103 QTest::newRow("filename/quoted") << balooQueryUrl(filenameQ
) << textQ
<< QDate
{} << 0 << QStringList() << false << true;
104 QTest::newRow("filename/mixed") << balooQueryUrl(filenameM
) << textM
<< QDate
{} << 0 << QStringList() << false << true;
105 QTest::newRow("filename/empty") << balooQueryUrl("filename:") << "" << QDate
{} << 0 << QStringList() << false << false;
106 QTest::newRow("filename/single_quote") << balooQueryUrl("filename:\"") << "\"" << QDate
{} << 0 << QStringList() << false << true;
107 QTest::newRow("filename/double_quote") << balooQueryUrl("filename:\"\"") << "" << QDate
{} << 0 << QStringList() << false << false;
109 // Combined content and filename search
110 QTest::newRow("content+filename") << balooQueryUrl(text
+ " " + filename
) << text
<< QDate
{} << 0 << QStringList() << true << true;
112 QTest::newRow("content+filename/quoted") << balooQueryUrl(textQ
+ " " + filenameQ
) << textS
<< QDate
{} << 0 << QStringList() << true << true;
115 QTest::newRow("rating") << balooQueryUrl(rating
) << "" << QDate
{} << 2 << QStringList() << false << false;
116 QTest::newRow("rating+content") << balooQueryUrl(rating
+ " " + text
) << text
<< QDate
{} << 2 << QStringList() << true << true;
117 QTest::newRow("rating+filename") << balooQueryUrl(rating
+ " " + filename
) << text
<< QDate
{} << 2 << QStringList() << false << true;
119 // Test for modified date
120 QTest::newRow("modified") << balooQueryUrl(modified
) << "" << modifiedDate
<< 0 << QStringList() << false << false;
121 QTest::newRow("modified+content") << balooQueryUrl(modified
+ " " + text
) << text
<< modifiedDate
<< 0 << QStringList() << true << true;
122 QTest::newRow("modified+filename") << balooQueryUrl(modified
+ " " + filename
) << text
<< modifiedDate
<< 0 << QStringList() << false << true;
125 QTest::newRow("tag") << balooQueryUrl(tag
) << "" << QDate
{} << 0 << QStringList
{tagA
} << false << false;
126 QTest::newRow("tag/space") << balooQueryUrl(tagS
) << "" << QDate
{} << 0 << QStringList
{tagBWithSpaces
} << false << false;
127 QTest::newRow("tag/double") << balooQueryUrl(tag
+ " " + tagS
) << "" << QDate
{} << 0 << QStringList
{tagA
, tagBWithSpaces
} << false << false;
128 QTest::newRow("tag+content") << balooQueryUrl(tag
+ " " + text
) << text
<< QDate
{} << 0 << QStringList
{tagA
} << true << true;
129 QTest::newRow("tag+filename") << balooQueryUrl(tag
+ " " + filename
) << text
<< QDate
{} << 0 << QStringList
{tagA
} << false << true;
131 // Combined search terms
132 QTest::newRow("searchTerms") << balooQueryUrl(rating
+ " AND " + modified
+ " AND " + tag
+ " AND " + tagS
) << "" << modifiedDate
<< 2
133 << QStringList
{tagA
, tagBWithSpaces
} << false << false;
135 QTest::newRow("searchTerms+content") << balooQueryUrl(rating
+ " AND " + modified
+ " " + text
+ " " + tag
+ " AND " + tagS
) << text
<< modifiedDate
<< 2
136 << QStringList
{tagA
, tagBWithSpaces
} << true << true;
138 QTest::newRow("searchTerms+filename") << balooQueryUrl(rating
+ " AND " + modified
+ " " + filename
+ " " + tag
+ " AND " + tagS
) << text
<< modifiedDate
139 << 2 << QStringList
{tagA
, tagBWithSpaces
} << false << true;
141 QTest::newRow("allTerms") << balooQueryUrl(text
+ " " + filename
+ " " + rating
+ " AND " + modified
+ " AND " + tag
) << text
<< modifiedDate
<< 2
142 << QStringList
{tagA
} << true << true;
144 QTest::newRow("allTerms/space") << balooQueryUrl(textS
+ " " + filenameS
+ " " + rating
+ " AND " + modified
+ " AND " + tagS
) << textS
<< modifiedDate
<< 2
145 << QStringList
{tagBWithSpaces
} << true << true;
147 // Test tags:/ URL scheme
148 const auto tagUrl
= [](const QString
&tag
) {
149 return QUrl(QStringLiteral("tags:/%1/").arg(tag
));
152 QTest::newRow("tagsUrl") << tagUrl(tagA
) << "" << QDate
{} << 0 << QStringList
{tagA
} << false << false;
153 QTest::newRow("tagsUrl/space") << tagUrl(tagBWithSpaces
) << "" << QDate
{} << 0 << QStringList
{tagBWithSpaces
} << false << false;
154 QTest::newRow("tagsUrl/hash") << tagUrl("tagC#hash") << "" << QDate
{} << 0 << QStringList
{QStringLiteral("tagC#hash")} << false << false;
155 QTest::newRow("tagsUrl/slash") << tagUrl("tagD/with/slash") << "" << QDate
{} << 0 << QStringList
{QStringLiteral("tagD/with/slash")} << false << false;
159 * The test verifies whether the different terms search URL (e.g. "baloosearch:/") are
160 * properly handled by the searchbox, and only "user" or filename terms are added to the
161 * text bar of the searchbox.
163 void DolphinQueryTest::testBalooSearchParsing()
165 QFETCH(QUrl
, searchUrl
);
166 QFETCH(QString
, expectedSearchTerm
);
167 QFETCH(QDate
, expectedModifiedSinceDate
);
168 QFETCH(int, expectedMinimumRating
);
169 QFETCH(QStringList
, expectedTags
);
170 QFETCH(bool, hasContent
);
171 QFETCH(bool, hasFileName
);
173 const Search::DolphinQuery query
= Search::DolphinQuery
{searchUrl
, /** No backupSearchPath should be needed because searchUrl should be valid. */ QUrl
{}};
175 // Checkt that the URL is supported
176 QVERIFY(Search::isSupportedSearchScheme(searchUrl
.scheme()));
178 // Check for parsed text (would be displayed on the input search bar)
179 QCOMPARE(query
.searchTerm(), expectedSearchTerm
);
181 QCOMPARE(query
.modifiedSinceDate(), expectedModifiedSinceDate
);
183 QCOMPARE(query
.minimumRating(), expectedMinimumRating
);
185 QCOMPARE(query
.requiredTags(), expectedTags
);
187 // Check that there were no unrecognized baloo query parameters in the above strings.
188 Q_ASSERT(query
.m_unrecognizedBalooQueryStrings
.isEmpty());
190 // Check if a search term is looked up in the file names or contents
191 QCOMPARE(query
.searchThrough() == Search::SearchThrough::FileContents
&& !query
.searchTerm().isEmpty(), hasContent
);
192 QCOMPARE(!query
.searchTerm().isEmpty(), hasFileName
); // The file names are always also searched even when searching through file contents.
196 * Tests whether exporting a DolphinQuery object to a URL and then constructing a DolphinQuery object from that URL recreates the same DolphinQuery.
198 void DolphinQueryTest::testExportImport()
200 /// Initialize the DolphinQuery with some standard settings.
201 const QUrl searchPath1
{"file:///someNonExistentUrl"};
202 Search::DolphinQuery query
{searchPath1
, searchPath1
};
203 query
.setSearchLocations(Search::SearchLocations::FromHere
);
204 QVERIFY(query
.searchLocations() == Search::SearchLocations::FromHere
);
205 query
.setSearchThrough(Search::SearchThrough::FileNames
);
206 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileNames
);
207 query
.setSearchTool(Search::SearchTool::Filenamesearch
);
208 QVERIFY(query
.searchTool() == Search::SearchTool::Filenamesearch
);
209 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath1
)); // Export then import
211 /// Test that exporting and importing works as expected no matter which aspect we change.
212 query
.setSearchThrough(Search::SearchThrough::FileContents
);
213 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileContents
);
214 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath1
)); // Export then import
216 constexpr QLatin1String searchTerm1
{"abc"};
217 query
.setSearchTerm(searchTerm1
);
218 QVERIFY(query
.searchTerm() == searchTerm1
);
219 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath1
)); // Export then import
221 query
.setSearchThrough(Search::SearchThrough::FileNames
);
222 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileNames
);
223 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath1
)); // Export then import
225 QVERIFY(query
.searchPath() == searchPath1
);
226 const QUrl searchPath2
{"file:///someNonExistentUrl2"};
227 query
.setSearchPath(searchPath2
);
228 QVERIFY(query
.searchPath() == searchPath2
);
229 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because otherUrl is imported.
231 query
.setSearchLocations(Search::SearchLocations::Everywhere
);
232 QVERIFY(query
.searchLocations() == Search::SearchLocations::Everywhere
);
233 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath2
)); // Export then import. searchPath2 is required to match as the fallback.
235 QVERIFY(query
.searchTerm() == searchTerm1
);
236 constexpr QLatin1String searchTerm2
{"xyz"};
237 query
.setSearchTerm(searchTerm2
);
238 QVERIFY(query
.searchTerm() == searchTerm2
);
239 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath2
)); // Export then import
241 QVERIFY(query
.searchLocations() == Search::SearchLocations::Everywhere
);
242 query
.setSearchLocations(Search::SearchLocations::FromHere
);
243 QVERIFY(query
.searchLocations() == Search::SearchLocations::FromHere
);
244 QVERIFY(query
.searchPath() == searchPath2
);
245 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
248 /// Test Baloo search queries
249 query
.setSearchTool(Search::SearchTool::Baloo
);
250 QVERIFY(query
.searchTool() == Search::SearchTool::Baloo
);
251 QVERIFY(query
.searchTerm() == searchTerm2
);
252 QVERIFY(query
.searchLocations() == Search::SearchLocations::FromHere
);
253 QVERIFY(query
.searchPath() == searchPath2
);
254 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileNames
);
255 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
257 /// Test that exporting and importing works as expected no matter which aspect we change.
258 query
.setSearchThrough(Search::SearchThrough::FileContents
);
259 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileContents
);
260 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
262 query
.setSearchTerm(searchTerm1
);
263 QVERIFY(query
.searchTerm() == searchTerm1
);
264 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
266 query
.setSearchThrough(Search::SearchThrough::FileNames
);
267 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileNames
);
268 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
270 QVERIFY(query
.searchPath() == searchPath2
);
271 query
.setSearchPath(searchPath1
);
272 QVERIFY(query
.searchPath() == searchPath1
);
273 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
275 query
.setSearchLocations(Search::SearchLocations::Everywhere
);
276 QVERIFY(query
.searchLocations() == Search::SearchLocations::Everywhere
);
277 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath1
)); // Export then import. searchPath1 is required to match as the fallback.
279 QVERIFY(query
.searchTerm() == searchTerm1
);
280 query
.setSearchTerm(searchTerm2
);
281 QVERIFY(query
.searchTerm() == searchTerm2
);
282 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), searchPath1
)); // Export then import
284 QVERIFY(query
.searchLocations() == Search::SearchLocations::Everywhere
);
285 query
.setSearchLocations(Search::SearchLocations::FromHere
);
286 QVERIFY(query
.searchLocations() == Search::SearchLocations::FromHere
);
287 QVERIFY(query
.searchPath() == searchPath1
);
288 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
290 QVERIFY(query
.fileType() == KFileMetaData::Type::Empty
);
291 query
.setFileType(KFileMetaData::Type::Archive
);
292 QVERIFY(query
.fileType() == KFileMetaData::Type::Archive
);
293 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
295 QVERIFY(!query
.modifiedSinceDate().isValid());
297 modifiedDate
.setDate(2018, 6, 3); // World Bicycle Day
298 query
.setModifiedSinceDate(modifiedDate
);
299 QVERIFY(query
.modifiedSinceDate() == modifiedDate
);
300 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
302 QVERIFY(query
.minimumRating() == 0);
303 query
.setMinimumRating(4);
304 QVERIFY(query
.minimumRating() == 4);
305 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
307 QVERIFY(query
.requiredTags().isEmpty());
308 query
.setRequiredTags({searchTerm1
, searchTerm2
});
309 QVERIFY(query
.requiredTags().contains(searchTerm1
));
310 QVERIFY(query
.requiredTags().contains(searchTerm2
));
311 QVERIFY(query
== Search::DolphinQuery(query
.toUrl(), QUrl
{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
313 QVERIFY(query
.searchTool() == Search::SearchTool::Baloo
);
314 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileNames
);
315 QVERIFY(query
.searchPath() == searchPath1
);
316 QVERIFY(query
.searchTerm() == searchTerm2
);
317 QVERIFY(query
.searchLocations() == Search::SearchLocations::FromHere
);
318 QVERIFY(query
.fileType() == KFileMetaData::Type::Archive
);
319 QVERIFY(query
.modifiedSinceDate() == modifiedDate
);
320 QVERIFY(query
.minimumRating() == 4);
322 /// Changing the search tool should not immediately drop all the extra information even if the search tool might not support searching for them.
323 /// This is mostly an attempt to not drop properties set by the user earlier than we have to.
324 query
.setSearchTool(Search::SearchTool::Filenamesearch
);
325 QVERIFY(query
.searchThrough() == Search::SearchThrough::FileNames
);
326 QVERIFY(query
.searchPath() == searchPath1
);
327 QVERIFY(query
.searchTerm() == searchTerm2
);
328 QVERIFY(query
.searchLocations() == Search::SearchLocations::FromHere
);
329 QVERIFY(query
.fileType() == KFileMetaData::Type::Archive
);
330 QVERIFY(query
.modifiedSinceDate() == modifiedDate
);
331 QVERIFY(query
.minimumRating() == 4);
335 QTEST_MAIN(DolphinQueryTest
)
337 #include "dolphinquerytest.moc"