Warning, file /frameworks/kservice/autotests/ksycocathreadtest.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
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"