File indexing completed on 2024-04-14 03:54:32
0001 /* 0002 SPDX-FileCopyrightText: 2006-2020 David Faure <faure@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "setupxdgdirs.h" 0008 0009 #include <locale.h> 0010 0011 #include <QTest> 0012 0013 #include <KConfig> 0014 #include <KConfigGroup> 0015 #include <KDesktopFile> 0016 #include <kbuildsycoca_p.h> 0017 #include <ksycoca.h> 0018 0019 #include <kapplicationtrader.h> 0020 #include <kservicegroup.h> 0021 0022 #include <QFile> 0023 #include <QSignalSpy> 0024 #include <QStandardPaths> 0025 #include <QThread> 0026 0027 #include <QDebug> 0028 #include <QLoggingCategory> 0029 #include <QMimeDatabase> 0030 0031 enum class ExpectedResult { 0032 NoResults, 0033 FakeApplicationOnly, 0034 FakeSchemeHandlerOnly, 0035 FakeApplicationAndOthers, 0036 NotFakeApplication, 0037 }; 0038 Q_DECLARE_METATYPE(ExpectedResult) 0039 0040 class KApplicationTraderTest : public QObject 0041 { 0042 Q_OBJECT 0043 public: 0044 private Q_SLOTS: 0045 void initTestCase(); 0046 void testTraderConstraints_data(); 0047 void testTraderConstraints(); 0048 void testQueryByMimeType(); 0049 void testThreads(); 0050 void testTraderQueryMustRebuildSycoca(); 0051 void testSetPreferredService(); 0052 void cleanupTestCase(); 0053 0054 private: 0055 QString createFakeApplication(const QString &filename, const QString &name, const QMap<QString, QString> &extraFields = {}); 0056 void checkResult(const KService::List &offers, ExpectedResult expectedResult); 0057 0058 QString m_fakeApplication; 0059 QString m_fakeSchemeHandler; 0060 QString m_fakeGnomeApplication; 0061 QStringList m_createdDesktopFiles; 0062 }; 0063 0064 QTEST_MAIN(KApplicationTraderTest) 0065 0066 extern KSERVICE_EXPORT int ksycoca_ms_between_checks; 0067 0068 void KApplicationTraderTest::initTestCase() 0069 { 0070 // Set up a layer in the bin dir so ksycoca finds the desktop files created by createFakeApplication 0071 // Note that we still need /usr in there so that mimetypes are found 0072 setupXdgDirs(); 0073 0074 qputenv("XDG_CURRENT_DESKTOP", "KDE"); 0075 0076 QStandardPaths::setTestModeEnabled(true); 0077 0078 // A non-C locale is necessary for some tests. 0079 // This locale must have the following properties: 0080 // - some character other than dot as decimal separator 0081 // If it cannot be set, locale-dependent tests are skipped. 0082 setlocale(LC_ALL, "fr_FR.utf8"); 0083 bool hasNonCLocale = (setlocale(LC_ALL, nullptr) == QByteArray("fr_FR.utf8")); 0084 if (!hasNonCLocale) { 0085 qDebug() << "Setting locale to fr_FR.utf8 failed"; 0086 } 0087 0088 // Ensure no leftovers from other tests 0089 QDir(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)).removeRecursively(); 0090 0091 // Create some fake services for the tests below, and ensure they are in ksycoca. 0092 0093 // fakeservice_deleteme: deleted and recreated by testDeletingService, don't use in other tests 0094 createFakeApplication(QStringLiteral("fakeservice_deleteme.desktop"), QStringLiteral("DeleteMe")); 0095 0096 // fakeapplication 0097 m_fakeApplication = createFakeApplication(QStringLiteral("fakeapplication.desktop"), QStringLiteral("FakeApplication")); 0098 m_fakeApplication = QFileInfo(m_fakeApplication).canonicalFilePath(); 0099 0100 m_fakeSchemeHandler = createFakeApplication(QStringLiteral("fakeschemehandler.desktop"), 0101 QStringLiteral("FakeSchemeHandler"), 0102 {{QStringLiteral("MimeType"), QStringLiteral("text/plain;x-scheme-handler/someprotocol")}}); 0103 m_fakeSchemeHandler = QFileInfo(m_fakeSchemeHandler).canonicalFilePath(); 0104 0105 // fakegnomeapplication (do not show in Plasma). Should never be returned. To test the filtering code in queryByMimeType. 0106 QMap<QString, QString> fields; 0107 fields.insert(QStringLiteral("OnlyShowIn"), QStringLiteral("Gnome")); 0108 m_fakeGnomeApplication = createFakeApplication(QStringLiteral("fakegnomeapplication.desktop"), QStringLiteral("FakeApplicationGnome"), fields); 0109 m_fakeGnomeApplication = QFileInfo(m_fakeGnomeApplication).canonicalFilePath(); 0110 0111 ksycoca_ms_between_checks = 0; 0112 } 0113 0114 void KApplicationTraderTest::cleanupTestCase() 0115 { 0116 for (const QString &file : std::as_const(m_createdDesktopFiles)) { 0117 QFile::remove(file); 0118 } 0119 } 0120 0121 // Helper method for all the trader tests 0122 static bool offerListHasService(const KService::List &offers, const QString &entryPath) 0123 { 0124 bool found = false; 0125 for (const auto &service : offers) { 0126 if (service->entryPath() == entryPath) { 0127 if (found) { // should be there only once 0128 qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath)); 0129 return false; // make test fail 0130 } 0131 found = true; 0132 } 0133 } 0134 return found; 0135 } 0136 0137 void KApplicationTraderTest::checkResult(const KService::List &offers, ExpectedResult expectedResult) 0138 { 0139 switch (expectedResult) { 0140 case ExpectedResult::NoResults: 0141 if (!offers.isEmpty()) { 0142 qWarning() << "Got" << offers.count() << "unexpected results, including" << offers.at(0)->entryPath(); 0143 } 0144 QCOMPARE(offers.count(), 0); 0145 break; 0146 case ExpectedResult::FakeApplicationOnly: 0147 if (offers.count() != 1) { 0148 for (const auto &service : offers) { 0149 qWarning() << " " << service->entryPath(); 0150 } 0151 } 0152 QCOMPARE(offers.count(), 1); 0153 QCOMPARE(offers.at(0)->entryPath(), m_fakeApplication); 0154 break; 0155 case ExpectedResult::FakeSchemeHandlerOnly: 0156 if (offers.count() != 1) { 0157 for (const auto &service : offers) { 0158 qWarning() << " " << service->entryPath(); 0159 } 0160 } 0161 QCOMPARE(offers.count(), 1); 0162 QCOMPARE(offers.at(0)->entryPath(), m_fakeSchemeHandler); 0163 break; 0164 case ExpectedResult::FakeApplicationAndOthers: 0165 QVERIFY(!offers.isEmpty()); 0166 if (!offerListHasService(offers, m_fakeApplication)) { 0167 qWarning() << m_fakeApplication << "not found. Here's what we have:"; 0168 for (const auto &service : offers) { 0169 qWarning() << " " << service->entryPath(); 0170 } 0171 } 0172 QVERIFY(offerListHasService(offers, m_fakeApplication)); 0173 break; 0174 case ExpectedResult::NotFakeApplication: 0175 QVERIFY(!offerListHasService(offers, m_fakeApplication)); 0176 break; 0177 } 0178 QVERIFY(!offerListHasService(offers, m_fakeGnomeApplication)); 0179 } 0180 0181 using FF = KApplicationTrader::FilterFunc; 0182 Q_DECLARE_METATYPE(KApplicationTrader::FilterFunc) 0183 0184 void KApplicationTraderTest::testTraderConstraints_data() 0185 { 0186 QTest::addColumn<KApplicationTrader::FilterFunc>("filterFunc"); 0187 QTest::addColumn<ExpectedResult>("expectedResult"); 0188 0189 QTest::newRow("no_constraint") << FF([](const KService::Ptr &) { 0190 return true; 0191 }) << ExpectedResult::FakeApplicationAndOthers; 0192 0193 // == tests 0194 FF name_comparison = [](const KService::Ptr &serv) { 0195 return serv->name() == QLatin1String("FakeApplication"); 0196 }; 0197 QTest::newRow("name_comparison") << name_comparison << ExpectedResult::FakeApplicationOnly; 0198 FF isDontExist = [](const KService::Ptr &serv) { 0199 return serv->name() == QLatin1String("IDontExist"); 0200 }; 0201 QTest::newRow("no_such_name") << isDontExist << ExpectedResult::NoResults; 0202 FF no_such_name_by_case = [](const KService::Ptr &serv) { 0203 return serv->name() == QLatin1String("fakeapplication"); 0204 }; 0205 QTest::newRow("no_such_name_by_case") << no_such_name_by_case << ExpectedResult::NoResults; 0206 0207 // Name =~ 'fAkEaPPlicaTion' 0208 FF match_case_insensitive = [](const KService::Ptr &serv) { 0209 return serv->name().compare(QLatin1String{"fAkEaPPlicaTion"}, Qt::CaseInsensitive) == 0; 0210 }; 0211 QTest::newRow("match_case_insensitive") << match_case_insensitive << ExpectedResult::FakeApplicationOnly; 0212 0213 // 'FakeApp' ~ Name 0214 FF is_contained_in = [](const KService::Ptr &serv) { 0215 return serv->name().contains(QLatin1String{"FakeApp"}); 0216 }; 0217 QTest::newRow("is_contained_in") << is_contained_in << ExpectedResult::FakeApplicationOnly; 0218 0219 // 'FakeApplicationNot' ~ Name 0220 FF is_contained_in_fail = [](const KService::Ptr &serv) { 0221 return serv->name().contains(QLatin1String{"FakeApplicationNot"}); 0222 }; 0223 QTest::newRow("is_contained_in_fail") << is_contained_in_fail << ExpectedResult::NoResults; 0224 0225 // 'faKeApP' ~~ Name 0226 FF is_contained_in_case_insensitive = [](const KService::Ptr &serv) { 0227 return serv->name().contains(QLatin1String{"faKeApP"}, Qt::CaseInsensitive); 0228 }; 0229 QTest::newRow("is_contained_in_case_insensitive") << is_contained_in_case_insensitive << ExpectedResult::FakeApplicationOnly; 0230 0231 // 'faKeApPp' ~ Name 0232 FF is_contained_in_case_in_fail = [](const KService::Ptr &serv) { 0233 return serv->name().contains(QLatin1String{"faKeApPp"}, Qt::CaseInsensitive); 0234 }; 0235 QTest::newRow("is_contained_in_case_in_fail") << is_contained_in_case_in_fail << ExpectedResult::NoResults; 0236 0237 // 'FkApli' subseq Name 0238 FF subseq = [](const KService::Ptr &serv) { 0239 return KApplicationTrader::isSubsequence(QStringLiteral("FkApli"), serv->name()); 0240 }; 0241 QTest::newRow("subseq") << subseq << ExpectedResult::FakeApplicationOnly; 0242 0243 // 'fkApli' subseq Name 0244 FF subseq_fail = [](const KService::Ptr &serv) { 0245 return KApplicationTrader::isSubsequence(QStringLiteral("fkApli"), serv->name()); 0246 }; 0247 QTest::newRow("subseq_fail") << subseq_fail << ExpectedResult::NoResults; 0248 0249 // 'fkApLI' ~subseq Name 0250 FF subseq_case_insensitive = [](const KService::Ptr &serv) { 0251 return KApplicationTrader::isSubsequence(QStringLiteral("fkApLI"), serv->name(), Qt::CaseInsensitive); 0252 }; 0253 QTest::newRow("subseq_case_insensitive") << subseq_case_insensitive << ExpectedResult::FakeApplicationOnly; 0254 0255 // 'fk_Apli' ~subseq Name 0256 FF subseq_case_insensitive_fail = [](const KService::Ptr &serv) { 0257 return KApplicationTrader::isSubsequence(QStringLiteral("fk_Apli"), serv->name(), Qt::CaseInsensitive); 0258 }; 0259 QTest::newRow("subseq_case_insensitive_fail") << subseq_case_insensitive_fail << ExpectedResult::NoResults; 0260 0261 // Test another property, parsed as a double 0262 FF testVersion = [](const KService::Ptr &serv) { 0263 double d = serv->property<double>(QStringLiteral("X-KDE-Version")); 0264 return d > 5.559 && d < 5.561; 0265 }; 0266 QTest::newRow("float_parsing") << testVersion << ExpectedResult::FakeApplicationAndOthers; 0267 } 0268 0269 void KApplicationTraderTest::testTraderConstraints() 0270 { 0271 QFETCH(KApplicationTrader::FilterFunc, filterFunc); 0272 QFETCH(ExpectedResult, expectedResult); 0273 0274 const KService::List offers = KApplicationTrader::query(filterFunc); 0275 checkResult(offers, expectedResult); 0276 } 0277 0278 void KApplicationTraderTest::testQueryByMimeType() 0279 { 0280 KService::List offers; 0281 0282 // Without constraint 0283 0284 offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain")); 0285 checkResult(offers, ExpectedResult::FakeApplicationAndOthers); 0286 0287 offers = KApplicationTrader::queryByMimeType(QStringLiteral("image/png")); 0288 checkResult(offers, ExpectedResult::NotFakeApplication); 0289 0290 offers = KApplicationTrader::queryByMimeType(QStringLiteral("x-scheme-handler/someprotocol")); 0291 checkResult(offers, ExpectedResult::FakeSchemeHandlerOnly); 0292 0293 QTest::ignoreMessage(QtWarningMsg, "KApplicationTrader: mimeType \"no/such/mimetype\" not found"); 0294 offers = KApplicationTrader::queryByMimeType(QStringLiteral("no/such/mimetype")); 0295 checkResult(offers, ExpectedResult::NoResults); 0296 0297 // With constraint 0298 0299 FF isFakeApplication = [](const KService::Ptr &serv) { 0300 return serv->name() == QLatin1String("FakeApplication"); 0301 }; 0302 offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"), isFakeApplication); 0303 checkResult(offers, ExpectedResult::FakeApplicationOnly); 0304 0305 FF isDontExist = [](const KService::Ptr &serv) { 0306 return serv->name() == QLatin1String("IDontExist"); 0307 }; 0308 offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"), isDontExist); 0309 checkResult(offers, ExpectedResult::NoResults); 0310 } 0311 0312 QString KApplicationTraderTest::createFakeApplication(const QString &filename, const QString &name, const QMap<QString, QString> &extraFields) 0313 { 0314 const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + filename; 0315 QFile::remove(fakeService); 0316 m_createdDesktopFiles << fakeService; 0317 KDesktopFile file(fakeService); 0318 KConfigGroup group = file.desktopGroup(); 0319 group.writeEntry("Name", name); 0320 group.writeEntry("Type", "Application"); 0321 group.writeEntry("Exec", "ls"); 0322 group.writeEntry("Categories", "FakeCategory"); 0323 group.writeEntry("X-KDE-Version", "5.56"); 0324 group.writeEntry("MimeType", "text/plain;"); 0325 for (auto it = extraFields.begin(); it != extraFields.end(); ++it) { 0326 group.writeEntry(it.key(), it.value()); 0327 } 0328 return fakeService; 0329 } 0330 0331 #include <QFutureSynchronizer> 0332 #include <QThreadPool> 0333 #include <QtConcurrentRun> 0334 0335 // Testing for concurrent access to ksycoca from multiple threads 0336 // Use thread-sanitizer to see the data races 0337 0338 void KApplicationTraderTest::testThreads() 0339 { 0340 QThreadPool::globalInstance()->setMaxThreadCount(10); 0341 QFutureSynchronizer<void> sync; 0342 // Can't use data-driven tests here, QTestLib isn't threadsafe. 0343 sync.addFuture(QtConcurrent::run(&KApplicationTraderTest::testQueryByMimeType, this)); 0344 sync.addFuture(QtConcurrent::run(&KApplicationTraderTest::testQueryByMimeType, this)); 0345 sync.waitForFinished(); 0346 } 0347 0348 void KApplicationTraderTest::testTraderQueryMustRebuildSycoca() 0349 { 0350 auto filter = [](const KService::Ptr &serv) { 0351 return serv->name() == QLatin1String("MustRebuild"); 0352 }; 0353 QCOMPARE(KApplicationTrader::query(filter).count(), 0); 0354 createFakeApplication(QStringLiteral("fakeservice_querymustrebuild.desktop"), QStringLiteral("MustRebuild")); 0355 KService::List offers = KApplicationTrader::query(filter); 0356 QCOMPARE(offers.count(), 1); 0357 } 0358 0359 void KApplicationTraderTest::testSetPreferredService() 0360 { 0361 const KService::Ptr oldPref = KApplicationTrader::preferredService(QLatin1String("text/plain")); 0362 const KService::Ptr newPref = KService::serviceByDesktopPath(m_fakeApplication); 0363 KApplicationTrader::setPreferredService(QLatin1String("text/plain"), newPref); 0364 QCOMPARE(KApplicationTrader::preferredService(QLatin1String("text/plain"))->entryPath(), m_fakeApplication); 0365 KApplicationTrader::setPreferredService(QLatin1String("text/plain"), oldPref); 0366 QCOMPARE(KApplicationTrader::preferredService(QLatin1String("text/plain"))->storageId(), oldPref->storageId()); 0367 } 0368 0369 #include "kapplicationtradertest.moc"