File indexing completed on 2023-09-24 07:59:53

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