File indexing completed on 2024-04-14 03:51:30

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"