/* This file is part of the KDE project SPDX-FileCopyrightText: 2009 David Faure SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "setupxdgdirs.h" static QString fakeAppDesktopFile() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String{"/applications/org.kde.testapp.desktop"}; } static QSet s_threadsWhoSawFakeService; // clazy:exclude=non-pod-global-static static QMutex s_setMutex; // clazy:exclude=non-pod-global-static static int threadsWhoSawFakeService() { QMutexLocker locker(&s_setMutex); return s_threadsWhoSawFakeService.count(); } static QAtomicInt s_fakeServiceDeleted = 0; // clazy:exclude=non-pod-global-static class WorkerObject : public QObject { Q_OBJECT public: WorkerObject() : QObject() { } public Q_SLOTS: void work() { // qDebug() << QThread::currentThread() << "working..."; QMimeDatabase db; const QList allMimeTypes = db.allMimeTypes(); Q_ASSERT(!allMimeTypes.isEmpty()); const KService::List lst = KService::allServices(); Q_ASSERT(!lst.isEmpty()); for (const KService::Ptr &service : lst) { Q_ASSERT(service->isType(KST_KService)); const QString name = service->name(); const QString entryPath = service->entryPath(); // qDebug() << name << "entryPath=" << entryPath << "menuId=" << service->menuId(); Q_ASSERT(!name.isEmpty()); Q_ASSERT(!entryPath.isEmpty()); KService::Ptr lookedupService = KService::serviceByDesktopPath(entryPath); if (!lookedupService) { if (entryPath == QLatin1String{"threadfakeservice.desktop"} && s_fakeServiceDeleted) { // ok, it got deleted meanwhile continue; } qWarning() << entryPath << "is gone!"; } Q_ASSERT(lookedupService); // not null QCOMPARE(lookedupService->entryPath(), entryPath); if (service->isApplication()) { const QString menuId = service->menuId(); if (menuId.isEmpty()) { qWarning("%s has an empty menuId!", qPrintable(entryPath)); } Q_ASSERT(!menuId.isEmpty()); lookedupService = KService::serviceByMenuId(menuId); if (!lookedupService) { if (menuId == QLatin1String{"threadfakeservice"} && s_fakeServiceDeleted) { // ok, it got deleted meanwhile continue; } qWarning() << menuId << "is gone!"; } Q_ASSERT(lookedupService); // not null QCOMPARE(lookedupService->menuId(), menuId); } } KServiceGroup::Ptr root = KServiceGroup::root(); Q_ASSERT(root); if (KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))) { QMutexLocker locker(&s_setMutex); s_threadsWhoSawFakeService.insert(QThread::currentThread()); } } }; class WorkerThread : public QThread { Q_OBJECT public: WorkerThread() : QThread() , m_stop(false) { } void run() override { WorkerObject wo; while (!m_stop) { wo.work(); } } virtual void stop() { m_stop = true; } private: QAtomicInt m_stop; // bool }; /** * Threads with an event loop will be able to process "database changed" signals. * Threads without an event loop (like WorkerThread) cannot, so they will keep using * the old data. */ class EventLoopThread : public WorkerThread { Q_OBJECT public: void run() override { // WorkerObject must belong to this thread, this is why we don't // have the slot work() in WorkerThread itself. Typical QThread trap! WorkerObject wo; QTimer timer; connect(&timer, &QTimer::timeout, &wo, &WorkerObject::work); timer.start(100); exec(); } void stop() override { quit(); } }; // This code runs in the main thread class KSycocaThreadTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testCreateService(); void testDeleteService() { deleteFakeService(); QTimer::singleShot(1000, this, SLOT(slotFinish())); } void slotFinish() { qDebug() << "Terminating"; for (int i = 0; i < threads.size(); i++) { threads[i]->stop(); } for (int i = 0; i < threads.size(); i++) { threads[i]->wait(); } cleanupTestCase(); QCoreApplication::instance()->quit(); } private: void createFakeService(); void deleteFakeService(); QList threads; }; static void runKBuildSycoca() { QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged); KBuildSycoca builder; QVERIFY(builder.recreate()); qDebug() << "waiting for signal"; QVERIFY(spy.wait(20000)); qDebug() << "got signal"; } void KSycocaThreadTest::initTestCase() { // Set up a layer in the bin dir so ksycoca finds the Application servicetypes setupXdgDirs(); QStandardPaths::setTestModeEnabled(true); createFakeService(); qDebug() << "Created org.kde.testapp, running kbuilsycoca"; runKBuildSycoca(); // Process the event int count = 0; while (!KService::serviceByDesktopName(QStringLiteral("org.kde.testapp"))) { qApp->processEvents(); if (++count == 20) { qFatal("sycoca doesn't have org.kde.testapp.desktop"); } } } void KSycocaThreadTest::cleanupTestCase() { QFile::remove(fakeAppDesktopFile()); } // duplicated from kcoreaddons/autotests/kdirwatch_unittest.cpp static void waitUntilAfter(const QDateTime &ctime) { int totalWait = 0; QDateTime now; Q_FOREVER { now = QDateTime::currentDateTime(); if (now.toSecsSinceEpoch() == ctime.toSecsSinceEpoch()) // truncate milliseconds { totalWait += 50; QTest::qWait(50); } else { QVERIFY(now > ctime); // can't go back in time ;) QTest::qWait(50); // be safe break; } } // if (totalWait > 0) qDebug() << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate); } void KSycocaThreadTest::testCreateService() { // Wait one second so that ksycoca can detect a mtime change // ## IMHO this is a Qt bug, QFileInfo::lastModified() should include milliseconds waitUntilAfter(QDateTime::currentDateTime()); createFakeService(); QVERIFY(QFile::exists(fakeAppDesktopFile())); qDebug() << "executing kbuildsycoca (1)"; runKBuildSycoca(); QTRY_VERIFY(KService::serviceByDesktopName(QStringLiteral("org.kde.testapp"))); // Now wait to check that all threads saw that new service QTRY_COMPARE_WITH_TIMEOUT(threadsWhoSawFakeService(), threads.size(), 20000); } void KSycocaThreadTest::deleteFakeService() { s_fakeServiceDeleted = 1; qDebug() << "now deleting the fake service"; KService::Ptr fakeService = KService::serviceByDesktopName(QStringLiteral("org.kde.testapp")); QVERIFY(fakeService); const QString servPath = fakeAppDesktopFile(); QFile::remove(servPath); QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged); QVERIFY(spy.isValid()); qDebug() << "executing kbuildsycoca (2)"; runKBuildSycoca(); QCOMPARE(spy.count(), 1); QVERIFY(fakeService); // the whole point of refcounting is that this KService instance is still valid. QVERIFY(!QFile::exists(servPath)); } void KSycocaThreadTest::createFakeService() { KDesktopFile file(fakeAppDesktopFile()); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "Foo"); group.writeEntry("Type", "Application"); group.writeEntry("Exec", "bla"); } QTEST_MAIN(KSycocaThreadTest) #include "ksycocathreadtest.moc"