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