File indexing completed on 2024-04-21 03:56:50

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 <ksycoca.h>
0025 
0026 #include "setupxdgdirs.h"
0027 
0028 static QString fakeAppDesktopFile()
0029 {
0030     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String{"/applications/org.kde.testapp.desktop"};
0031 }
0032 
0033 static QSet<QThread *> s_threadsWhoSawFakeService; // clazy:exclude=non-pod-global-static
0034 static QMutex s_setMutex; // clazy:exclude=non-pod-global-static
0035 static int threadsWhoSawFakeService()
0036 {
0037     QMutexLocker locker(&s_setMutex);
0038     return s_threadsWhoSawFakeService.count();
0039 }
0040 static QAtomicInt s_fakeServiceDeleted = 0; // clazy:exclude=non-pod-global-static
0041 
0042 class WorkerObject : public QObject
0043 {
0044     Q_OBJECT
0045 public:
0046     WorkerObject()
0047         : QObject()
0048     {
0049     }
0050 
0051 public Q_SLOTS:
0052     void work()
0053     {
0054         // qDebug() << QThread::currentThread() << "working...";
0055 
0056         QMimeDatabase db;
0057         const QList<QMimeType> allMimeTypes = db.allMimeTypes();
0058         Q_ASSERT(!allMimeTypes.isEmpty());
0059 
0060         const KService::List lst = KService::allServices();
0061         Q_ASSERT(!lst.isEmpty());
0062 
0063         for (const KService::Ptr &service : lst) {
0064             Q_ASSERT(service->isType(KST_KService));
0065             const QString name = service->name();
0066             const QString entryPath = service->entryPath();
0067             // qDebug() << name << "entryPath=" << entryPath << "menuId=" << service->menuId();
0068             Q_ASSERT(!name.isEmpty());
0069             Q_ASSERT(!entryPath.isEmpty());
0070 
0071             KService::Ptr lookedupService = KService::serviceByDesktopPath(entryPath);
0072             if (!lookedupService) {
0073                 if (entryPath == QLatin1String{"threadfakeservice.desktop"} && s_fakeServiceDeleted) { // ok, it got deleted meanwhile
0074                     continue;
0075                 }
0076                 qWarning() << entryPath << "is gone!";
0077             }
0078             Q_ASSERT(lookedupService); // not null
0079             QCOMPARE(lookedupService->entryPath(), entryPath);
0080 
0081             if (service->isApplication()) {
0082                 const QString menuId = service->menuId();
0083                 if (menuId.isEmpty()) {
0084                     qWarning("%s has an empty menuId!", qPrintable(entryPath));
0085                 }
0086                 Q_ASSERT(!menuId.isEmpty());
0087                 lookedupService = KService::serviceByMenuId(menuId);
0088                 if (!lookedupService) {
0089                     if (menuId == QLatin1String{"threadfakeservice"} && s_fakeServiceDeleted) { // ok, it got deleted meanwhile
0090                         continue;
0091                     }
0092                     qWarning() << menuId << "is gone!";
0093                 }
0094                 Q_ASSERT(lookedupService); // not null
0095                 QCOMPARE(lookedupService->menuId(), menuId);
0096             }
0097         }
0098 
0099         KServiceGroup::Ptr root = KServiceGroup::root();
0100         Q_ASSERT(root);
0101 
0102         if (KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))) {
0103             QMutexLocker locker(&s_setMutex);
0104             s_threadsWhoSawFakeService.insert(QThread::currentThread());
0105         }
0106     }
0107 };
0108 
0109 class WorkerThread : public QThread
0110 {
0111     Q_OBJECT
0112 public:
0113     WorkerThread()
0114         : QThread()
0115         , m_stop(false)
0116     {
0117     }
0118     void run() override
0119     {
0120         WorkerObject wo;
0121         while (!m_stop) {
0122             wo.work();
0123         }
0124     }
0125     virtual void stop()
0126     {
0127         m_stop = true;
0128     }
0129 
0130 private:
0131     QAtomicInt m_stop; // bool
0132 };
0133 
0134 /**
0135  * Threads with an event loop will be able to process "database changed" signals.
0136  * Threads without an event loop (like WorkerThread) cannot, so they will keep using
0137  * the old data.
0138  */
0139 class EventLoopThread : public WorkerThread
0140 {
0141     Q_OBJECT
0142 public:
0143     void run() override
0144     {
0145         // WorkerObject must belong to this thread, this is why we don't
0146         // have the slot work() in WorkerThread itself. Typical QThread trap!
0147         WorkerObject wo;
0148         QTimer timer;
0149         connect(&timer, &QTimer::timeout, &wo, &WorkerObject::work);
0150         timer.start(100);
0151         exec();
0152     }
0153     void stop() override
0154     {
0155         quit();
0156     }
0157 };
0158 
0159 // This code runs in the main thread
0160 class KSycocaThreadTest : public QObject
0161 {
0162     Q_OBJECT
0163 
0164 private Q_SLOTS:
0165     void initTestCase();
0166     void cleanupTestCase();
0167     void testCreateService();
0168     void testDeleteService()
0169     {
0170         deleteFakeService();
0171         QTimer::singleShot(1000, this, SLOT(slotFinish()));
0172     }
0173     void slotFinish()
0174     {
0175         qDebug() << "Terminating";
0176         for (int i = 0; i < threads.size(); i++) {
0177             threads[i]->stop();
0178         }
0179         for (int i = 0; i < threads.size(); i++) {
0180             threads[i]->wait();
0181         }
0182         cleanupTestCase();
0183         QCoreApplication::instance()->quit();
0184     }
0185 
0186 private:
0187     void createFakeService();
0188     void deleteFakeService();
0189     QList<WorkerThread *> threads;
0190 };
0191 
0192 static void runKBuildSycoca()
0193 {
0194     QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged);
0195 
0196     KBuildSycoca builder;
0197     QVERIFY(builder.recreate());
0198     qDebug() << "waiting for signal";
0199     QVERIFY(spy.wait(20000));
0200     qDebug() << "got signal";
0201 }
0202 
0203 void KSycocaThreadTest::initTestCase()
0204 {
0205     // Set up a layer in the bin dir so ksycoca finds the Application servicetypes
0206     setupXdgDirs();
0207     QStandardPaths::setTestModeEnabled(true);
0208 
0209     createFakeService();
0210 
0211     qDebug() << "Created org.kde.testapp, running kbuilsycoca";
0212     runKBuildSycoca();
0213     // Process the event
0214     int count = 0;
0215     while (!KService::serviceByDesktopName(QStringLiteral("org.kde.testapp"))) {
0216         qApp->processEvents();
0217         if (++count == 20) {
0218             qFatal("sycoca doesn't have org.kde.testapp.desktop");
0219         }
0220     }
0221 }
0222 
0223 void KSycocaThreadTest::cleanupTestCase()
0224 {
0225     QFile::remove(fakeAppDesktopFile());
0226 }
0227 
0228 // duplicated from kcoreaddons/autotests/kdirwatch_unittest.cpp
0229 static void waitUntilAfter(const QDateTime &ctime)
0230 {
0231     int totalWait = 0;
0232     QDateTime now;
0233     Q_FOREVER {
0234         now = QDateTime::currentDateTime();
0235         if (now.toSecsSinceEpoch() == ctime.toSecsSinceEpoch()) // truncate milliseconds
0236         {
0237             totalWait += 50;
0238             QTest::qWait(50);
0239         } else {
0240             QVERIFY(now > ctime); // can't go back in time ;)
0241             QTest::qWait(50); // be safe
0242             break;
0243         }
0244     }
0245     // if (totalWait > 0)
0246     qDebug() << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate);
0247 }
0248 
0249 void KSycocaThreadTest::testCreateService()
0250 {
0251     // Wait one second so that ksycoca can detect a mtime change
0252     // ## IMHO this is a Qt bug, QFileInfo::lastModified() should include milliseconds
0253     waitUntilAfter(QDateTime::currentDateTime());
0254 
0255     createFakeService();
0256     QVERIFY(QFile::exists(fakeAppDesktopFile()));
0257     qDebug() << "executing kbuildsycoca (1)";
0258     runKBuildSycoca();
0259 
0260     QTRY_VERIFY(KService::serviceByDesktopName(QStringLiteral("org.kde.testapp")));
0261 
0262     // Now wait to check that all threads saw that new service
0263     QTRY_COMPARE_WITH_TIMEOUT(threadsWhoSawFakeService(), threads.size(), 20000);
0264 }
0265 
0266 void KSycocaThreadTest::deleteFakeService()
0267 {
0268     s_fakeServiceDeleted = 1;
0269 
0270     qDebug() << "now deleting the fake service";
0271     KService::Ptr fakeService = KService::serviceByDesktopName(QStringLiteral("org.kde.testapp"));
0272     QVERIFY(fakeService);
0273     const QString servPath = fakeAppDesktopFile();
0274     QFile::remove(servPath);
0275 
0276     QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged);
0277 
0278     QVERIFY(spy.isValid());
0279 
0280     qDebug() << "executing kbuildsycoca (2)";
0281     runKBuildSycoca();
0282 
0283     QCOMPARE(spy.count(), 1);
0284 
0285     QVERIFY(fakeService); // the whole point of refcounting is that this KService instance is still valid.
0286     QVERIFY(!QFile::exists(servPath));
0287 }
0288 
0289 void KSycocaThreadTest::createFakeService()
0290 {
0291     KDesktopFile file(fakeAppDesktopFile());
0292     KConfigGroup group = file.desktopGroup();
0293     group.writeEntry("Name", "Foo");
0294     group.writeEntry("Type", "Application");
0295     group.writeEntry("Exec", "bla");
0296 }
0297 
0298 QTEST_MAIN(KSycocaThreadTest)
0299 #include "ksycocathreadtest.moc"