1 /*
2     This file is part of the KDE libraries
3 
4     SPDX-FileCopyrightText: 2009 David Faure <faure@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include <kdirwatch.h>
10 
11 #include <QDebug>
12 #include <QDir>
13 #include <QFileInfo>
14 #include <QSignalSpy>
15 #include <QTemporaryDir>
16 #include <QTest>
17 #include <QThread>
18 #include <sys/stat.h>
19 #ifdef Q_OS_UNIX
20 #include <unistd.h> // ::link()
21 #endif
22 
23 #include "config-tests.h"
24 #include "kcoreaddons_debug.h"
25 
26 // Debugging notes: to see which inotify signals are emitted, either set s_verboseDebug=true
27 // at the top of kdirwatch.cpp, or use the command-line tool "inotifywait -m /path"
28 
29 // Note that kdirlistertest and kdirmodeltest also exercise KDirWatch quite a lot.
30 
methodToString(KDirWatch::Method method)31 static const char *methodToString(KDirWatch::Method method)
32 {
33     switch (method) {
34     case KDirWatch::FAM:
35         return "Fam";
36     case KDirWatch::INotify:
37         return "INotify";
38     case KDirWatch::Stat:
39         return "Stat";
40     case KDirWatch::QFSWatch:
41         return "QFSWatch";
42     }
43     return "ERROR!";
44 }
45 
46 class StaticObject
47 {
48 public:
49     KDirWatch m_dirWatch;
50 };
51 Q_GLOBAL_STATIC(StaticObject, s_staticObject)
52 
53 class StaticObjectUsingSelf // like KSambaShare does, bug 353080
54 {
55 public:
StaticObjectUsingSelf()56     StaticObjectUsingSelf()
57     {
58         KDirWatch::self();
59     }
~StaticObjectUsingSelf()60     ~StaticObjectUsingSelf()
61     {
62         if (KDirWatch::exists() && KDirWatch::self()->contains(QDir::homePath())) {
63             KDirWatch::self()->removeDir(QDir::homePath());
64         }
65     }
66 };
67 Q_GLOBAL_STATIC(StaticObjectUsingSelf, s_staticObjectUsingSelf)
68 
69 class KDirWatch_UnitTest : public QObject
70 {
71     Q_OBJECT
72 public:
KDirWatch_UnitTest()73     KDirWatch_UnitTest()
74     {
75         // Speed up the test by making the kdirwatch timer (to compress changes) faster
76         qputenv("KDIRWATCH_POLLINTERVAL", "50");
77         qputenv("KDIRWATCH_METHOD", KDIRWATCH_TEST_METHOD);
78         s_staticObjectUsingSelf();
79 
80         m_path = m_tempDir.path() + QLatin1Char('/');
81         KDirWatch *dirW = &s_staticObject()->m_dirWatch;
82         m_stat = dirW->internalMethod() == KDirWatch::Stat;
83         m_slow = (dirW->internalMethod() == KDirWatch::FAM || m_stat);
84         qCDebug(KCOREADDONS_DEBUG) << "Using method" << methodToString(dirW->internalMethod());
85     }
86 
87 private Q_SLOTS: // test methods
initTestCase()88     void initTestCase()
89     {
90         QFileInfo pathInfo(m_path);
91         QVERIFY(pathInfo.isDir() && pathInfo.isWritable());
92 
93         // By creating the files upfront, we save waiting a full second for an mtime change
94         createFile(m_path + QLatin1String("ExistingFile"));
95         createFile(m_path + QLatin1String("TestFile"));
96         createFile(m_path + QLatin1String("nested_0"));
97         createFile(m_path + QLatin1String("nested_1"));
98 
99         s_staticObject()->m_dirWatch.addFile(m_path + QLatin1String("ExistingFile"));
100     }
101     void touchOneFile();
102     void touch1000Files();
103     void watchAndModifyOneFile();
104     void removeAndReAdd();
105     void watchNonExistent();
106     void watchNonExistentWithSingleton();
107     void testDelete();
108     void testDeleteAndRecreateFile();
109     void testDeleteAndRecreateDir();
110     void testMoveTo();
111     void nestedEventLoop();
112     void testHardlinkChange();
113     void stopAndRestart();
114     void benchCreateTree();
115     void benchCreateWatcher();
116     void benchNotifyWatcher();
117     void testRefcounting();
118 
119 protected Q_SLOTS: // internal slots
120     void nestedEventLoopSlot();
121 
122 private:
123     void waitUntilMTimeChange(const QString &path);
124     void waitUntilNewSecond();
125     void waitUntilAfter(const QDateTime &ctime);
126     QList<QVariantList> waitForDirtySignal(KDirWatch &watch, int expected);
127     QList<QVariantList> waitForDeletedSignal(KDirWatch &watch, int expected);
128     bool waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path);
129     bool waitForRecreationSignal(KDirWatch &watch, const QString &path);
130     bool verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath);
131     void createFile(const QString &path);
132     QString createFile(int num);
133     void removeFile(int num);
134     void appendToFile(const QString &path);
135     void appendToFile(int num);
136     int createDirectoryTree(const QString &path, int depth = 4);
137 
138     QTemporaryDir m_tempDir;
139     QString m_path;
140     bool m_slow;
141     bool m_stat;
142 };
143 
144 QTEST_MAIN(KDirWatch_UnitTest)
145 
146 // Just to make the inotify packets bigger
147 static const char s_filePrefix[] = "This_is_a_test_file_";
148 
149 static const int s_maxTries = 50;
150 
151 // helper method: create a file
createFile(const QString & path)152 void KDirWatch_UnitTest::createFile(const QString &path)
153 {
154     QFile file(path);
155     QVERIFY(file.open(QIODevice::WriteOnly));
156 #ifdef Q_OS_FREEBSD
157     // FreeBSD has inotify implemented as user-space library over native kevent API.
158     // When using it, one has to open() a file to start watching it, so workaround
159     // test breakage by giving inotify time to react to file creation.
160     // Full context: https://github.com/libinotify-kqueue/libinotify-kqueue/issues/10
161     if (!m_slow)
162         QThread::msleep(1);
163 #endif
164     file.write(QByteArray("foo"));
165     file.close();
166 }
167 
168 // helper method: create a file (identified by number)
createFile(int num)169 QString KDirWatch_UnitTest::createFile(int num)
170 {
171     const QString fileName = QLatin1String(s_filePrefix) + QString::number(num);
172     createFile(m_path + fileName);
173     return m_path + fileName;
174 }
175 
176 // helper method: delete a file (identified by number)
removeFile(int num)177 void KDirWatch_UnitTest::removeFile(int num)
178 {
179     const QString fileName = QLatin1String(s_filePrefix) + QString::number(num);
180     QFile::remove(m_path + fileName);
181 }
182 
createDirectoryTree(const QString & basePath,int depth)183 int KDirWatch_UnitTest::createDirectoryTree(const QString &basePath, int depth)
184 {
185     int filesCreated = 0;
186 
187     const int numFiles = 10;
188     for (int i = 0; i < numFiles; ++i) {
189         createFile(basePath + QLatin1Char('/') + QLatin1String(s_filePrefix) + QString::number(i));
190         ++filesCreated;
191     }
192 
193     if (depth <= 0) {
194         return filesCreated;
195     }
196 
197     const int numFolders = 5;
198     for (int i = 0; i < numFolders; ++i) {
199         const QString childPath = basePath + QLatin1String("/subdir") + QString::number(i);
200         QDir().mkdir(childPath);
201         filesCreated += createDirectoryTree(childPath, depth - 1);
202     }
203 
204     return filesCreated;
205 }
206 
waitUntilMTimeChange(const QString & path)207 void KDirWatch_UnitTest::waitUntilMTimeChange(const QString &path)
208 {
209     // Wait until the current second is more than the file's mtime
210     // otherwise this change will go unnoticed
211 
212     QFileInfo fi(path);
213     QVERIFY(fi.exists());
214     const QDateTime ctime = fi.lastModified();
215     waitUntilAfter(ctime);
216 }
217 
waitUntilNewSecond()218 void KDirWatch_UnitTest::waitUntilNewSecond()
219 {
220     QDateTime now = QDateTime::currentDateTime();
221     waitUntilAfter(now);
222 }
223 
waitUntilAfter(const QDateTime & ctime)224 void KDirWatch_UnitTest::waitUntilAfter(const QDateTime &ctime)
225 {
226     int totalWait = 0;
227     QDateTime now;
228     Q_FOREVER {
229         now = QDateTime::currentDateTime();
230         if (now.toMSecsSinceEpoch() / 1000 == ctime.toMSecsSinceEpoch() / 1000) // truncate milliseconds
231         {
232             totalWait += 50;
233             QTest::qWait(50);
234         } else {
235             QVERIFY(now > ctime); // can't go back in time ;)
236             QTest::qWait(50); // be safe
237             break;
238         }
239     }
240     // if (totalWait > 0)
241     qCDebug(KCOREADDONS_DEBUG) << "Waited" << totalWait << "ms so that now" << now.toString(Qt::ISODate) << "is >" << ctime.toString(Qt::ISODate);
242 }
243 
244 // helper method: modifies a file
appendToFile(const QString & path)245 void KDirWatch_UnitTest::appendToFile(const QString &path)
246 {
247     QVERIFY(QFile::exists(path));
248     waitUntilMTimeChange(path);
249 
250     QFile file(path);
251     QVERIFY(file.open(QIODevice::Append | QIODevice::WriteOnly));
252     file.write(QByteArray("foobar"));
253     file.close();
254 }
255 
256 // helper method: modifies a file (identified by number)
appendToFile(int num)257 void KDirWatch_UnitTest::appendToFile(int num)
258 {
259     const QString fileName = QLatin1String(s_filePrefix) + QString::number(num);
260     appendToFile(m_path + fileName);
261 }
262 
removeTrailingSlash(const QString & path)263 static QString removeTrailingSlash(const QString &path)
264 {
265     if (path.endsWith(QLatin1Char('/'))) {
266         return path.left(path.length() - 1);
267     } else {
268         return path;
269     }
270 }
271 
272 // helper method
waitForDirtySignal(KDirWatch & watch,int expected)273 QList<QVariantList> KDirWatch_UnitTest::waitForDirtySignal(KDirWatch &watch, int expected)
274 {
275     QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
276     int numTries = 0;
277     // Give time for KDirWatch to notify us
278     while (spyDirty.count() < expected) {
279         if (++numTries > s_maxTries) {
280             qWarning() << "Timeout waiting for KDirWatch. Got" << spyDirty.count() << "dirty() signals, expected" << expected;
281             return std::move(spyDirty);
282         }
283         spyDirty.wait(50);
284     }
285     return std::move(spyDirty);
286 }
287 
waitForOneSignal(KDirWatch & watch,const char * sig,const QString & path)288 bool KDirWatch_UnitTest::waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path)
289 {
290     const QString expectedPath = removeTrailingSlash(path);
291     while (true) {
292         QSignalSpy spyDirty(&watch, sig);
293         int numTries = 0;
294         // Give time for KDirWatch to notify us
295         while (spyDirty.isEmpty()) {
296             if (++numTries > s_maxTries) {
297                 qWarning() << "Timeout waiting for KDirWatch signal" << QByteArray(sig).mid(1) << "(" << path << ")";
298                 return false;
299             }
300             spyDirty.wait(50);
301         }
302         return verifySignalPath(spyDirty, sig, expectedPath);
303     }
304 }
305 
verifySignalPath(QSignalSpy & spy,const char * sig,const QString & expectedPath)306 bool KDirWatch_UnitTest::verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath)
307 {
308     for (int i = 0; i < spy.count(); ++i) {
309         const QString got = spy[i][0].toString();
310         if (got == expectedPath) {
311             return true;
312         }
313         if (got.startsWith(expectedPath + QLatin1Char('/'))) {
314             qCDebug(KCOREADDONS_DEBUG) << "Ignoring (inotify) notification of" << (sig + 1) << '(' << got << ')';
315             continue;
316         }
317         qWarning() << "Expected" << sig << '(' << expectedPath << ')' << "but got" << sig << '(' << got << ')';
318         return false;
319     }
320     return false;
321 }
322 
waitForRecreationSignal(KDirWatch & watch,const QString & path)323 bool KDirWatch_UnitTest::waitForRecreationSignal(KDirWatch &watch, const QString &path)
324 {
325     // When watching for a deleted + created signal pair, the two might come so close that
326     // using waitForOneSignal will miss the created signal.  This function monitors both all
327     // the time to ensure both are received.
328     //
329     // In addition, it allows dirty() to be emitted (for that same path) as an alternative
330 
331     const QString expectedPath = removeTrailingSlash(path);
332     QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
333     QSignalSpy spyDeleted(&watch, &KDirWatch::deleted);
334     QSignalSpy spyCreated(&watch, &KDirWatch::created);
335 
336     int numTries = 0;
337     while (spyDeleted.isEmpty() && spyDirty.isEmpty()) {
338         if (++numTries > s_maxTries) {
339             return false;
340         }
341         spyDeleted.wait(50);
342         while (!spyDirty.isEmpty()) {
343             if (spyDirty.at(0).at(0).toString() != expectedPath) { // unrelated
344                 spyDirty.removeFirst();
345             }
346         }
347     }
348     if (!spyDirty.isEmpty()) {
349         return true;
350     }
351 
352     // Don't bother waiting for the created signal if the signal spy already received a signal.
353     if (spyCreated.isEmpty() && !spyCreated.wait(50 * s_maxTries)) {
354         qWarning() << "Timeout waiting for KDirWatch signal created(QString) (" << path << ")";
355         return false;
356     }
357 
358     return verifySignalPath(spyDeleted, "deleted(QString)", expectedPath) && verifySignalPath(spyCreated, "created(QString)", expectedPath);
359 }
360 
waitForDeletedSignal(KDirWatch & watch,int expected)361 QList<QVariantList> KDirWatch_UnitTest::waitForDeletedSignal(KDirWatch &watch, int expected)
362 {
363     QSignalSpy spyDeleted(&watch, &KDirWatch::created);
364     int numTries = 0;
365     // Give time for KDirWatch to notify us
366     while (spyDeleted.count() < expected) {
367         if (++numTries > s_maxTries) {
368             qWarning() << "Timeout waiting for KDirWatch. Got" << spyDeleted.count() << "deleted() signals, expected" << expected;
369             return std::move(spyDeleted);
370         }
371         spyDeleted.wait(50);
372     }
373     return std::move(spyDeleted);
374 }
375 
touchOneFile()376 void KDirWatch_UnitTest::touchOneFile() // watch a dir, create a file in it
377 {
378     KDirWatch watch;
379     watch.addDir(m_path);
380     watch.startScan();
381 
382     waitUntilMTimeChange(m_path);
383 
384     // dirty(the directory) should be emitted.
385     QSignalSpy spyCreated(&watch, &KDirWatch::created);
386     createFile(0);
387     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
388     QCOMPARE(spyCreated.count(), 0); // "This is not emitted when creating a file is created in a watched directory."
389 
390     removeFile(0);
391 }
392 
touch1000Files()393 void KDirWatch_UnitTest::touch1000Files()
394 {
395     KDirWatch watch;
396     watch.addDir(m_path);
397     watch.startScan();
398 
399     waitUntilMTimeChange(m_path);
400 
401     const int fileCount = 100;
402     for (int i = 0; i < fileCount; ++i) {
403         createFile(i);
404     }
405 
406     QList<QVariantList> spy = waitForDirtySignal(watch, fileCount);
407     if (watch.internalMethod() == KDirWatch::INotify) {
408         QVERIFY(spy.count() >= fileCount);
409     } else {
410         // More stupid backends just see one mtime change on the directory
411         QVERIFY(spy.count() >= 1);
412     }
413 
414     for (int i = 0; i < fileCount; ++i) {
415         removeFile(i);
416     }
417 }
418 
watchAndModifyOneFile()419 void KDirWatch_UnitTest::watchAndModifyOneFile() // watch a specific file, and modify it
420 {
421     KDirWatch watch;
422     const QString existingFile = m_path + QLatin1String("ExistingFile");
423     watch.addFile(existingFile);
424     watch.startScan();
425     if (m_slow) {
426         waitUntilNewSecond();
427     }
428     appendToFile(existingFile);
429     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile));
430 }
431 
removeAndReAdd()432 void KDirWatch_UnitTest::removeAndReAdd()
433 {
434     KDirWatch watch;
435     watch.addDir(m_path);
436     // This triggers bug #374075.
437     watch.addDir(QStringLiteral(":/kio5/newfile-templates"));
438     watch.startScan();
439     if (watch.internalMethod() != KDirWatch::INotify) {
440         waitUntilNewSecond(); // necessary for mtime checks in scanEntry
441     }
442     createFile(0);
443     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
444 
445     // Just like KDirLister does: remove the watch, then re-add it.
446     watch.removeDir(m_path);
447     watch.addDir(m_path);
448     if (watch.internalMethod() != KDirWatch::INotify) {
449         waitUntilMTimeChange(m_path); // necessary for FAM and QFSWatcher
450     }
451     createFile(1);
452     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
453 }
454 
watchNonExistent()455 void KDirWatch_UnitTest::watchNonExistent()
456 {
457     KDirWatch watch;
458     // Watch "subdir", that doesn't exist yet
459     const QString subdir = m_path + QLatin1String("subdir");
460     QVERIFY(!QFile::exists(subdir));
461     watch.addDir(subdir);
462     watch.startScan();
463 
464     if (m_slow) {
465         waitUntilNewSecond();
466     }
467 
468     // Now create it, KDirWatch should emit created()
469     qCDebug(KCOREADDONS_DEBUG) << "Creating" << subdir;
470     QDir().mkdir(subdir);
471 
472     QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), subdir));
473 
474     KDirWatch::statistics();
475 
476     // Play with addDir/removeDir, just for fun
477     watch.addDir(subdir);
478     watch.removeDir(subdir);
479     watch.addDir(subdir);
480 
481     // Now watch files that don't exist yet
482     const QString file = subdir + QLatin1String("/0");
483     watch.addFile(file); // doesn't exist yet
484     const QString file1 = subdir + QLatin1String("/1");
485     watch.addFile(file1); // doesn't exist yet
486     watch.removeFile(file1); // forget it again
487 
488     KDirWatch::statistics();
489 
490     QVERIFY(!QFile::exists(file));
491     // Now create it, KDirWatch should emit created
492     qCDebug(KCOREADDONS_DEBUG) << "Creating" << file;
493     createFile(file);
494     QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), file));
495 
496     appendToFile(file);
497     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file));
498 
499     // Create the file after all; we're not watching for it, but the dir will emit dirty
500     createFile(file1);
501     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), subdir));
502 }
503 
watchNonExistentWithSingleton()504 void KDirWatch_UnitTest::watchNonExistentWithSingleton()
505 {
506     const QString file = QLatin1String("/root/.ssh/authorized_keys");
507     KDirWatch::self()->addFile(file);
508     // When running this test in KDIRWATCH_METHOD=QFSWatch, or when FAM is not available
509     // and we fallback to qfswatch when inotify fails above, we end up creating the fsWatch
510     // in the kdirwatch singleton. Bug 261541 discovered that Qt hanged when deleting fsWatch
511     // once QCoreApp was gone, this is what this test is about.
512 }
513 
testDelete()514 void KDirWatch_UnitTest::testDelete()
515 {
516     const QString file1 = m_path + QLatin1String("del");
517     if (!QFile::exists(file1)) {
518         createFile(file1);
519     }
520     waitUntilMTimeChange(file1);
521 
522     // Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed)
523     KDirWatch watch;
524     watch.addFile(file1);
525 
526     KDirWatch::statistics();
527 
528     QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
529     QFile::remove(file1);
530     QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), file1));
531     QTest::qWait(40); // just in case delayed processing would emit it
532     QCOMPARE(spyDirty.count(), 0);
533 }
534 
testDeleteAndRecreateFile()535 void KDirWatch_UnitTest::testDeleteAndRecreateFile() // Useful for /etc/localtime for instance
536 {
537     const QString subdir = m_path + QLatin1String("subdir");
538     QDir().mkdir(subdir);
539     const QString file1 = subdir + QLatin1String("/1");
540     if (!QFile::exists(file1)) {
541         createFile(file1);
542     }
543     waitUntilMTimeChange(file1);
544 
545     // Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed)
546     KDirWatch watch;
547     watch.addFile(file1);
548 
549     // Make sure this even works multiple times, as needed for ksycoca
550     for (int i = 0; i < 5; ++i) {
551         if (m_slow || watch.internalMethod() == KDirWatch::QFSWatch) {
552             waitUntilNewSecond();
553         }
554 
555         qCDebug(KCOREADDONS_DEBUG) << "Attempt #" << (i + 1) << "removing+recreating" << file1;
556 
557         // When watching for a deleted + created signal pair, the two might come so close that
558         // using waitForOneSignal will miss the created signal.  This function monitors both all
559         // the time to ensure both are received.
560         //
561         // In addition, allow dirty() to be emitted (for that same path) as an alternative
562 
563         const QString expectedPath = file1;
564         QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
565         QSignalSpy spyDeleted(&watch, &KDirWatch::deleted);
566         QSignalSpy spyCreated(&watch, &KDirWatch::created);
567 
568         // WHEN
569         QFile::remove(file1);
570         // And recreate immediately, to try and fool KDirWatch with unchanged ctime/mtime ;)
571         // (This emulates the /etc/localtime case)
572         createFile(file1);
573 
574         // THEN
575         int numTries = 0;
576         while (spyDeleted.isEmpty() && spyDirty.isEmpty()) {
577             if (++numTries > s_maxTries) {
578                 QFAIL("Failed to detect file deletion and recreation through either a deleted/created signal pair or through a dirty signal!");
579                 return;
580             }
581             spyDeleted.wait(50);
582             while (!spyDirty.isEmpty()) {
583                 if (spyDirty.at(0).at(0).toString() != expectedPath) { // unrelated
584                     spyDirty.removeFirst();
585                 } else {
586                     break;
587                 }
588             }
589         }
590         if (!spyDirty.isEmpty()) {
591             continue; // all ok
592         }
593 
594         // Don't bother waiting for the created signal if the signal spy already received a signal.
595         if (spyCreated.isEmpty() && !spyCreated.wait(50 * s_maxTries)) {
596             qWarning() << "Timeout waiting for KDirWatch signal created(QString) (" << expectedPath << ")";
597             QFAIL("Timeout waiting for KDirWatch signal created, after deleted was emitted");
598             return;
599         }
600 
601         QVERIFY(verifySignalPath(spyDeleted, "deleted(QString)", expectedPath) && verifySignalPath(spyCreated, "created(QString)", expectedPath));
602     }
603 
604     waitUntilMTimeChange(file1);
605 
606     appendToFile(file1);
607     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1));
608 }
609 
testDeleteAndRecreateDir()610 void KDirWatch_UnitTest::testDeleteAndRecreateDir()
611 {
612     // Like KDirModelTest::testOverwriteFileWithDir does at the end.
613     // The linux-2.6.31 bug made kdirwatch emit deletion signals about the -new- dir!
614     QTemporaryDir *tempDir1 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("olddir-"));
615     KDirWatch watch;
616     const QString path1 = tempDir1->path() + QLatin1Char('/');
617     watch.addDir(path1);
618 
619     delete tempDir1;
620     QTemporaryDir *tempDir2 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("newdir-"));
621     const QString path2 = tempDir2->path() + QLatin1Char('/');
622     watch.addDir(path2);
623 
624     QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), path1));
625 
626     delete tempDir2;
627 }
628 
testMoveTo()629 void KDirWatch_UnitTest::testMoveTo()
630 {
631     // This reproduces the famous digikam crash, #222974
632     // A watched file was being rewritten (overwritten by ksavefile),
633     // which gives inotify notifications "moved_to" followed by "delete_self"
634     //
635     // What happened then was that the delayed slotRescan
636     // would adjust things, making it status==Normal but the entry was
637     // listed as a "non-existent sub-entry" for the parent directory.
638     // That's inconsistent, and after removeFile() a dangling sub-entry would be left.
639 
640     // Initial data: creating file subdir/1
641     const QString file1 = m_path + QLatin1String("moveTo");
642     createFile(file1);
643 
644     KDirWatch watch;
645     watch.addDir(m_path);
646     watch.addFile(file1);
647     watch.startScan();
648 
649     if (watch.internalMethod() != KDirWatch::INotify) {
650         waitUntilMTimeChange(m_path);
651     }
652 
653     // Atomic rename of "temp" to "file1", much like KAutoSave would do when saving file1 again
654     // ### TODO: this isn't an atomic rename anymore. We need ::rename for that, or API from Qt.
655     const QString filetemp = m_path + QLatin1String("temp");
656     createFile(filetemp);
657     QFile::remove(file1);
658     QVERIFY(QFile::rename(filetemp, file1)); // overwrite file1 with the tempfile
659     qCDebug(KCOREADDONS_DEBUG) << "Overwrite file1 with tempfile";
660 
661     QSignalSpy spyCreated(&watch, &KDirWatch::created);
662     QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
663     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
664 
665     // Getting created() on an unwatched file is an inotify bonus, it's not part of the requirements.
666     if (watch.internalMethod() == KDirWatch::INotify) {
667         QCOMPARE(spyCreated.count(), 1);
668         QCOMPARE(spyCreated[0][0].toString(), file1);
669 
670         QCOMPARE(spyDirty.size(), 2);
671         QCOMPARE(spyDirty[1][0].toString(), filetemp);
672     }
673 
674     // make sure we're still watching it
675     appendToFile(file1);
676     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1));
677 
678     watch.removeFile(file1); // now we remove it
679 
680     // Just touch another file to trigger a findSubEntry - this where the crash happened
681     waitUntilMTimeChange(m_path);
682     createFile(filetemp);
683 #ifdef Q_OS_WIN
684     if (watch.internalMethod() == KDirWatch::QFSWatch) {
685         QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue);
686     }
687 #endif
688     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
689 }
690 
nestedEventLoop()691 void KDirWatch_UnitTest::nestedEventLoop() // #220153: watch two files, and modify 2nd while in slot for 1st
692 {
693     KDirWatch watch;
694 
695     const QString file0 = m_path + QLatin1String("nested_0");
696     watch.addFile(file0);
697     const QString file1 = m_path + QLatin1String("nested_1");
698     watch.addFile(file1);
699     watch.startScan();
700 
701     if (m_slow) {
702         waitUntilNewSecond();
703     }
704 
705     appendToFile(file0);
706 
707     // use own spy, to connect it before nestedEventLoopSlot, otherwise it reverses order
708     QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
709     connect(&watch, &KDirWatch::dirty, this, &KDirWatch_UnitTest::nestedEventLoopSlot);
710     waitForDirtySignal(watch, 1);
711     QVERIFY(spyDirty.count() >= 2);
712     QCOMPARE(spyDirty[0][0].toString(), file0);
713     QCOMPARE(spyDirty[spyDirty.count() - 1][0].toString(), file1);
714 }
715 
nestedEventLoopSlot()716 void KDirWatch_UnitTest::nestedEventLoopSlot()
717 {
718     const KDirWatch *const_watch = qobject_cast<const KDirWatch *>(sender());
719     KDirWatch *watch = const_cast<KDirWatch *>(const_watch);
720     // let's not come in this slot again
721     disconnect(watch, &KDirWatch::dirty, this, &KDirWatch_UnitTest::nestedEventLoopSlot);
722 
723     const QString file1 = m_path + QLatin1String("nested_1");
724     appendToFile(file1);
725     // The nested event processing here was from a messagebox in #220153
726     QList<QVariantList> spy = waitForDirtySignal(*watch, 1);
727     QVERIFY(spy.count() >= 1);
728     QCOMPARE(spy[spy.count() - 1][0].toString(), file1);
729 
730     // Now the user pressed reload...
731     const QString file0 = m_path + QLatin1String("nested_0");
732     watch->removeFile(file0);
733     watch->addFile(file0);
734 }
735 
testHardlinkChange()736 void KDirWatch_UnitTest::testHardlinkChange()
737 {
738 #ifdef Q_OS_UNIX
739 
740     // The unittest for the "detecting hardlink change to /etc/localtime" problem
741     // described on kde-core-devel (2009-07-03).
742     // It shows that watching a specific file doesn't inform us that the file is
743     // being recreated. Better watch the directory, for that.
744     // Well, it works with inotify (and fam - which uses inotify I guess?)
745 
746     const QString existingFile = m_path + QLatin1String("ExistingFile");
747     KDirWatch watch;
748     watch.addFile(existingFile);
749     watch.startScan();
750 
751     QFile::remove(existingFile);
752     const QString testFile = m_path + QLatin1String("TestFile");
753     QVERIFY(::link(QFile::encodeName(testFile).constData(), QFile::encodeName(existingFile).constData()) == 0); // make ExistingFile "point" to TestFile
754     QVERIFY(QFile::exists(existingFile));
755 
756     QVERIFY(waitForRecreationSignal(watch, existingFile));
757 
758     // The mtime of the existing file is the one of "TestFile", so it's old.
759     // We won't detect the change then, if we use that as baseline for waiting.
760     // We really need msec granularity, but that requires using statx which isn't available everywhere...
761     waitUntilNewSecond();
762     appendToFile(existingFile);
763     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile));
764 #else
765     QSKIP("Unix-specific");
766 #endif
767 }
768 
stopAndRestart()769 void KDirWatch_UnitTest::stopAndRestart()
770 {
771     KDirWatch watch;
772     watch.addDir(m_path);
773     watch.startScan();
774 
775     waitUntilMTimeChange(m_path);
776 
777     watch.stopDirScan(m_path);
778 
779     qCDebug(KCOREADDONS_DEBUG) << "create file 2 at" << QDateTime::currentDateTime().toMSecsSinceEpoch();
780     const QString file2 = createFile(2);
781     QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
782     QTest::qWait(200);
783     QCOMPARE(spyDirty.count(), 0); // suspended -> no signal
784 
785     watch.restartDirScan(m_path);
786 
787     QTest::qWait(200);
788 
789 #ifndef Q_OS_WIN
790     QCOMPARE(spyDirty.count(), 0); // as documented by restartDirScan: no signal
791     // On Windows, however, signals will get emitted, due to the ifdef Q_OS_WIN in the timestamp
792     // comparison ("trust QFSW since the mtime of dirs isn't modified")
793 #endif
794 
795     KDirWatch::statistics();
796 
797     waitUntilMTimeChange(m_path); // necessary for the mtime comparison in scanEntry
798 
799     qCDebug(KCOREADDONS_DEBUG) << "create file 3 at" << QDateTime::currentDateTime().toMSecsSinceEpoch();
800     const QString file3 = createFile(3);
801 #ifdef Q_OS_WIN
802     if (watch.internalMethod() == KDirWatch::QFSWatch) {
803         QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue);
804     }
805 #endif
806     QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
807 
808     QFile::remove(file2);
809     QFile::remove(file3);
810 }
811 
benchCreateTree()812 void KDirWatch_UnitTest::benchCreateTree()
813 {
814 #if !ENABLE_BENCHMARKS
815     QSKIP("Benchmarks are disabled in debug mode");
816 #endif
817     QTemporaryDir dir;
818 
819     QBENCHMARK {
820         createDirectoryTree(dir.path());
821     }
822 }
823 
benchCreateWatcher()824 void KDirWatch_UnitTest::benchCreateWatcher()
825 {
826 #if !ENABLE_BENCHMARKS
827     QSKIP("Benchmarks are disabled in debug mode");
828 #endif
829     QTemporaryDir dir;
830     createDirectoryTree(dir.path());
831 
832     QBENCHMARK {
833         KDirWatch watch;
834         watch.addDir(dir.path(), KDirWatch::WatchSubDirs | KDirWatch::WatchFiles);
835     }
836 }
837 
benchNotifyWatcher()838 void KDirWatch_UnitTest::benchNotifyWatcher()
839 {
840 #if !ENABLE_BENCHMARKS
841     QSKIP("Benchmarks are disabled in debug mode");
842 #endif
843     QTemporaryDir dir;
844     // create the dir once upfront
845     auto numFiles = createDirectoryTree(dir.path());
846     waitUntilMTimeChange(dir.path());
847 
848     KDirWatch watch;
849     watch.addDir(dir.path(), KDirWatch::WatchSubDirs | KDirWatch::WatchFiles);
850 
851     // now touch all the files repeatedly and wait for the dirty updates to come in
852     QSignalSpy spy(&watch, &KDirWatch::dirty);
853     QBENCHMARK {
854         createDirectoryTree(dir.path());
855         QTRY_COMPARE_WITH_TIMEOUT(spy.count(), numFiles, 30000);
856         spy.clear();
857     }
858 }
859 
testRefcounting()860 void KDirWatch_UnitTest::testRefcounting()
861 {
862 #if QT_CONFIG(cxx11_future)
863     bool initialExists = false;
864     bool secondExists = true; // the expectation is it will be set false
865     auto thread = QThread::create([&] {
866         QTemporaryDir dir;
867         {
868             KDirWatch watch;
869             watch.addFile(dir.path());
870             initialExists = KDirWatch::exists();
871         } // out of scope, the internal private should have been unset
872         secondExists = KDirWatch::exists();
873     });
874     thread->start();
875     thread->wait();
876     delete thread;
877     QVERIFY(initialExists);
878     QVERIFY(!secondExists);
879 #endif
880 }
881 
882 #include "kdirwatch_unittest.moc"
883