diff --git a/autotests/ksycocathreadtest.cpp b/autotests/ksycocathreadtest.cpp index 476469f..ea9dc5c 100644 --- a/autotests/ksycocathreadtest.cpp +++ b/autotests/ksycocathreadtest.cpp @@ -1,372 +1,374 @@ /* This file is part of the KDE project Copyright 2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QString fakeTextPluginDesktopFile() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + "threadtextplugin.desktop"; } static QString fakeServiceDesktopFile() { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + "threadfakeservice.desktop"; } // Helper method for all the trader tests static bool offerListHasService(const KService::List &offers, const QString &entryPath) { bool found = false; KService::List::const_iterator it = offers.begin(); for (; it != offers.end(); ++it) { if ((*it)->entryPath() == entryPath) { if (found) { // should be there only once qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath)); return false; // make test fail } found = true; } } return found; } static QSet s_threadsWhoSawFakeService; static QMutex s_setMutex; static int threadsWhoSawFakeService() { QMutexLocker locker(&s_setMutex); return s_threadsWhoSawFakeService.count(); } static QAtomicInt s_fakeServiceDeleted = 0; class WorkerObject : public QObject { Q_OBJECT public: WorkerObject() : QObject() {} public Q_SLOTS: void work() { //qDebug() << QThread::currentThread() << "working..."; const KServiceType::List allServiceTypes = KServiceType::allServiceTypes(); Q_ASSERT(!allServiceTypes.isEmpty()); QMimeDatabase db; const QList allMimeTypes = db.allMimeTypes(); Q_ASSERT(!allMimeTypes.isEmpty()); const KService::List lst = KService::allServices(); Q_ASSERT(!lst.isEmpty()); for (KService::List::ConstIterator it = lst.begin(); it != lst.end(); ++it) { const KService::Ptr service = (*it); 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 == "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 == "threadfakeservice" && s_fakeServiceDeleted) { // ok, it got deleted meanwhile continue; } qWarning() << menuId << "is gone!"; } Q_ASSERT(lookedupService); // not null QCOMPARE(lookedupService->menuId(), menuId); } } KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("KPluginInfo")); Q_ASSERT(offerListHasService(offers, QStringLiteral("threadtextplugin.desktop"))); offers = KServiceTypeTrader::self()->query(QStringLiteral("KPluginInfo"), QStringLiteral("Library == 'threadtextplugin'")); Q_ASSERT(offers.count() == 1); QVERIFY(offerListHasService(offers, QStringLiteral("threadtextplugin.desktop"))); 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, SIGNAL(timeout()), &wo, SLOT(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: - // Note that this isn't a QTest based test. - // All these methods are called manually. 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(); QVector threads; }; static void runKBuildSycoca() { QSignalSpy spy(KSycoca::self(), SIGNAL(databaseChanged(QStringList))); 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 KPluginInfo and Application servicetypes + const QByteArray defaultDataDirs = qEnvironmentVariableIsSet("XDG_DATA_DIRS") ? qgetenv("XDG_DATA_DIRS") : QByteArray("/usr/local:/usr"); + const QByteArray modifiedDataDirs = QFile::encodeName(QCoreApplication::applicationDirPath()) + "/share:" + defaultDataDirs; + qputenv("XDG_DATA_DIRS", modifiedDataDirs); QStandardPaths::enableTestMode(true); // This service is always there. Used in the trader queries from the thread. const QString fakeTextPlugin = fakeTextPluginDesktopFile(); if (!QFile::exists(fakeTextPlugin)) { KDesktopFile file(fakeTextPlugin); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "ThreadTextPlugin"); group.writeEntry("Type", "Service"); group.writeEntry("X-KDE-Library", "threadtextplugin"); group.writeEntry("X-KDE-Protocols", "http,ftp"); group.writeEntry("ServiceTypes", "KPluginInfo"); group.writeEntry("MimeType", "text/plain;"); file.sync(); qDebug() << "Created" << fakeTextPlugin << ", running kbuilsycoca"; runKBuildSycoca(); // Process the event int count = 0; while (!KService::serviceByDesktopPath(QStringLiteral("threadtextplugin.desktop"))) { qApp->processEvents(); if (++count == 20) { qFatal("sycoca doesn't have threadtextplugin.desktop"); } } } // Start clean const QString servPath = fakeServiceDesktopFile(); if (QFile::exists(servPath)) { QFile::remove(servPath); } if (KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))) { deleteFakeService(); } threads.resize(5); for (int i = 0; i < threads.size(); i++) { threads[i] = i < 3 ? new WorkerThread : new EventLoopThread; threads[i]->start(); } } void KSycocaThreadTest::cleanupTestCase() { QFile::remove(fakeTextPluginDesktopFile()); } // 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.toTime_t() == ctime.toTime_t()) // 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(fakeServiceDesktopFile())); qDebug() << "executing kbuildsycoca (1)"; runKBuildSycoca(); QTRY_VERIFY(KService::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop"))); // 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::serviceByDesktopPath(QStringLiteral("threadfakeservice.desktop")); QVERIFY(fakeService); const QString servPath = fakeServiceDesktopFile(); QFile::remove(servPath); QSignalSpy spy(KSycoca::self(), SIGNAL(databaseChanged(QStringList))); QVERIFY(spy.isValid()); qDebug() << "executing kbuildsycoca (2)"; runKBuildSycoca(); QVERIFY(!spy.isEmpty()); QVERIFY(spy[0][0].toStringList().contains(QStringLiteral("services"))); QVERIFY(fakeService); // the whole point of refcounting is that this KService instance is still valid. QVERIFY(!QFile::exists(servPath)); } void KSycocaThreadTest::createFakeService() { KDesktopFile file(fakeServiceDesktopFile()); KConfigGroup group = file.desktopGroup(); group.writeEntry("Name", "ThreadFakeService"); group.writeEntry("Type", "Service"); group.writeEntry("X-KDE-Library", "threadfakeservice"); group.writeEntry("X-KDE-Protocols", "http,ftp"); group.writeEntry("ServiceTypes", "KPluginInfo"); group.writeEntry("MimeType", "text/plain;"); } QTEST_MAIN(KSycocaThreadTest) #include "ksycocathreadtest.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 02dfcf2..9ce2d31 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,191 +1,200 @@ include(CheckSymbolExists) include(CheckFunctionExists) check_function_exists(mmap HAVE_MMAP) check_symbol_exists(posix_madvise "sys/mman.h" HAVE_MADVISE) configure_file(config-ksycoca.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-ksycoca.h ) set(kservice_SRCS kdeinit/ktoolinvocation.cpp services/kautostart.cpp services/kmimetypefactory.cpp services/kmimetypetrader.cpp services/kservice.cpp services/kserviceaction.cpp services/kservicefactory.cpp services/kservicegroup.cpp services/kservicegroupfactory.cpp services/kserviceoffer.cpp services/kservicetype.cpp services/kservicetypefactory.cpp services/kservicetypeprofile.cpp services/kservicetypetrader.cpp services/ktraderparse.cpp services/ktraderparsetree.cpp services/kplugininfo.cpp sycoca/ksycoca.cpp sycoca/ksycocadevices.cpp sycoca/ksycocadict.cpp sycoca/ksycocaentry.cpp sycoca/ksycocafactory.cpp sycoca/kmemfile.cpp sycoca/kbuildmimetypefactory.cpp sycoca/kbuildservicetypefactory.cpp sycoca/kbuildservicefactory.cpp sycoca/kbuildservicegroupfactory.cpp sycoca/kbuildsycoca.cpp sycoca/kctimefactory.cpp sycoca/kmimeassociations.cpp sycoca/vfolder_menu.cpp plugin/kplugintrader.cpp plugin/kdbusservicestarter.cpp ) ecm_qt_declare_logging_category(kservice_SRCS HEADER servicesdebug.h IDENTIFIER SERVICES CATEGORY_NAME kf5.kservice.services) ecm_qt_declare_logging_category(kservice_SRCS HEADER sycocadebug.h IDENTIFIER SYCOCA CATEGORY_NAME kf5.kservice.sycoca) if (WIN32) LIST(APPEND kservice_SRCS kdeinit/ktoolinvocation_win.cpp ) endif() if (UNIX) LIST(APPEND kservice_SRCS kdeinit/ktoolinvocation_x11.cpp ) endif() bison_target(TraderParser services/yacc.y ${CMAKE_CURRENT_BINARY_DIR}/yacc.c COMPILE_FLAGS "-p kiotrader -d" ) flex_target(TraderLexer services/lex.l ${CMAKE_CURRENT_BINARY_DIR}/lex.c COMPILE_FLAGS "-Pkiotrader -B -i" ) add_flex_bison_dependency(TraderLexer TraderParser) list(APPEND kservice_SRCS ${BISON_TraderParser_OUTPUTS} ${FLEX_TraderLexer_OUTPUTS}) set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/yacc.h PROPERTY SKIP_AUTOMOC TRUE) # don't run automoc on this file # kservice cannot depend on kinit (because kinit->kio->kservice), so we need a copy of org.kde.KLauncher.xml here. # And I don't want to have it here as a source file (who wants to edit dbus xml by hand), so it can be # generated from klauncher's implementation header. qt5_add_dbus_interface(kservice_SRCS kdeinit/org.kde.KLauncher.xml klauncher_iface) add_library(KF5Service ${kservice_SRCS}) if(WIN32) #unistd.h does not exist on windows target_compile_definitions(KF5Service PRIVATE YY_NO_UNISTD_H=1) endif() generate_export_header(KF5Service BASE_NAME KService) add_library(KF5::Service ALIAS KF5Service) set(kservice_includes ${CMAKE_CURRENT_BINARY_DIR}/.. # Since we publicly include kservice_version.h ${CMAKE_CURRENT_SOURCE_DIR}/services ${CMAKE_CURRENT_SOURCE_DIR}/sycoca ${CMAKE_CURRENT_SOURCE_DIR}/plugin ${CMAKE_CURRENT_SOURCE_DIR}/kdeinit ) target_include_directories(KF5Service PUBLIC "$") target_include_directories(KF5Service INTERFACE "$") target_link_libraries(KF5Service PUBLIC KF5::ConfigCore # KConfig and friends KF5::CoreAddons # KShell KPluginLoader PRIVATE KF5::I18n KF5::DBusAddons # KDEInitInterface Qt5::Xml # (for vfolder menu) QDomDocument ) set_target_properties(KF5Service PROPERTIES VERSION ${KSERVICE_VERSION_STRING} SOVERSION ${KSERVICE_SOVERSION} EXPORT_NAME Service ) ecm_generate_headers(KService_HEADERS HEADER_NAMES KPluginTrader KDBusServiceStarter RELATIVE plugin REQUIRED_HEADERS KService_HEADERS ) ecm_generate_headers(KService_HEADERS HEADER_NAMES KSycoca KSycocaEntry KSycocaType RELATIVE sycoca REQUIRED_HEADERS KService_HEADERS ) ecm_generate_headers(KService_HEADERS HEADER_NAMES KToolInvocation RELATIVE kdeinit REQUIRED_HEADERS KService_HEADERS ) ecm_generate_headers(KService_HEADERS HEADER_NAMES KAutostart KMimeTypeTrader KService KServiceAction KServiceGroup KServiceType KServiceTypeProfile KServiceTypeTrader KPluginInfo RELATIVE services REQUIRED_HEADERS KService_HEADERS ) install(FILES services/kplugininfo.desktop services/application.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR} ) +# Local copy for the unittests +add_custom_target(copy_servicetypes) +file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${KDE_INSTALL_KSERVICETYPES5DIR}) +add_custom_command(TARGET copy_servicetypes PRE_BUILD COMMAND + ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/services/kplugininfo.desktop ${CMAKE_CURRENT_SOURCE_DIR}/services/application.desktop + ${CMAKE_BINARY_DIR}/bin/${KDE_INSTALL_KSERVICETYPES5DIR}) +add_dependencies(KF5Service copy_servicetypes) + if (WIN32) install( FILES applications.menu DESTINATION ${KDE_INSTALL_DATAROOTDIR}/xdg/menus RENAME ${APPLICATIONS_MENU_NAME} ) else () install( FILES applications.menu DESTINATION ${KDE_INSTALL_SYSCONFDIR}/xdg/menus RENAME ${APPLICATIONS_MENU_NAME} ) endif () install(TARGETS KF5Service EXPORT KF5ServiceTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/kservice_export.h" ${KService_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KService COMPONENT Devel ) if(BUILD_QCH) ecm_add_qch( KF5Service_QCH NAME KService BASE_NAME KF5Service VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KService_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" LINK_QCHS KF5Config_QCH KF5CoreAddons_QCH BLANK_MACROS KSERVICE_EXPORT KSERVICE_DEPRECATED KSERVICE_DEPRECATED_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() add_subdirectory(kbuildsycoca) include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KService LIB_NAME KF5Service DEPS "KConfigCore" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KService) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR})