File indexing completed on 2024-04-14 14:27:18

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2009 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include <QSignalSpy>
0009 #include <QTest>
0010 
0011 #include <QDebug>
0012 #include <QFile>
0013 #include <QMimeDatabase>
0014 #include <QStandardPaths>
0015 #include <QThread>
0016 #include <QTimer>
0017 #include <kbuildsycoca_p.h>
0018 
0019 #include <KConfig>
0020 #include <KConfigGroup>
0021 #include <KDesktopFile>
0022 #include <QMutex>
0023 #include <kservicegroup.h>
0024 #include <kservicetype.h>
0025 #include <kservicetypeprofile.h>
0026 #include <ksycoca.h>
0027 
0028 #include "setupxdgdirs.h"
0029 
0030 static QString fakeTextPluginDesktopFile()
0031 {
0032     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String{"/kservices5/threadtextplugin.desktop"};
0033 }
0034 
0035 static QString fakeServiceDesktopFile()
0036 {
0037     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String{"/kservices5/threadfakeservice.desktop"};
0038 }
0039 
0040 // Helper method for all the trader tests
0041 static bool offerListHasService(const KService::List &offers, const QString &entryPath)
0042 {
0043     bool found = false;
0044     for (const auto &servicePtr : offers) {
0045         if (servicePtr->entryPath() == entryPath) {
0046             if (found) { // should be there only once
0047                 qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath));
0048                 return false; // make test fail
0049             }
0050             found = true;
0051         }
0052     }
0053     return found;
0054 }
0055 
0056 static QSet<QThread *> s_threadsWhoSawFakeService; // clazy:exclude=non-pod-global-static
0057 static QMutex s_setMutex; // clazy:exclude=non-pod-global-static
0058 static int threadsWhoSawFakeService()
0059 {
0060     QMutexLocker locker(&s_setMutex);
0061     return s_threadsWhoSawFakeService.count();
0062 }
0063 static QAtomicInt s_fakeServiceDeleted = 0; // clazy:exclude=non-pod-global-static
0064 
0065 class WorkerObject : public QObject
0066 {
0067     Q_OBJECT
0068 public:
0069     WorkerObject()
0070         : QObject()
0071     {
0072     }
0073 
0074 public Q_SLOTS:
0075     void work()
0076     {
0077         // qDebug() << QThread::currentThread() << "working...";
0078 
0079         const KServiceType::List allServiceTypes = KServiceType::allServiceTypes();
0080         Q_ASSERT(!allServiceTypes.isEmpty());
0081 
0082         QMimeDatabase db;
0083         const QList<QMimeType> allMimeTypes = db.allMimeTypes();
0084         Q_ASSERT(!allMimeTypes.isEmpty());
0085 
0086         const KService::List lst = KService::allServices();
0087         Q_ASSERT(!lst.isEmpty());
0088 
0089         for (const KService::Ptr &service : lst) {
0090             Q_ASSERT(service->isType(KST_KService));
0091             const QString name = service->name();
0092             const QString entryPath = service->entryPath();
0093             // qDebug() << name << "entryPath=" << entryPath << "menuId=" << service->menuId();
0094             Q_ASSERT(!name.isEmpty());
0095             Q_ASSERT(!entryPath.isEmpty());
0096 
0097             KService::Ptr lookedupService = KService::serviceByDesktopPath(entryPath);
0098             if (!lookedupService) {
0099                 if (entryPath == QLatin1String{"threadfakeservice.desktop"} && s_fakeServiceDeleted) { // ok, it got deleted meanwhile
0100                     continue;
0101                 }
0102                 qWarning() << entryPath << "is gone!";
0103             }
0104             Q_ASSERT(lookedupService); // not null
0105             QCOMPARE(lookedupService->entryPath(), entryPath);
0106 
0107             if (service->isApplication()) {
0108                 const QString menuId = service->menuId();
0109                 if (menuId.isEmpty()) {
0110                     qWarning("%s has an empty menuId!", qPrintable(entryPath));
0111                 }
0112                 Q_ASSERT(!menuId.isEmpty());
0113                 lookedupService = KService::serviceByMenuId(menuId);
0114                 if (!lookedupService) {
0115                     if (menuId == QLatin1String{"threadfakeservice"} && s_fakeServiceDeleted) { // ok, it got deleted meanwhile
0116                         continue;
0117                     }
0118                     qWarning() << menuId << "is gone!";
0119                 }
0120                 Q_ASSERT(lookedupService); // not null
0121                 QCOMPARE(lookedupService->menuId(), menuId);
0122             }
0123         }
0124 
0125 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 90)
0126         KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("KPluginInfo"));
0127         Q_ASSERT(offerListHasService(offers, QStringLiteral("threadtextplugin.desktop")));
0128 
0129         offers = KServiceTypeTrader::self()->query(QStringLiteral("KPluginInfo"), QStringLiteral("Library == 'threadtextplugin'"));
0130         Q_ASSERT(offers.count() == 1);
0131         QVERIFY(offerListHasService(offers, QStringLiteral("threadtextplugin.desktop")));
0132 #endif
0133 
0134         KServiceGroup::Ptr root = KServiceGroup::root();
0135         Q_ASSERT(root);
0136 
0137         if (KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))) {
0138             QMutexLocker locker(&s_setMutex);
0139             s_threadsWhoSawFakeService.insert(QThread::currentThread());
0140         }
0141     }
0142 };
0143 
0144 class WorkerThread : public QThread
0145 {
0146     Q_OBJECT
0147 public:
0148     WorkerThread()
0149         : QThread()
0150         , m_stop(false)
0151     {
0152     }
0153     void run() override
0154     {
0155         WorkerObject wo;
0156         while (!m_stop) {
0157             wo.work();
0158         }
0159     }
0160     virtual void stop()
0161     {
0162         m_stop = true;
0163     }
0164 
0165 private:
0166     QAtomicInt m_stop; // bool
0167 };
0168 
0169 /**
0170  * Threads with an event loop will be able to process "database changed" signals.
0171  * Threads without an event loop (like WorkerThread) cannot, so they will keep using
0172  * the old data.
0173  */
0174 class EventLoopThread : public WorkerThread
0175 {
0176     Q_OBJECT
0177 public:
0178     void run() override
0179     {
0180         // WorkerObject must belong to this thread, this is why we don't
0181         // have the slot work() in WorkerThread itself. Typical QThread trap!
0182         WorkerObject wo;
0183         QTimer timer;
0184         connect(&timer, &QTimer::timeout, &wo, &WorkerObject::work);
0185         timer.start(100);
0186         exec();
0187     }
0188     void stop() override
0189     {
0190         quit();
0191     }
0192 };
0193 
0194 // This code runs in the main thread
0195 class KSycocaThreadTest : public QObject
0196 {
0197     Q_OBJECT
0198 
0199 private Q_SLOTS:
0200     void initTestCase();
0201     void cleanupTestCase();
0202     void testCreateService();
0203     void testDeleteService()
0204     {
0205         deleteFakeService();
0206         QTimer::singleShot(1000, this, SLOT(slotFinish()));
0207     }
0208     void slotFinish()
0209     {
0210         qDebug() << "Terminating";
0211         for (int i = 0; i < threads.size(); i++) {
0212             threads[i]->stop();
0213         }
0214         for (int i = 0; i < threads.size(); i++) {
0215             threads[i]->wait();
0216         }
0217         cleanupTestCase();
0218         QCoreApplication::instance()->quit();
0219     }
0220 
0221 private:
0222     void createFakeService();
0223     void deleteFakeService();
0224     QVector<WorkerThread *> threads;
0225 };
0226 
0227 static void runKBuildSycoca()
0228 {
0229 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 80)
0230     QSignalSpy spy(KSycoca::self(), qOverload<const QStringList &>(&KSycoca::databaseChanged));
0231 #else
0232     QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged);
0233 #endif
0234 
0235     KBuildSycoca builder;
0236     QVERIFY(builder.recreate());
0237     qDebug() << "waiting for signal";
0238     QVERIFY(spy.wait(20000));
0239     qDebug() << "got signal";
0240 }
0241 
0242 void KSycocaThreadTest::initTestCase()
0243 {
0244     // Set up a layer in the bin dir so ksycoca finds the KPluginInfo and Application servicetypes
0245     setupXdgDirs();
0246     QStandardPaths::setTestModeEnabled(true);
0247 
0248     // This service is always there. Used in the trader queries from the thread.
0249     const QString fakeTextPlugin = fakeTextPluginDesktopFile();
0250     if (!QFile::exists(fakeTextPlugin)) {
0251         KDesktopFile file(fakeTextPlugin);
0252         KConfigGroup group = file.desktopGroup();
0253         group.writeEntry("Name", "ThreadTextPlugin");
0254         group.writeEntry("Type", "Service");
0255         group.writeEntry("X-KDE-Library", "threadtextplugin");
0256         group.writeEntry("X-KDE-Protocols", "http,ftp");
0257         group.writeEntry("ServiceTypes", "KPluginInfo");
0258         group.writeEntry("MimeType", "text/plain;");
0259         file.sync();
0260         qDebug() << "Created" << fakeTextPlugin << ", running kbuilsycoca";
0261         runKBuildSycoca();
0262         // Process the event
0263         int count = 0;
0264         while (!KService::serviceByDesktopPath(QStringLiteral("threadtextplugin.desktop"))) {
0265             qApp->processEvents();
0266             if (++count == 20) {
0267                 qFatal("sycoca doesn't have threadtextplugin.desktop");
0268             }
0269         }
0270     }
0271 
0272     // Start clean
0273     const QString servPath = fakeServiceDesktopFile();
0274     if (QFile::exists(servPath)) {
0275         QFile::remove(servPath);
0276     }
0277     if (KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))) {
0278         deleteFakeService();
0279     }
0280     threads.resize(5);
0281     for (int i = 0; i < threads.size(); i++) {
0282         threads[i] = i < 3 ? new WorkerThread : new EventLoopThread;
0283         threads[i]->start();
0284     }
0285 }
0286 
0287 void KSycocaThreadTest::cleanupTestCase()
0288 {
0289     QFile::remove(fakeTextPluginDesktopFile());
0290 }
0291 
0292 // duplicated from kcoreaddons/autotests/kdirwatch_unittest.cpp
0293 static void waitUntilAfter(const QDateTime &ctime)
0294 {
0295     int totalWait = 0;
0296     QDateTime now;
0297     Q_FOREVER {
0298         now = QDateTime::currentDateTime();
0299         if (now.toSecsSinceEpoch() == ctime.toSecsSinceEpoch()) // truncate milliseconds
0300         {
0301             totalWait += 50;
0302             QTest::qWait(50);
0303         } else {
0304             QVERIFY(now > ctime); // can't go back in time ;)
0305             QTest::qWait(50); // be safe
0306             break;
0307         }
0308     }
0309     // if (totalWait > 0)
0310     qDebug() << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate);
0311 }
0312 
0313 void KSycocaThreadTest::testCreateService()
0314 {
0315     // Wait one second so that ksycoca can detect a mtime change
0316     // ## IMHO this is a Qt bug, QFileInfo::lastModified() should include milliseconds
0317     waitUntilAfter(QDateTime::currentDateTime());
0318 
0319     createFakeService();
0320     QVERIFY(QFile::exists(fakeServiceDesktopFile()));
0321     qDebug() << "executing kbuildsycoca (1)";
0322     runKBuildSycoca();
0323 
0324     QTRY_VERIFY(KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop")));
0325 
0326     // Now wait to check that all threads saw that new service
0327     QTRY_COMPARE_WITH_TIMEOUT(threadsWhoSawFakeService(), threads.size(), 20000);
0328 }
0329 
0330 void KSycocaThreadTest::deleteFakeService()
0331 {
0332     s_fakeServiceDeleted = 1;
0333 
0334     qDebug() << "now deleting the fake service";
0335     KService::Ptr fakeService = KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"));
0336     QVERIFY(fakeService);
0337     const QString servPath = fakeServiceDesktopFile();
0338     QFile::remove(servPath);
0339 
0340 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 80)
0341     QSignalSpy spy(KSycoca::self(), qOverload<const QStringList &>(&KSycoca::databaseChanged));
0342 #else
0343     QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged);
0344 #endif
0345 
0346     QVERIFY(spy.isValid());
0347 
0348     qDebug() << "executing kbuildsycoca (2)";
0349     runKBuildSycoca();
0350 
0351 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 80)
0352     QVERIFY(!spy.isEmpty());
0353     QVERIFY(spy[0][0].toStringList().contains(QLatin1String("services")));
0354 #else
0355     QCOMPARE(spy.count(), 1);
0356 #endif
0357 
0358     QVERIFY(fakeService); // the whole point of refcounting is that this KService instance is still valid.
0359     QVERIFY(!QFile::exists(servPath));
0360 }
0361 
0362 void KSycocaThreadTest::createFakeService()
0363 {
0364     KDesktopFile file(fakeServiceDesktopFile());
0365     KConfigGroup group = file.desktopGroup();
0366     group.writeEntry("Name", "ThreadFakeService");
0367     group.writeEntry("Type", "Service");
0368     group.writeEntry("X-KDE-Library", "threadfakeservice");
0369     group.writeEntry("X-KDE-Protocols", "http,ftp");
0370     group.writeEntry("ServiceTypes", "KPluginInfo");
0371     group.writeEntry("MimeType", "text/plain;");
0372 }
0373 
0374 QTEST_MAIN(KSycocaThreadTest)
0375 #include "ksycocathreadtest.moc"