File indexing completed on 2024-04-21 15:03:04

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"