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"