File indexing completed on 2024-04-14 14:19:07

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"