File indexing completed on 2024-04-28 07:42:12
0001 /* 0002 This file is part of the KDE libraries 0003 0004 SPDX-FileCopyrightText: 2009 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org> 0006 0007 SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 #include <kdirwatch.h> 0011 #include <kdirwatch_p.h> 0012 0013 #include <QDebug> 0014 #include <QDir> 0015 #include <QFileInfo> 0016 #include <QSignalSpy> 0017 #include <QTemporaryDir> 0018 #include <QTest> 0019 #include <QThread> 0020 #include <sys/stat.h> 0021 #ifdef Q_OS_UNIX 0022 #include <unistd.h> // ::link() 0023 #endif 0024 0025 #include "kcoreaddons_debug.h" 0026 #include "kdirwatch_test_utils.h" 0027 0028 using namespace KDirWatchTestUtils; 0029 0030 // Debugging notes: to see which inotify signals are emitted, either set s_verboseDebug=true 0031 // at the top of kdirwatch.cpp, or use the command-line tool "inotifywait -m /path" 0032 0033 class KDirWatch_UnitTest : public QObject 0034 { 0035 Q_OBJECT 0036 public: 0037 KDirWatch_UnitTest() 0038 { 0039 // Speed up the test by making the kdirwatch timer (to compress changes) faster 0040 qputenv("KDIRWATCH_POLLINTERVAL", "50"); 0041 qputenv("KDIRWATCH_METHOD", KDIRWATCH_TEST_METHOD); 0042 s_staticObjectUsingSelf(); 0043 0044 m_path = m_tempDir.path() + QLatin1Char('/'); 0045 KDirWatch *dirW = &s_staticObject()->m_dirWatch; 0046 m_stat = dirW->internalMethod() == KDirWatch::Stat; 0047 m_slow = m_stat; 0048 qCDebug(KCOREADDONS_DEBUG) << "Using method" << methodToString(dirW->internalMethod()); 0049 } 0050 0051 private Q_SLOTS: // test methods 0052 void initTestCase() 0053 { 0054 QFileInfo pathInfo(m_path); 0055 QVERIFY(pathInfo.isDir() && pathInfo.isWritable()); 0056 0057 // By creating the files upfront, we save waiting a full second for an mtime change 0058 createFile(m_path + QLatin1String("ExistingFile")); 0059 createFile(m_path + QLatin1String("TestFile")); 0060 createFile(m_path + QLatin1String("nested_0")); 0061 createFile(m_path + QLatin1String("nested_1")); 0062 0063 s_staticObject()->m_dirWatch.addFile(m_path + QLatin1String("ExistingFile")); 0064 } 0065 void touchOneFile(); 0066 void touch1000Files(); 0067 void watchAndModifyOneFile(); 0068 void removeAndReAdd(); 0069 void watchNonExistent(); 0070 void watchNonExistentWithSingleton(); 0071 void testDelete(); 0072 void testDeleteAndRecreateFile(); 0073 void testDeleteAndRecreateDir(); 0074 void testMoveTo(); 0075 void nestedEventLoop(); 0076 void testHardlinkChange(); 0077 void stopAndRestart(); 0078 void testRefcounting(); 0079 void testRelativeRefcounting(); 0080 void testMoveToThread(); 0081 0082 protected Q_SLOTS: // internal slots 0083 void nestedEventLoopSlot(); 0084 0085 private: 0086 QList<QVariantList> waitForDirtySignal(KDirWatch &watch, int expected); 0087 QList<QVariantList> waitForDeletedSignal(KDirWatch &watch, int expected); 0088 bool waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path); 0089 bool waitForRecreationSignal(KDirWatch &watch, const QString &path); 0090 bool verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath); 0091 QString createFile(int num); 0092 void createFile(const QString &file) 0093 { 0094 KDirWatchTestUtils::createFile(file, m_slow); 0095 } 0096 void removeFile(int num); 0097 void appendToFile(const QString &path); 0098 void appendToFile(int num); 0099 0100 QTemporaryDir m_tempDir; 0101 QString m_path; 0102 bool m_slow; 0103 bool m_stat; 0104 }; 0105 0106 QTEST_MAIN(KDirWatch_UnitTest) 0107 0108 static const int s_maxTries = 50; 0109 0110 // helper method: create a file (identified by number) 0111 QString KDirWatch_UnitTest::createFile(int num) 0112 { 0113 const QString fileName = QLatin1String(s_filePrefix) + QString::number(num); 0114 KDirWatchTestUtils::createFile(m_path + fileName, m_slow); 0115 return m_path + fileName; 0116 } 0117 0118 // helper method: delete a file (identified by number) 0119 void KDirWatch_UnitTest::removeFile(int num) 0120 { 0121 const QString fileName = QLatin1String(s_filePrefix) + QString::number(num); 0122 QFile::remove(m_path + fileName); 0123 } 0124 0125 // helper method: modifies a file 0126 void KDirWatch_UnitTest::appendToFile(const QString &path) 0127 { 0128 QVERIFY(QFile::exists(path)); 0129 waitUntilMTimeChange(path); 0130 0131 QFile file(path); 0132 QVERIFY(file.open(QIODevice::Append | QIODevice::WriteOnly)); 0133 file.write(QByteArray("foobar")); 0134 file.close(); 0135 } 0136 0137 // helper method: modifies a file (identified by number) 0138 void KDirWatch_UnitTest::appendToFile(int num) 0139 { 0140 const QString fileName = QLatin1String(s_filePrefix) + QString::number(num); 0141 appendToFile(m_path + fileName); 0142 } 0143 0144 static QString removeTrailingSlash(const QString &path) 0145 { 0146 if (path.endsWith(QLatin1Char('/'))) { 0147 return path.left(path.length() - 1); 0148 } else { 0149 return path; 0150 } 0151 } 0152 0153 // helper method 0154 QList<QVariantList> KDirWatch_UnitTest::waitForDirtySignal(KDirWatch &watch, int expected) 0155 { 0156 QSignalSpy spyDirty(&watch, &KDirWatch::dirty); 0157 int numTries = 0; 0158 // Give time for KDirWatch to notify us 0159 while (spyDirty.count() < expected) { 0160 if (++numTries > s_maxTries) { 0161 qWarning() << "Timeout waiting for KDirWatch. Got" << spyDirty.count() << "dirty() signals, expected" << expected; 0162 return spyDirty; 0163 } 0164 spyDirty.wait(50); 0165 } 0166 return spyDirty; 0167 } 0168 0169 bool KDirWatch_UnitTest::waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path) 0170 { 0171 const QString expectedPath = removeTrailingSlash(path); 0172 while (true) { 0173 QSignalSpy spyDirty(&watch, sig); 0174 int numTries = 0; 0175 // Give time for KDirWatch to notify us 0176 while (spyDirty.isEmpty()) { 0177 if (++numTries > s_maxTries) { 0178 qWarning() << "Timeout waiting for KDirWatch signal" << QByteArray(sig).mid(1) << "(" << path << ")"; 0179 return false; 0180 } 0181 spyDirty.wait(50); 0182 } 0183 return verifySignalPath(spyDirty, sig, expectedPath); 0184 } 0185 } 0186 0187 bool KDirWatch_UnitTest::verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath) 0188 { 0189 for (int i = 0; i < spy.count(); ++i) { 0190 const QString got = spy[i][0].toString(); 0191 if (got == expectedPath) { 0192 return true; 0193 } 0194 if (got.startsWith(expectedPath + QLatin1Char('/'))) { 0195 qCDebug(KCOREADDONS_DEBUG) << "Ignoring (inotify) notification of" << (sig + 1) << '(' << got << ')'; 0196 continue; 0197 } 0198 qWarning() << "Expected" << sig << '(' << expectedPath << ')' << "but got" << sig << '(' << got << ')'; 0199 return false; 0200 } 0201 return false; 0202 } 0203 0204 bool KDirWatch_UnitTest::waitForRecreationSignal(KDirWatch &watch, const QString &path) 0205 { 0206 // When watching for a deleted + created signal pair, the two might come so close that 0207 // using waitForOneSignal will miss the created signal. This function monitors both all 0208 // the time to ensure both are received. 0209 // 0210 // In addition, it allows dirty() to be emitted (for that same path) as an alternative 0211 0212 const QString expectedPath = removeTrailingSlash(path); 0213 QSignalSpy spyDirty(&watch, &KDirWatch::dirty); 0214 QSignalSpy spyDeleted(&watch, &KDirWatch::deleted); 0215 QSignalSpy spyCreated(&watch, &KDirWatch::created); 0216 0217 int numTries = 0; 0218 while (spyDeleted.isEmpty() && spyDirty.isEmpty()) { 0219 if (++numTries > s_maxTries) { 0220 return false; 0221 } 0222 spyDeleted.wait(50); 0223 while (!spyDirty.isEmpty()) { 0224 if (spyDirty.at(0).at(0).toString() != expectedPath) { // unrelated 0225 spyDirty.removeFirst(); 0226 } 0227 } 0228 } 0229 if (!spyDirty.isEmpty()) { 0230 return true; 0231 } 0232 0233 // Don't bother waiting for the created signal if the signal spy already received a signal. 0234 if (spyCreated.isEmpty() && !spyCreated.wait(50 * s_maxTries)) { 0235 qWarning() << "Timeout waiting for KDirWatch signal created(QString) (" << path << ")"; 0236 return false; 0237 } 0238 0239 return verifySignalPath(spyDeleted, "deleted(QString)", expectedPath) && verifySignalPath(spyCreated, "created(QString)", expectedPath); 0240 } 0241 0242 QList<QVariantList> KDirWatch_UnitTest::waitForDeletedSignal(KDirWatch &watch, int expected) 0243 { 0244 QSignalSpy spyDeleted(&watch, &KDirWatch::created); 0245 int numTries = 0; 0246 // Give time for KDirWatch to notify us 0247 while (spyDeleted.count() < expected) { 0248 if (++numTries > s_maxTries) { 0249 qWarning() << "Timeout waiting for KDirWatch. Got" << spyDeleted.count() << "deleted() signals, expected" << expected; 0250 return spyDeleted; 0251 } 0252 spyDeleted.wait(50); 0253 } 0254 return spyDeleted; 0255 } 0256 0257 void KDirWatch_UnitTest::touchOneFile() // watch a dir, create a file in it 0258 { 0259 KDirWatch watch; 0260 watch.addDir(m_path); 0261 watch.startScan(); 0262 0263 waitUntilMTimeChange(m_path); 0264 0265 // dirty(the directory) should be emitted. 0266 QSignalSpy spyCreated(&watch, &KDirWatch::created); 0267 createFile(0); 0268 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); 0269 QCOMPARE(spyCreated.count(), 0); // "This is not emitted when creating a file is created in a watched directory." 0270 0271 removeFile(0); 0272 } 0273 0274 void KDirWatch_UnitTest::touch1000Files() 0275 { 0276 KDirWatch watch; 0277 watch.addDir(m_path); 0278 watch.startScan(); 0279 0280 waitUntilMTimeChange(m_path); 0281 0282 const int fileCount = 100; 0283 for (int i = 0; i < fileCount; ++i) { 0284 createFile(i); 0285 } 0286 0287 QList<QVariantList> spy = waitForDirtySignal(watch, fileCount); 0288 if (watch.internalMethod() == KDirWatch::INotify) { 0289 QVERIFY(spy.count() >= fileCount); 0290 } else { 0291 // More stupid backends just see one mtime change on the directory 0292 QVERIFY(spy.count() >= 1); 0293 } 0294 0295 for (int i = 0; i < fileCount; ++i) { 0296 removeFile(i); 0297 } 0298 } 0299 0300 void KDirWatch_UnitTest::watchAndModifyOneFile() // watch a specific file, and modify it 0301 { 0302 KDirWatch watch; 0303 const QString existingFile = m_path + QLatin1String("ExistingFile"); 0304 watch.addFile(existingFile); 0305 watch.startScan(); 0306 if (m_slow) { 0307 waitUntilNewSecond(); 0308 } 0309 appendToFile(existingFile); 0310 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile)); 0311 } 0312 0313 void KDirWatch_UnitTest::removeAndReAdd() 0314 { 0315 KDirWatch watch; 0316 watch.addDir(m_path); 0317 // This triggers bug #374075. 0318 watch.addDir(QStringLiteral(":/kio5/newfile-templates")); 0319 watch.startScan(); 0320 if (watch.internalMethod() != KDirWatch::INotify) { 0321 waitUntilNewSecond(); // necessary for mtime checks in scanEntry 0322 } 0323 createFile(0); 0324 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); 0325 0326 // Just like KDirLister does: remove the watch, then re-add it. 0327 watch.removeDir(m_path); 0328 watch.addDir(m_path); 0329 if (watch.internalMethod() != KDirWatch::INotify) { 0330 waitUntilMTimeChange(m_path); // necessary for QFSWatcher 0331 } 0332 createFile(1); 0333 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); 0334 } 0335 0336 void KDirWatch_UnitTest::watchNonExistent() 0337 { 0338 KDirWatch watch; 0339 // Watch "subdir", that doesn't exist yet 0340 const QString subdir = m_path + QLatin1String("subdir"); 0341 QVERIFY(!QFile::exists(subdir)); 0342 watch.addDir(subdir); 0343 watch.startScan(); 0344 0345 if (m_slow) { 0346 waitUntilNewSecond(); 0347 } 0348 0349 // Now create it, KDirWatch should emit created() 0350 qCDebug(KCOREADDONS_DEBUG) << "Creating" << subdir; 0351 QDir().mkdir(subdir); 0352 0353 QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), subdir)); 0354 0355 qCDebug(KCOREADDONS_DEBUG) << &watch; 0356 0357 // Play with addDir/removeDir, just for fun 0358 watch.addDir(subdir); 0359 watch.removeDir(subdir); 0360 watch.addDir(subdir); 0361 0362 // Now watch files that don't exist yet 0363 const QString file = subdir + QLatin1String("/0"); 0364 watch.addFile(file); // doesn't exist yet 0365 const QString file1 = subdir + QLatin1String("/1"); 0366 watch.addFile(file1); // doesn't exist yet 0367 watch.removeFile(file1); // forget it again 0368 0369 qCDebug(KCOREADDONS_DEBUG) << &watch; 0370 0371 QVERIFY(!QFile::exists(file)); 0372 // Now create it, KDirWatch should emit created 0373 qCDebug(KCOREADDONS_DEBUG) << "Creating" << file; 0374 createFile(file); 0375 QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), file)); 0376 0377 appendToFile(file); 0378 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file)); 0379 0380 // Create the file after all; we're not watching for it, but the dir will emit dirty 0381 createFile(file1); 0382 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), subdir)); 0383 } 0384 0385 void KDirWatch_UnitTest::watchNonExistentWithSingleton() 0386 { 0387 const QString file = QLatin1String("/root/.ssh/authorized_keys"); 0388 KDirWatch::self()->addFile(file); 0389 // When running this test in KDIRWATCH_METHOD=QFSWatch, or when we fallback to qfswatch 0390 // when inotify fails above, we end up creating the fsWatch 0391 // in the kdirwatch singleton. Bug 261541 discovered that Qt hanged when deleting fsWatch 0392 // once QCoreApp was gone, this is what this test is about. 0393 } 0394 0395 void KDirWatch_UnitTest::testDelete() 0396 { 0397 const QString file1 = m_path + QLatin1String("del"); 0398 if (!QFile::exists(file1)) { 0399 createFile(file1); 0400 } 0401 waitUntilMTimeChange(file1); 0402 0403 // Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed) 0404 KDirWatch watch; 0405 watch.addFile(file1); 0406 0407 qCDebug(KCOREADDONS_DEBUG) << &watch; 0408 0409 QSignalSpy spyDirty(&watch, &KDirWatch::dirty); 0410 QFile::remove(file1); 0411 QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), file1)); 0412 QTest::qWait(40); // just in case delayed processing would emit it 0413 QCOMPARE(spyDirty.count(), 0); 0414 } 0415 0416 void KDirWatch_UnitTest::testDeleteAndRecreateFile() // Useful for /etc/localtime for instance 0417 { 0418 const QString subdir = m_path + QLatin1String("subdir"); 0419 QDir().mkdir(subdir); 0420 const QString file1 = subdir + QLatin1String("/1"); 0421 if (!QFile::exists(file1)) { 0422 createFile(file1); 0423 } 0424 waitUntilMTimeChange(file1); 0425 0426 // Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed) 0427 KDirWatch watch; 0428 watch.addFile(file1); 0429 0430 // Make sure this even works multiple times, as needed for ksycoca 0431 for (int i = 0; i < 5; ++i) { 0432 if (m_slow || watch.internalMethod() == KDirWatch::QFSWatch) { 0433 waitUntilNewSecond(); 0434 } 0435 0436 qCDebug(KCOREADDONS_DEBUG) << "Attempt #" << (i + 1) << "removing+recreating" << file1; 0437 0438 // When watching for a deleted + created signal pair, the two might come so close that 0439 // using waitForOneSignal will miss the created signal. This function monitors both all 0440 // the time to ensure both are received. 0441 // 0442 // In addition, allow dirty() to be emitted (for that same path) as an alternative 0443 0444 const QString expectedPath = file1; 0445 QSignalSpy spyDirty(&watch, &KDirWatch::dirty); 0446 QSignalSpy spyDeleted(&watch, &KDirWatch::deleted); 0447 QSignalSpy spyCreated(&watch, &KDirWatch::created); 0448 0449 // WHEN 0450 QFile::remove(file1); 0451 // And recreate immediately, to try and fool KDirWatch with unchanged ctime/mtime ;) 0452 // (This emulates the /etc/localtime case) 0453 createFile(file1); 0454 0455 // THEN 0456 int numTries = 0; 0457 while (spyDeleted.isEmpty() && spyDirty.isEmpty()) { 0458 if (++numTries > s_maxTries) { 0459 QFAIL("Failed to detect file deletion and recreation through either a deleted/created signal pair or through a dirty signal!"); 0460 return; 0461 } 0462 spyDeleted.wait(50); 0463 while (!spyDirty.isEmpty()) { 0464 if (spyDirty.at(0).at(0).toString() != expectedPath) { // unrelated 0465 spyDirty.removeFirst(); 0466 } else { 0467 break; 0468 } 0469 } 0470 } 0471 if (!spyDirty.isEmpty()) { 0472 continue; // all ok 0473 } 0474 0475 // Don't bother waiting for the created signal if the signal spy already received a signal. 0476 if (spyCreated.isEmpty() && !spyCreated.wait(50 * s_maxTries)) { 0477 qWarning() << "Timeout waiting for KDirWatch signal created(QString) (" << expectedPath << ")"; 0478 QFAIL("Timeout waiting for KDirWatch signal created, after deleted was emitted"); 0479 return; 0480 } 0481 0482 QVERIFY(verifySignalPath(spyDeleted, "deleted(QString)", expectedPath) && verifySignalPath(spyCreated, "created(QString)", expectedPath)); 0483 } 0484 0485 waitUntilMTimeChange(file1); 0486 0487 appendToFile(file1); 0488 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1)); 0489 } 0490 0491 void KDirWatch_UnitTest::testDeleteAndRecreateDir() 0492 { 0493 // Like KDirModelTest::testOverwriteFileWithDir does at the end. 0494 // The linux-2.6.31 bug made kdirwatch emit deletion signals about the -new- dir! 0495 QTemporaryDir *tempDir1 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("olddir-")); 0496 KDirWatch watch; 0497 const QString path1 = tempDir1->path() + QLatin1Char('/'); 0498 watch.addDir(path1); 0499 0500 delete tempDir1; 0501 QTemporaryDir *tempDir2 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("newdir-")); 0502 const QString path2 = tempDir2->path() + QLatin1Char('/'); 0503 watch.addDir(path2); 0504 0505 QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), path1)); 0506 0507 delete tempDir2; 0508 } 0509 0510 void KDirWatch_UnitTest::testMoveTo() 0511 { 0512 // This reproduces the famous digikam crash, #222974 0513 // A watched file was being rewritten (overwritten by ksavefile), 0514 // which gives inotify notifications "moved_to" followed by "delete_self" 0515 // 0516 // What happened then was that the delayed slotRescan 0517 // would adjust things, making it status==Normal but the entry was 0518 // listed as a "non-existent sub-entry" for the parent directory. 0519 // That's inconsistent, and after removeFile() a dangling sub-entry would be left. 0520 0521 // Initial data: creating file subdir/1 0522 const QString file1 = m_path + QLatin1String("moveTo"); 0523 createFile(file1); 0524 0525 KDirWatch watch; 0526 watch.addDir(m_path); 0527 watch.addFile(file1); 0528 watch.startScan(); 0529 0530 if (watch.internalMethod() != KDirWatch::INotify) { 0531 waitUntilMTimeChange(m_path); 0532 } 0533 0534 // Atomic rename of "temp" to "file1", much like KAutoSave would do when saving file1 again 0535 // ### TODO: this isn't an atomic rename anymore. We need ::rename for that, or API from Qt. 0536 const QString filetemp = m_path + QLatin1String("temp"); 0537 createFile(filetemp); 0538 QFile::remove(file1); 0539 QVERIFY(QFile::rename(filetemp, file1)); // overwrite file1 with the tempfile 0540 qCDebug(KCOREADDONS_DEBUG) << "Overwrite file1 with tempfile"; 0541 0542 QSignalSpy spyCreated(&watch, &KDirWatch::created); 0543 QSignalSpy spyDirty(&watch, &KDirWatch::dirty); 0544 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); 0545 0546 // Getting created() on an unwatched file is an inotify bonus, it's not part of the requirements. 0547 if (watch.internalMethod() == KDirWatch::INotify) { 0548 QCOMPARE(spyCreated.count(), 1); 0549 QCOMPARE(spyCreated[0][0].toString(), file1); 0550 0551 QCOMPARE(spyDirty.size(), 2); 0552 QCOMPARE(spyDirty[1][0].toString(), filetemp); 0553 } 0554 0555 // make sure we're still watching it 0556 appendToFile(file1); 0557 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1)); 0558 0559 watch.removeFile(file1); // now we remove it 0560 0561 // Just touch another file to trigger a findSubEntry - this where the crash happened 0562 waitUntilMTimeChange(m_path); 0563 createFile(filetemp); 0564 #ifdef Q_OS_WIN 0565 if (watch.internalMethod() == KDirWatch::QFSWatch) { 0566 QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue); 0567 } 0568 #endif 0569 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); 0570 } 0571 0572 void KDirWatch_UnitTest::nestedEventLoop() // #220153: watch two files, and modify 2nd while in slot for 1st 0573 { 0574 KDirWatch watch; 0575 0576 const QString file0 = m_path + QLatin1String("nested_0"); 0577 watch.addFile(file0); 0578 const QString file1 = m_path + QLatin1String("nested_1"); 0579 watch.addFile(file1); 0580 watch.startScan(); 0581 0582 if (m_slow) { 0583 waitUntilNewSecond(); 0584 } 0585 0586 appendToFile(file0); 0587 0588 // use own spy, to connect it before nestedEventLoopSlot, otherwise it reverses order 0589 QSignalSpy spyDirty(&watch, &KDirWatch::dirty); 0590 connect(&watch, &KDirWatch::dirty, this, &KDirWatch_UnitTest::nestedEventLoopSlot); 0591 waitForDirtySignal(watch, 1); 0592 QVERIFY(spyDirty.count() >= 2); 0593 QCOMPARE(spyDirty[0][0].toString(), file0); 0594 QCOMPARE(spyDirty[spyDirty.count() - 1][0].toString(), file1); 0595 } 0596 0597 void KDirWatch_UnitTest::nestedEventLoopSlot() 0598 { 0599 const KDirWatch *const_watch = qobject_cast<const KDirWatch *>(sender()); 0600 KDirWatch *watch = const_cast<KDirWatch *>(const_watch); 0601 // let's not come in this slot again 0602 disconnect(watch, &KDirWatch::dirty, this, &KDirWatch_UnitTest::nestedEventLoopSlot); 0603 0604 const QString file1 = m_path + QLatin1String("nested_1"); 0605 appendToFile(file1); 0606 // The nested event processing here was from a messagebox in #220153 0607 QList<QVariantList> spy = waitForDirtySignal(*watch, 1); 0608 QVERIFY(spy.count() >= 1); 0609 QCOMPARE(spy[spy.count() - 1][0].toString(), file1); 0610 0611 // Now the user pressed reload... 0612 const QString file0 = m_path + QLatin1String("nested_0"); 0613 watch->removeFile(file0); 0614 watch->addFile(file0); 0615 } 0616 0617 void KDirWatch_UnitTest::testHardlinkChange() 0618 { 0619 #ifdef Q_OS_UNIX 0620 0621 // The unittest for the "detecting hardlink change to /etc/localtime" problem 0622 // described on kde-core-devel (2009-07-03). 0623 // It shows that watching a specific file doesn't inform us that the file is 0624 // being recreated. Better watch the directory, for that. 0625 // Well, it works with inotify 0626 0627 const QString existingFile = m_path + QLatin1String("ExistingFile"); 0628 KDirWatch watch; 0629 watch.addFile(existingFile); 0630 watch.startScan(); 0631 0632 QFile::remove(existingFile); 0633 const QString testFile = m_path + QLatin1String("TestFile"); 0634 QVERIFY(::link(QFile::encodeName(testFile).constData(), QFile::encodeName(existingFile).constData()) == 0); // make ExistingFile "point" to TestFile 0635 QVERIFY(QFile::exists(existingFile)); 0636 0637 QVERIFY(waitForRecreationSignal(watch, existingFile)); 0638 0639 // The mtime of the existing file is the one of "TestFile", so it's old. 0640 // We won't detect the change then, if we use that as baseline for waiting. 0641 // We really need msec granularity, but that requires using statx which isn't available everywhere... 0642 waitUntilNewSecond(); 0643 appendToFile(existingFile); 0644 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile)); 0645 #else 0646 QSKIP("Unix-specific"); 0647 #endif 0648 } 0649 0650 void KDirWatch_UnitTest::stopAndRestart() 0651 { 0652 KDirWatch watch; 0653 watch.addDir(m_path); 0654 watch.startScan(); 0655 0656 waitUntilMTimeChange(m_path); 0657 0658 watch.stopDirScan(m_path); 0659 0660 qCDebug(KCOREADDONS_DEBUG) << "create file 2 at" << QDateTime::currentDateTime().toMSecsSinceEpoch(); 0661 const QString file2 = createFile(2); 0662 QSignalSpy spyDirty(&watch, &KDirWatch::dirty); 0663 QTest::qWait(200); 0664 QCOMPARE(spyDirty.count(), 0); // suspended -> no signal 0665 0666 watch.restartDirScan(m_path); 0667 0668 QTest::qWait(200); 0669 0670 #ifndef Q_OS_WIN 0671 QCOMPARE(spyDirty.count(), 0); // as documented by restartDirScan: no signal 0672 // On Windows, however, signals will get emitted, due to the ifdef Q_OS_WIN in the timestamp 0673 // comparison ("trust QFSW since the mtime of dirs isn't modified") 0674 #endif 0675 0676 qCDebug(KCOREADDONS_DEBUG) << &watch; 0677 0678 waitUntilMTimeChange(m_path); // necessary for the mtime comparison in scanEntry 0679 0680 qCDebug(KCOREADDONS_DEBUG) << "create file 3 at" << QDateTime::currentDateTime().toMSecsSinceEpoch(); 0681 const QString file3 = createFile(3); 0682 #ifdef Q_OS_WIN 0683 if (watch.internalMethod() == KDirWatch::QFSWatch) { 0684 QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue); 0685 } 0686 #endif 0687 QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path)); 0688 0689 QFile::remove(file2); 0690 QFile::remove(file3); 0691 } 0692 0693 void KDirWatch_UnitTest::testRefcounting() 0694 { 0695 #if QT_CONFIG(cxx11_future) 0696 bool initialExists = false; 0697 bool secondExists = true; // the expectation is it will be set false 0698 auto thread = QThread::create([&] { 0699 QTemporaryDir dir; 0700 { 0701 KDirWatch watch; 0702 watch.addFile(dir.path()); 0703 initialExists = KDirWatch::exists(); 0704 } // out of scope, the internal private should have been unset 0705 secondExists = KDirWatch::exists(); 0706 }); 0707 thread->start(); 0708 thread->wait(); 0709 delete thread; 0710 QVERIFY(initialExists); 0711 QVERIFY(!secondExists); 0712 #endif 0713 } 0714 0715 void KDirWatch_UnitTest::testRelativeRefcounting() 0716 { 0717 // Relative files aren't supported but should still result in correct ref 0718 // handling. Specifically when watch1 gets destroyed it should take all 0719 // its entries with it regardless of whether the entry was well-formed 0720 KDirWatch watch0; 0721 0722 if (watch0.internalMethod() != KDirWatch::INotify) { 0723 // Only test on inotify. Otherwise Entry count expectations may diverge. 0724 return; 0725 } 0726 0727 const auto initialSize = watch0.d->m_mapEntries.size(); 0728 { 0729 KDirWatch watch1; 0730 watch1.addFile(QStringLiteral("AVeryRelativePath.txt")); 0731 // NOTE: addFile actually adds two entires: one for '.' to watch for the appearance of the file and one for the file. 0732 QCOMPARE(watch0.d->m_mapEntries.size(), initialSize + 2); 0733 } 0734 // NOTE: we leak the directory entry from above but it has no clients so it's mostly harmless 0735 QCOMPARE(watch0.d->m_mapEntries.size(), initialSize + 1); 0736 } 0737 0738 void KDirWatch_UnitTest::testMoveToThread() 0739 { 0740 QTemporaryDir dir; 0741 { 0742 const QRegularExpression expression(QStringLiteral("KDirwatch is moving its thread. This is not supported at this time;.+")); 0743 QTest::ignoreMessage(QtCriticalMsg, expression); 0744 0745 auto watch = new KDirWatch; 0746 watch->addDir(dir.path()); 0747 0748 auto thread = new QThread; 0749 watch->moveToThread(thread); 0750 thread->start(); 0751 0752 waitUntilMTimeChange(dir.path()); 0753 0754 QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater); 0755 QObject::connect(thread, &QThread::finished, watch, &QObject::deleteLater); 0756 0757 thread->quit(); 0758 thread->wait(); 0759 } 0760 // trigger an event on the now deleted watch. This should not crash! 0761 const QString file = dir.path() + QLatin1String("/bar"); 0762 createFile(file); 0763 waitUntilMTimeChange(file); 0764 } 0765 0766 #include "kdirwatch_unittest.moc"