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