Changeset View
Changeset View
Standalone View
Standalone View
autotests/kapplicationtradertest.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | Copyright (C) 2006-2019 David Faure <faure@kde.org> | ||||
3 | | ||||
4 | This library is free software; you can redistribute it and/or | ||||
5 | modify it under the terms of the GNU Library General Public | ||||
6 | License as published by the Free Software Foundation; either | ||||
7 | version 2 of the License, or (at your option) any later version. | ||||
8 | | ||||
9 | This library is distributed in the hope that it will be useful, | ||||
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||||
12 | Library General Public License for more details. | ||||
13 | | ||||
14 | You should have received a copy of the GNU Library General Public License | ||||
15 | along with this library; see the file COPYING.LIB. If not, write to | ||||
16 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, | ||||
17 | Boston, MA 02110-1301, USA. | ||||
18 | */ | ||||
19 | | ||||
20 | #include "setupxdgdirs.h" | ||||
21 | | ||||
22 | #include <locale.h> | ||||
23 | | ||||
24 | #include <qtest.h> | ||||
25 | | ||||
26 | #include <kconfig.h> | ||||
27 | #include <kconfiggroup.h> | ||||
28 | #include <kdesktopfile.h> | ||||
29 | #include <ksycoca.h> | ||||
30 | #include <kbuildsycoca_p.h> | ||||
31 | #include <../src/services/ktraderparsetree_p.h> | ||||
32 | | ||||
33 | #include <kservicegroup.h> | ||||
34 | #include <kapplicationtrader.h> | ||||
35 | #include <kservicetype.h> | ||||
36 | #include <kservicetypeprofile.h> | ||||
37 | | ||||
38 | #include <qfile.h> | ||||
39 | #include <qstandardpaths.h> | ||||
40 | #include <qthread.h> | ||||
41 | #include <qsignalspy.h> | ||||
42 | | ||||
43 | #include <QDebug> | ||||
44 | #include <QLoggingCategory> | ||||
45 | #include <QMimeDatabase> | ||||
46 | | ||||
47 | enum class ExpectedResult { | ||||
48 | NoResults, | ||||
49 | FakeApplicationOnly, | ||||
50 | FakeApplicationAndOthers, | ||||
51 | NotFakeApplication, | ||||
52 | }; | ||||
53 | Q_DECLARE_METATYPE(ExpectedResult) | ||||
54 | | ||||
55 | class KApplicationTraderTest : public QObject | ||||
56 | { | ||||
57 | Q_OBJECT | ||||
58 | public: | ||||
59 | private Q_SLOTS: | ||||
60 | void initTestCase(); | ||||
61 | void testTraderConstraints_data(); | ||||
62 | void testTraderConstraints(); | ||||
63 | void testSubseqConstraints(); | ||||
64 | void testQueryByMimeType(); | ||||
65 | void testThreads(); | ||||
66 | void testTraderQueryMustRebuildSycoca(); | ||||
67 | void cleanupTestCase(); | ||||
68 | | ||||
69 | private: | ||||
70 | QString createFakeApplication(const QString &filename, const QString &name); | ||||
71 | void checkResult(const KService::List &offers, ExpectedResult expectedResult); | ||||
72 | | ||||
73 | QString m_fakeApplication; | ||||
74 | QStringList m_createdDesktopFiles; | ||||
75 | }; | ||||
76 | | ||||
77 | QTEST_MAIN(KApplicationTraderTest) | ||||
78 | | ||||
79 | extern KSERVICE_EXPORT int ksycoca_ms_between_checks; | ||||
80 | | ||||
81 | void KApplicationTraderTest::initTestCase() | ||||
82 | { | ||||
83 | // Set up a layer in the bin dir so ksycoca finds the desktop files created by createFakeApplication | ||||
84 | // Note that we still need /usr in there so that mimetypes are found | ||||
85 | setupXdgDirs(); | ||||
86 | | ||||
87 | QStandardPaths::setTestModeEnabled(true); | ||||
88 | | ||||
89 | // A non-C locale is necessary for some tests. | ||||
90 | // This locale must have the following properties: | ||||
91 | // - some character other than dot as decimal separator | ||||
92 | // If it cannot be set, locale-dependent tests are skipped. | ||||
93 | setlocale(LC_ALL, "fr_FR.utf8"); | ||||
94 | bool hasNonCLocale = (setlocale(LC_ALL, nullptr) == QByteArray("fr_FR.utf8")); | ||||
95 | if (!hasNonCLocale) { | ||||
96 | qDebug() << "Setting locale to fr_FR.utf8 failed"; | ||||
97 | } | ||||
98 | | ||||
99 | // Create some fake services for the tests below, and ensure they are in ksycoca. | ||||
100 | | ||||
101 | // fakeservice_deleteme: deleted and recreated by testDeletingService, don't use in other tests | ||||
102 | createFakeApplication(QStringLiteral("fakeservice_deleteme.desktop"), QStringLiteral("DeleteMe")); | ||||
103 | | ||||
104 | // fakeapplication | ||||
105 | m_fakeApplication = createFakeApplication(QStringLiteral("fakeapplication.desktop"), QStringLiteral("FakeApplication")); | ||||
106 | | ||||
107 | ksycoca_ms_between_checks = 0; | ||||
108 | } | ||||
109 | | ||||
110 | void KApplicationTraderTest::cleanupTestCase() | ||||
111 | { | ||||
112 | for (const QString &file : qAsConst(m_createdDesktopFiles)) { | ||||
113 | QFile::remove(file); | ||||
114 | } | ||||
115 | } | ||||
116 | | ||||
117 | // Helper method for all the trader tests | ||||
118 | static bool offerListHasService(const KService::List &offers, | ||||
119 | const QString &entryPath) | ||||
120 | { | ||||
121 | bool found = false; | ||||
122 | for (const auto &service : offers) { | ||||
123 | if (service->entryPath() == entryPath) { | ||||
124 | if (found) { // should be there only once | ||||
125 | qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath)); | ||||
126 | return false; // make test fail | ||||
127 | } | ||||
128 | found = true; | ||||
129 | } | ||||
130 | } | ||||
131 | if (!found) { | ||||
132 | qWarning() << entryPath << "not found. Here's what we have:"; | ||||
133 | for (const auto &service : offers) { | ||||
134 | qWarning() << " " << service->entryPath(); | ||||
135 | } | ||||
136 | } | ||||
137 | return found; | ||||
138 | } | ||||
139 | | ||||
140 | void KApplicationTraderTest::checkResult(const KService::List &offers, ExpectedResult expectedResult) | ||||
141 | { | ||||
142 | switch (expectedResult) { | ||||
143 | case ExpectedResult::NoResults: | ||||
144 | if (!offers.isEmpty()) { | ||||
145 | qWarning() << "Got" << offers.count() << "unexpected results, including" << offers.at(0)->entryPath(); | ||||
146 | } | ||||
147 | QCOMPARE(offers.count(), 0); | ||||
148 | break; | ||||
149 | case ExpectedResult::FakeApplicationOnly: | ||||
150 | if (offers.count() != 1) { | ||||
151 | for (const auto &service : offers) { | ||||
152 | qWarning() << " " << service->entryPath(); | ||||
153 | } | ||||
154 | } | ||||
155 | QCOMPARE(offers.count(), 1); | ||||
156 | QCOMPARE(offers.at(0)->entryPath(), m_fakeApplication); | ||||
157 | break; | ||||
158 | case ExpectedResult::FakeApplicationAndOthers: | ||||
159 | QVERIFY(!offers.isEmpty()); | ||||
160 | QVERIFY(offerListHasService(offers, m_fakeApplication)); | ||||
161 | break; | ||||
162 | case ExpectedResult::NotFakeApplication: | ||||
163 | QVERIFY(!offerListHasService(offers, m_fakeApplication)); | ||||
164 | break; | ||||
165 | } | ||||
166 | } | ||||
167 | | ||||
168 | void KApplicationTraderTest::testTraderConstraints_data() | ||||
169 | { | ||||
170 | QTest::addColumn<QString>("constraint"); | ||||
171 | QTest::addColumn<ExpectedResult>("expectedResult"); | ||||
172 | | ||||
173 | QTest::newRow("no_constraint") << QString() << ExpectedResult::FakeApplicationAndOthers; | ||||
174 | QTest::newRow("name_comparison") << QStringLiteral("Name == 'FakeApplication'") << ExpectedResult::FakeApplicationOnly; | ||||
175 | QTest::newRow("no_such_name") << QStringLiteral("Name == 'IDontExist'") << ExpectedResult::NoResults; | ||||
176 | QTest::newRow("no_such_name_by_case") << QStringLiteral("Name == 'fakeapplication'") << ExpectedResult::NoResults; | ||||
177 | QTest::newRow("match_case_insensitive") << "Name =~ 'fAkEaPPlicaTion'" << ExpectedResult::FakeApplicationOnly; | ||||
178 | QTest::newRow("is_contained_in") << "'FakeApp' ~ Name" << ExpectedResult::FakeApplicationOnly; | ||||
179 | QTest::newRow("is_contained_in_fail") << "'FakeApplicationNot' ~ Name" << ExpectedResult::NoResults; | ||||
180 | QTest::newRow("is_contained_in_case_insensitive") << "'faKeApP' ~~ Name" << ExpectedResult::FakeApplicationOnly; | ||||
181 | QTest::newRow("is_contained_in_case_in_fail") << "'faKeApPp' ~ Name" << ExpectedResult::NoResults; | ||||
182 | QTest::newRow("subseq") << "'FkApli' subseq Name" << ExpectedResult::FakeApplicationOnly; | ||||
183 | QTest::newRow("subseq_fail") << "'fkApli' subseq Name" << ExpectedResult::NoResults; | ||||
184 | QTest::newRow("subseq_case_insensitive") << "'fkApLI' ~subseq Name" << ExpectedResult::FakeApplicationOnly; | ||||
185 | QTest::newRow("subseq_case_insensitive_fail") << "'fk_Apli' ~subseq Name" << ExpectedResult::NoResults; | ||||
186 | // Test float parsing, must use dot as decimal separator independent of locale. | ||||
187 | QTest::newRow("float_parsing") << "([X-KDE-Version] > 5.559) and ([X-KDE-Version] < 5.561)" << ExpectedResult::FakeApplicationAndOthers; | ||||
188 | // A test with an invalid query, to test for memleaks | ||||
189 | QTest::newRow("invalid_query") << "A == B OR C == D AND OR Foo == 'Parse Error'" << ExpectedResult::NoResults; | ||||
190 | } | ||||
191 | | ||||
192 | void KApplicationTraderTest::testTraderConstraints() | ||||
193 | { | ||||
194 | QFETCH(QString, constraint); | ||||
195 | QFETCH(ExpectedResult, expectedResult); | ||||
196 | | ||||
197 | const KService::List offers = KApplicationTrader::self()->query(constraint); | ||||
198 | checkResult(offers, expectedResult); | ||||
199 | } | ||||
200 | | ||||
201 | void KApplicationTraderTest::testSubseqConstraints() | ||||
202 | { | ||||
203 | auto test = [](const char* pattern, const char* text, bool sensitive) { | ||||
204 | return KTraderParse::ParseTreeSubsequenceMATCH::isSubseq( | ||||
205 | QString(pattern), | ||||
206 | QString(text), | ||||
207 | sensitive? Qt::CaseSensitive : Qt::CaseInsensitive | ||||
208 | ); | ||||
209 | }; | ||||
210 | | ||||
211 | // Case Sensitive | ||||
212 | QVERIFY2(!test("", "", 1), "both empty"); | ||||
213 | QVERIFY2(!test("", "something", 1), "empty pattern"); | ||||
214 | QVERIFY2(!test("something", "", 1), "empty text"); | ||||
215 | QVERIFY2(test("lngfile", "somereallylongfile", 1), "match ending"); | ||||
216 | QVERIFY2(test("somelong", "somereallylongfile", 1), "match beginning"); | ||||
217 | QVERIFY2(test("reallylong", "somereallylongfile", 1), "match middle"); | ||||
218 | QVERIFY2(test("across", "a 23 c @#! r o01 o 5 s_s", 1), "match across"); | ||||
219 | QVERIFY2(!test("nocigar", "soclosebutnociga", 1), "close but no match"); | ||||
220 | QVERIFY2(!test("god", "dog", 1), "incorrect letter order"); | ||||
221 | QVERIFY2(!test("mismatch", "mIsMaTcH", 1), "case sensitive mismatch"); | ||||
222 | | ||||
223 | // Case insensitive | ||||
224 | QVERIFY2(test("mismatch", "mIsMaTcH", 0), "case insensitive match"); | ||||
225 | QVERIFY2(test("tryhards", "Try Your Hardest", 0), "uppercase text"); | ||||
226 | QVERIFY2(test("TRYHARDS", "try your hardest", 0), "uppercase pattern"); | ||||
227 | } | ||||
228 | | ||||
229 | void KApplicationTraderTest::testQueryByMimeType() | ||||
230 | { | ||||
231 | KService::List offers; | ||||
232 | | ||||
233 | // Without constraint | ||||
234 | | ||||
235 | offers = KApplicationTrader::self()->queryByMimeType(QStringLiteral("text/plain")); | ||||
236 | checkResult(offers, ExpectedResult::FakeApplicationAndOthers); | ||||
237 | | ||||
238 | offers = KApplicationTrader::self()->queryByMimeType(QStringLiteral("image/png")); | ||||
239 | checkResult(offers, ExpectedResult::NotFakeApplication); | ||||
240 | | ||||
241 | QTest::ignoreMessage(QtWarningMsg, "KApplicationTrader: mimeType \"no/such/mimetype\" not found"); | ||||
242 | offers = KApplicationTrader::self()->queryByMimeType(QStringLiteral("no/such/mimetype")); | ||||
243 | checkResult(offers, ExpectedResult::NoResults); | ||||
244 | | ||||
245 | // With constraint | ||||
246 | | ||||
247 | offers = KApplicationTrader::self()->queryByMimeType(QStringLiteral("text/plain"), QStringLiteral("Name == 'FakeApplication'")); | ||||
248 | checkResult(offers, ExpectedResult::FakeApplicationAndOthers); | ||||
249 | | ||||
250 | offers = KApplicationTrader::self()->queryByMimeType(QStringLiteral("text/plain"), QStringLiteral("Name == 'IDontExist'")); | ||||
251 | checkResult(offers, ExpectedResult::NoResults); | ||||
252 | } | ||||
253 | | ||||
254 | QString KApplicationTraderTest::createFakeApplication(const QString &filename, const QString &name) | ||||
255 | { | ||||
256 | const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + filename; | ||||
257 | QFile::remove(fakeService); | ||||
258 | m_createdDesktopFiles << fakeService; | ||||
259 | KDesktopFile file(fakeService); | ||||
260 | KConfigGroup group = file.desktopGroup(); | ||||
261 | group.writeEntry("Name", name); | ||||
262 | group.writeEntry("Type", "Application"); | ||||
263 | group.writeEntry("Exec", "ls"); | ||||
264 | group.writeEntry("Categories", "FakeCategory"); | ||||
265 | group.writeEntry("X-KDE-Version", "5.56"); | ||||
266 | group.writeEntry("MimeType", "text/plain;"); | ||||
267 | return fakeService; | ||||
268 | } | ||||
269 | | ||||
270 | #include <QThreadPool> | ||||
271 | #include <QFutureSynchronizer> | ||||
272 | #include <qtconcurrentrun.h> | ||||
273 | | ||||
274 | // Testing for concurrent access to ksycoca from multiple threads | ||||
275 | // Use thread-sanitizer to see the data races | ||||
276 | | ||||
277 | void KApplicationTraderTest::testThreads() | ||||
278 | { | ||||
279 | QThreadPool::globalInstance()->setMaxThreadCount(10); | ||||
280 | QFutureSynchronizer<void> sync; | ||||
281 | // Can't use data-driven tests here, QTestLib isn't threadsafe. | ||||
282 | sync.addFuture(QtConcurrent::run(this, &KApplicationTraderTest::testSubseqConstraints)); | ||||
283 | sync.addFuture(QtConcurrent::run(this, &KApplicationTraderTest::testSubseqConstraints)); | ||||
284 | sync.addFuture(QtConcurrent::run(this, &KApplicationTraderTest::testQueryByMimeType)); | ||||
285 | sync.waitForFinished(); | ||||
286 | } | ||||
287 | | ||||
288 | void KApplicationTraderTest::testTraderQueryMustRebuildSycoca() | ||||
289 | { | ||||
290 | QCOMPARE(KApplicationTrader::self()->query(QStringLiteral("Name == 'MustRebuild'")).count(), 0); | ||||
291 | createFakeApplication(QStringLiteral("fakeservice_querymustrebuild.desktop"), QStringLiteral("MustRebuild")); | ||||
292 | KService::List offers = KApplicationTrader::self()->query(QStringLiteral("Name == 'MustRebuild'")); | ||||
293 | QCOMPARE(offers.count(), 1); | ||||
294 | } | ||||
295 | | ||||
296 | #include "kapplicationtradertest.moc" |