File indexing completed on 2024-12-01 12:38:23

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2015 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include <KConfigGroup>
0009 #include <KDesktopFile>
0010 #include <QDebug>
0011 #include <QProcess>
0012 #include <QSignalSpy>
0013 #include <QTemporaryDir>
0014 #include <QTest>
0015 #include <kbuildsycoca_p.h>
0016 #include <kservice.h>
0017 #include <kservicefactory_p.h>
0018 #include <kservicetype.h>
0019 #include <kservicetypefactory_p.h>
0020 #include <ksycoca.h>
0021 #include <ksycoca_p.h>
0022 
0023 // ## use QFile::setFileTime when it lands in Qt
0024 #include <time.h>
0025 #ifdef Q_OS_UNIX
0026 #include <sys/time.h>
0027 #include <utime.h>
0028 #endif
0029 
0030 // taken from tst_qstandardpaths
0031 #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(Q_OS_BLACKBERRY) && !defined(Q_OS_ANDROID)
0032 #define Q_XDG_PLATFORM
0033 #endif
0034 
0035 // On Unix, lastModified() finally returns milliseconds as well, since Qt 5.8.0
0036 // Not sure about the situation on Windows though.
0037 static const int s_waitDelay = 10;
0038 
0039 extern KSERVICE_EXPORT int ksycoca_ms_between_checks;
0040 
0041 class KSycocaTest : public QObject
0042 {
0043     Q_OBJECT
0044 
0045 private Q_SLOTS:
0046     void initTestCase()
0047     {
0048         QStandardPaths::setTestModeEnabled(true);
0049 
0050         QVERIFY(m_tempDir.isValid());
0051 
0052         // we don't need the services dir -> ensure there isn't one, so we can check allResourceDirs below.
0053         QDir(servicesDir()).removeRecursively();
0054 
0055         QDir(menusDir()).removeRecursively();
0056         QDir().mkpath(menusDir() + QLatin1String{"/fakeSubserviceDirectory"});
0057 
0058 #ifdef Q_XDG_PLATFORM
0059         qputenv("XDG_DATA_DIRS", QFile::encodeName(m_tempDir.path()));
0060 
0061         // so that vfolder_menu doesn't go look into /etc and /usr
0062         qputenv("XDG_CONFIG_DIRS", QFile::encodeName(m_tempDir.path()));
0063 #else
0064         // We need to make changes to a global dir without messing up the system
0065         QSKIP("This test requires XDG_DATA_DIRS");
0066 #endif
0067         createGlobalServiceType();
0068     }
0069 
0070     void cleanupTestCase()
0071     {
0072         QFile::remove(serviceTypesDir() + QLatin1String{"/fakeLocalServiceType.desktop"});
0073         QFile::remove(KSycoca::absoluteFilePath());
0074     }
0075     void ensureCacheValidShouldCreateDB();
0076     void kBuildSycocaShouldEmitDatabaseChanged();
0077     void dirInFutureShouldRebuildSycocaOnce();
0078     void dirTimestampShouldBeCheckedRecursively();
0079     void recursiveCheckShouldIgnoreLinksGoingUp();
0080     void testAllResourceDirs();
0081     void testDeletingSycoca();
0082     void testNonReadableSycoca();
0083     void extraFileInFutureShouldRebuildSycocaOnce();
0084 
0085 private:
0086     void createGlobalServiceType()
0087     {
0088         KDesktopFile file(serviceTypesDir() + QLatin1String{"/fakeGlobalServiceType.desktop"});
0089         KConfigGroup group = file.desktopGroup();
0090         group.writeEntry("Comment", "Fake Global ServiceType");
0091         group.writeEntry("Type", "ServiceType");
0092         group.writeEntry("X-KDE-ServiceType", "FakeGlobalServiceType");
0093         file.sync();
0094         qDebug() << "created" << serviceTypesDir() + QLatin1String{"/fakeGlobalServiceType.desktop"};
0095     }
0096     QString servicesDir() const
0097     {
0098         return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String{"/kservices5"};
0099     }
0100 
0101     QString serviceTypesDir() const
0102     {
0103         return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String{"/kservicetypes5"};
0104     }
0105 
0106     QString extraFile() const
0107     {
0108         return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String{"/mimeapps.list"};
0109     }
0110 
0111     QString menusDir() const
0112     {
0113         return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String{"/menus"};
0114     }
0115 
0116     QString appsDir() const
0117     {
0118         return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/');
0119     }
0120 
0121     static void runKBuildSycoca(const QProcessEnvironment &environment, bool global = false);
0122 
0123     QTemporaryDir m_tempDir;
0124 };
0125 
0126 QTEST_MAIN(KSycocaTest)
0127 
0128 void KSycocaTest::ensureCacheValidShouldCreateDB() // this is what kded does on startup
0129 {
0130     QFile::remove(KSycoca::absoluteFilePath());
0131     KSycoca::self()->ensureCacheValid();
0132     QVERIFY(QFile::exists(KSycoca::absoluteFilePath()));
0133     QVERIFY(KServiceType::serviceType(QStringLiteral("FakeGlobalServiceType")));
0134 }
0135 
0136 void KSycocaTest::kBuildSycocaShouldEmitDatabaseChanged()
0137 {
0138     // It used to be a DBus signal, now it's file watching
0139     QTest::qWait(s_waitDelay);
0140     // Ensure kbuildsycoca has something to do
0141     QVERIFY(QFile::remove(serviceTypesDir() + QLatin1String{"/fakeGlobalServiceType.desktop"}));
0142     // Run kbuildsycoca
0143 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 80)
0144     QSignalSpy spy(KSycoca::self(), qOverload<const QStringList &>(&KSycoca::databaseChanged));
0145 #else
0146     QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged);
0147 #endif
0148 
0149     runKBuildSycoca(QProcessEnvironment::systemEnvironment());
0150     qDebug() << "waiting for signal";
0151     QVERIFY(spy.wait(20000));
0152     qDebug() << "got signal";
0153     // Put it back for other tests
0154     createGlobalServiceType();
0155 }
0156 
0157 void KSycocaTest::dirInFutureShouldRebuildSycocaOnce()
0158 {
0159     const QDateTime oldTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0160 
0161     // ### use QFile::setFileTime when it lands in Qt...
0162 #ifdef Q_OS_UNIX
0163     const QString path = serviceTypesDir();
0164     struct timeval tp;
0165     gettimeofday(&tp, nullptr);
0166     struct utimbuf utbuf;
0167     utbuf.actime = tp.tv_sec;
0168     utbuf.modtime = tp.tv_sec + 60; // 60 second in the future
0169     QCOMPARE(utime(QFile::encodeName(path).constData(), &utbuf), 0);
0170     qDebug("Time changed for %s", qPrintable(path));
0171     qDebug() << QDateTime::currentDateTime() << QFileInfo(path).lastModified();
0172 #else
0173     QSKIP("This test requires utime");
0174 #endif
0175     ksycoca_ms_between_checks = 0;
0176 
0177     QTest::qWait(s_waitDelay);
0178 
0179     KSycoca::self()->ensureCacheValid();
0180     const QDateTime newTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0181     QVERIFY(newTimestamp > oldTimestamp);
0182 
0183     QTest::qWait(s_waitDelay);
0184 
0185     KSycoca::self()->ensureCacheValid();
0186     const QDateTime againTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0187     QCOMPARE(againTimestamp, newTimestamp); // same mtime, it didn't get rebuilt
0188 
0189     // Ensure we don't pollute the other tests, with our dir in the future.
0190 #ifdef Q_OS_UNIX
0191     utbuf.modtime = tp.tv_sec;
0192     QCOMPARE(utime(QFile::encodeName(path).constData(), &utbuf), 0);
0193     qDebug("Time changed back for %s", qPrintable(path));
0194     qDebug() << QDateTime::currentDateTime() << QFileInfo(path).lastModified();
0195 #endif
0196 }
0197 
0198 void KSycocaTest::dirTimestampShouldBeCheckedRecursively()
0199 {
0200 #ifndef Q_OS_UNIX
0201     QSKIP("This test requires utime");
0202 #endif
0203     const QDateTime oldTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0204 
0205     const QString path = menusDir() + QLatin1String("/fakeSubserviceDirectory");
0206 
0207     // ### use QFile::setFileTime when it lands in Qt...
0208 #ifdef Q_OS_UNIX
0209     struct timeval tp;
0210     gettimeofday(&tp, nullptr);
0211     struct utimbuf utbuf;
0212     utbuf.actime = tp.tv_sec;
0213     utbuf.modtime = tp.tv_sec + 60; // 60 second in the future
0214     QCOMPARE(utime(QFile::encodeName(path).constData(), &utbuf), 0);
0215     qDebug("Time changed for %s", qPrintable(path));
0216     qDebug() << QDateTime::currentDateTime() << QFileInfo(path).lastModified();
0217 #endif
0218 
0219     ksycoca_ms_between_checks = 0;
0220     QTest::qWait(s_waitDelay);
0221 
0222     qDebug() << "Waited 1s, calling ensureCacheValid (should rebuild)";
0223     KSycoca::self()->ensureCacheValid();
0224     const QDateTime newTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0225     if (newTimestamp <= oldTimestamp) {
0226         qWarning() << "oldTimestamp=" << oldTimestamp << "newTimestamp=" << newTimestamp;
0227     }
0228     QVERIFY(newTimestamp > oldTimestamp);
0229 
0230     QTest::qWait(s_waitDelay);
0231 
0232     qDebug() << "Waited 1s, calling ensureCacheValid (should not rebuild)";
0233     KSycoca::self()->ensureCacheValid();
0234     const QDateTime againTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0235     QCOMPARE(againTimestamp, newTimestamp); // same mtime, it didn't get rebuilt
0236 
0237     // Ensure we don't pollute the other tests
0238     QDir(path).removeRecursively();
0239 }
0240 
0241 void KSycocaTest::recursiveCheckShouldIgnoreLinksGoingUp()
0242 {
0243 #ifndef Q_OS_UNIX
0244     QSKIP("This test requires symlinks and utime");
0245 #endif
0246     ksycoca_ms_between_checks = 0;
0247     const QString link = menusDir() + QLatin1String("/linkGoingUp");
0248     QVERIFY(QFile::link(QStringLiteral(".."), link));
0249     QTest::qWait(s_waitDelay);
0250     KSycoca::self()->ensureCacheValid();
0251     QVERIFY2(QFile::exists(KSycoca::absoluteFilePath()), qPrintable(KSycoca::absoluteFilePath()));
0252     const QDateTime oldTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0253     QVERIFY(oldTimestamp.isValid());
0254 
0255     const QString path = QFileInfo(menusDir()).absolutePath(); // the parent of the menus dir
0256 
0257     // ### use QFile::setFileTime when it lands in Qt...
0258 #ifdef Q_OS_UNIX
0259     struct timeval tp;
0260     gettimeofday(&tp, nullptr);
0261     struct utimbuf utbuf;
0262     utbuf.actime = tp.tv_sec;
0263     utbuf.modtime = tp.tv_sec + 60; // 60 second in the future
0264     QCOMPARE(utime(QFile::encodeName(path).constData(), &utbuf), 0);
0265     qDebug("Time changed for %s", qPrintable(path));
0266     qDebug() << QDateTime::currentDateTime() << QFileInfo(path).lastModified();
0267 #endif
0268 
0269     ksycoca_ms_between_checks = 0;
0270     QTest::qWait(s_waitDelay);
0271 
0272     qDebug() << "Waited 1s, calling ensureCacheValid (should not rebuild)";
0273     KSycoca::self()->ensureCacheValid();
0274     const QDateTime againTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0275     QCOMPARE(againTimestamp, oldTimestamp); // same mtime, it didn't get rebuilt
0276 
0277     // Ensure we don't pollute the other tests
0278     QFile(link).remove();
0279 }
0280 
0281 void KSycocaTest::runKBuildSycoca(const QProcessEnvironment &environment, bool global)
0282 {
0283     QProcess proc;
0284     const QString kbuildsycoca = QStringLiteral(KBUILDSYCOCAEXE);
0285     QVERIFY(!kbuildsycoca.isEmpty());
0286     QStringList args;
0287     args << QStringLiteral("--testmode");
0288     if (global) {
0289         args << QStringLiteral("--global");
0290     }
0291     proc.setProcessChannelMode(QProcess::ForwardedChannels);
0292     proc.start(kbuildsycoca, args);
0293     proc.setProcessEnvironment(environment);
0294 
0295     proc.waitForFinished();
0296     QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
0297 }
0298 
0299 void KSycocaTest::testAllResourceDirs()
0300 {
0301     // Dirs that exist and dirs that don't exist, should both be in allResourceDirs().
0302     const QStringList dirs = KSycoca::self()->allResourceDirs();
0303     QVERIFY2(dirs.contains(servicesDir()), qPrintable(dirs.join(QLatin1Char{','})));
0304     QVERIFY2(dirs.contains(serviceTypesDir()), qPrintable(dirs.join(QLatin1Char{','})));
0305 }
0306 
0307 void KSycocaTest::testDeletingSycoca()
0308 {
0309     // Mostly the same as ensureCacheValidShouldCreateDB, but KSycoca::self() already exists
0310     // So this is a check that deleting sycoca doesn't make apps crash (bug 343618).
0311     QFile::remove(KSycoca::absoluteFilePath());
0312     ksycoca_ms_between_checks = 0;
0313     QVERIFY(KServiceType::serviceType(QStringLiteral("FakeGlobalServiceType")));
0314     QVERIFY(QFile::exists(KSycoca::absoluteFilePath()));
0315 }
0316 
0317 void KSycocaTest::testNonReadableSycoca()
0318 {
0319     // Lose readability (to simulate e.g. owned by root)
0320     QFile(KSycoca::absoluteFilePath()).setPermissions(QFile::WriteOwner);
0321     ksycoca_ms_between_checks = 0;
0322     KBuildSycoca builder;
0323     QVERIFY(builder.recreate());
0324     QVERIFY(!KServiceType::serviceType(QStringLiteral("FakeGlobalServiceType")));
0325 
0326     // cleanup
0327     QFile::remove(KSycoca::absoluteFilePath());
0328 }
0329 
0330 void KSycocaTest::extraFileInFutureShouldRebuildSycocaOnce()
0331 {
0332     const QDateTime oldTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0333 
0334     auto path = extraFile();
0335     QFile f(path);
0336     QVERIFY(f.open(QIODevice::WriteOnly));
0337     auto beginning = f.fileTime(QFileDevice::FileModificationTime);
0338     auto newdate = beginning.addSecs(60);
0339     qDebug() << "Time changed for " << newdate << path;
0340     QVERIFY(f.setFileTime(newdate, QFileDevice::FileModificationTime));
0341 
0342     ksycoca_ms_between_checks = 0;
0343 
0344     QTest::qWait(s_waitDelay);
0345 
0346     KSycoca::self()->ensureCacheValid();
0347     const QDateTime newTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0348     QVERIFY(newTimestamp > oldTimestamp);
0349 
0350     QTest::qWait(s_waitDelay);
0351 
0352     KSycoca::self()->ensureCacheValid();
0353     const QDateTime againTimestamp = QFileInfo(KSycoca::absoluteFilePath()).lastModified();
0354     QCOMPARE(againTimestamp, newTimestamp); // same mtime, it didn't get rebuilt
0355 
0356     // Ensure we don't pollute the other tests, with our extra file in the future.
0357     QVERIFY(QFile::remove(path));
0358 }
0359 
0360 #include "ksycocatest.moc"