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