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"