File indexing completed on 2024-04-14 03:54:33

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