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"