1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28#include <QtTest/QtTest>
29
30#include <QCoreApplication>
31
32#include <QTemporaryDir>
33#include <QFileSystemWatcher>
34#include <QElapsedTimer>
35#include <QTextStream>
36#include <QDir>
37#if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
38#include <windows.h>
39#endif
40
41/* All tests need to run in temporary directories not used
42 * by the application to avoid non-deterministic failures on Windows
43 * due to locked directories and left-overs from previous tests. */
44
45class tst_QFileSystemWatcher : public QObject
46{
47 Q_OBJECT
48public:
49 tst_QFileSystemWatcher();
50
51private slots:
52#ifdef QT_BUILD_INTERNAL
53 void basicTest_data();
54 void basicTest();
55
56 void watchDirectory_data();
57 void watchDirectory();
58#endif
59
60 void addPath();
61 void removePath();
62 void addPaths();
63 void removePaths();
64 void removePathsFilesInSameDirectory();
65
66#ifdef QT_BUILD_INTERNAL
67 void watchFileAndItsDirectory_data() { basicTest_data(); }
68 void watchFileAndItsDirectory();
69#endif
70
71 void nonExistingFile();
72
73 void removeFileAndUnWatch();
74
75 void destroyAfterQCoreApplication();
76
77#ifdef QT_BUILD_INTERNAL
78 void QTBUG2331();
79 void QTBUG2331_data() { basicTest_data(); }
80#endif
81
82 void signalsEmittedAfterFileMoved();
83
84 void watchUnicodeCharacters();
85#if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
86 void watchDirectoryAttributeChanges();
87#endif
88
89private:
90 QString m_tempDirPattern;
91};
92
93tst_QFileSystemWatcher::tst_QFileSystemWatcher()
94{
95 m_tempDirPattern = QDir::tempPath();
96 if (!m_tempDirPattern.endsWith(c: QLatin1Char('/')))
97 m_tempDirPattern += QLatin1Char('/');
98 m_tempDirPattern += QStringLiteral("tst_qfilesystemwatcherXXXXXX");
99
100#if defined(Q_OS_ANDROID) && !defined(Q_OS_ANDROID_EMBEDDED)
101 QDir::setCurrent(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
102#endif
103}
104
105#ifdef QT_BUILD_INTERNAL
106void tst_QFileSystemWatcher::basicTest_data()
107{
108 QTest::addColumn<QString>(name: "backend");
109 QTest::addColumn<QString>(name: "testFileName");
110 const QString testFile = QStringLiteral("testfile.txt");
111 // QTBUG-31341: Test the UNICODE capabilities; ensure no QString::toLower()
112 // is in the code path since that will lower case for example
113 // LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE with context, whereas the Windows file
114 // system will not.
115 const QString specialCharacterFile =
116 QString(QChar(ushort(0x130))) // LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE
117 + QChar(ushort(0x00DC)) // LATIN_CAPITAL_LETTER_U_WITH_DIAERESIS
118 + QStringLiteral(".txt");
119
120#if !defined(Q_OS_QNX) || !defined(QT_NO_INOTIFY)
121 QTest::newRow(dataTag: "native backend-testfile") << "native" << testFile;
122 QTest::newRow(dataTag: "native backend-specialchars") << "native" << specialCharacterFile;
123#endif
124 QTest::newRow(dataTag: "poller backend-testfile") << "poller" << testFile;
125}
126
127void tst_QFileSystemWatcher::basicTest()
128{
129 QFETCH(QString, backend);
130 QFETCH(QString, testFileName);
131
132 // create test file
133 QTemporaryDir temporaryDirectory(m_tempDirPattern);
134 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
135 QFile testFile(temporaryDirectory.path() + QLatin1Char('/') + testFileName);
136 QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
137 testFile.write(data: QByteArray("hello"));
138 testFile.close();
139
140 // set some file permissions
141 testFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
142
143 // create watcher, forcing it to use a specific backend
144 QFileSystemWatcher watcher;
145 watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
146 QVERIFY(watcher.addPath(testFile.fileName()));
147
148 QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::fileChanged);
149 QVERIFY(changedSpy.isValid());
150 QEventLoop eventLoop;
151 QTimer timer;
152 connect(sender: &timer, SIGNAL(timeout()), receiver: &eventLoop, SLOT(quit()));
153
154 // modify the file, should get a signal from the watcher
155
156 // resolution of the modification time is system dependent, but it's at most 1 second when using
157 // the polling engine. I've heard rumors that FAT32 has a 2 second resolution. So, we have to
158 // wait a bit before we can modify the file (hrmph)...
159 QTest::qWait(ms: 2000);
160
161 testFile.open(flags: QIODevice::WriteOnly | QIODevice::Append);
162 testFile.write(data: QByteArray("world"));
163 testFile.close();
164
165 // waiting max 5 seconds for notification for file modification to trigger
166 QTRY_COMPARE(changedSpy.count(), 1);
167 QCOMPARE(changedSpy.at(0).count(), 1);
168
169 QString fileName = changedSpy.at(i: 0).at(i: 0).toString();
170 QCOMPARE(fileName, testFile.fileName());
171
172 changedSpy.clear();
173
174 // remove the watch and modify the file, should not get a signal from the watcher
175 QVERIFY(watcher.removePath(testFile.fileName()));
176 testFile.open(flags: QIODevice::WriteOnly | QIODevice::Truncate);
177 testFile.write(data: QByteArray("hello universe!"));
178 testFile.close();
179
180 // waiting max 5 seconds for notification for file modification to trigger
181 timer.start(msec: 5000);
182 eventLoop.exec();
183
184 QCOMPARE(changedSpy.count(), 0);
185
186 // readd the file watch with a relative path
187 const QString relativeTestFileName = QDir::current().relativeFilePath(fileName: testFile.fileName());
188 QVERIFY(!relativeTestFileName.isEmpty());
189 QVERIFY(watcher.addPath(relativeTestFileName));
190 testFile.open(flags: QIODevice::WriteOnly | QIODevice::Truncate);
191 testFile.write(data: QByteArray("hello multiverse!"));
192 testFile.close();
193
194 QTRY_VERIFY(changedSpy.count() > 0);
195
196 QVERIFY(watcher.removePath(relativeTestFileName));
197
198 changedSpy.clear();
199
200 // readd the file watch
201 QVERIFY(watcher.addPath(testFile.fileName()));
202
203 // change the permissions, should get a signal from the watcher
204 testFile.setPermissions(QFile::ReadOwner);
205
206 // IN_ATTRIB doesn't work on QNX, so skip this test
207#if !defined(Q_OS_QNX)
208
209 // waiting max 5 seconds for notification for file permission modification to trigger
210 QTRY_COMPARE(changedSpy.count(), 1);
211 QCOMPARE(changedSpy.at(0).count(), 1);
212
213 fileName = changedSpy.at(i: 0).at(i: 0).toString();
214 QCOMPARE(fileName, testFile.fileName());
215
216#endif
217
218 changedSpy.clear();
219
220 // remove the watch and modify file permissions, should not get a signal from the watcher
221 QVERIFY(watcher.removePath(testFile.fileName()));
222 testFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOther);
223
224 // waiting max 5 seconds for notification for file modification to trigger
225 timer.start(msec: 5000);
226 eventLoop.exec();
227
228 QCOMPARE(changedSpy.count(), 0);
229
230 // readd the file watch
231 QVERIFY(watcher.addPath(testFile.fileName()));
232
233 // remove the file, should get a signal from the watcher
234 QVERIFY(testFile.remove());
235
236 // waiting max 5 seconds for notification for file removal to trigger
237 // > 0 && < 3 because some platforms may emit two changes
238 // XXX: which platforms? (QTBUG-23370)
239 QTRY_VERIFY(changedSpy.count() > 0 && changedSpy.count() < 3);
240 QCOMPARE(changedSpy.at(0).count(), 1);
241
242 fileName = changedSpy.at(i: 0).at(i: 0).toString();
243 QCOMPARE(fileName, testFile.fileName());
244
245 changedSpy.clear();
246
247 // recreate the file, we should not get any notification
248 QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
249 testFile.write(data: QByteArray("hello"));
250 testFile.close();
251
252 // waiting max 5 seconds for notification for file recreation to trigger
253 timer.start(msec: 5000);
254 eventLoop.exec();
255
256 QCOMPARE(changedSpy.count(), 0);
257
258 QVERIFY(testFile.remove());
259}
260
261void tst_QFileSystemWatcher::watchDirectory_data()
262{
263 QTest::addColumn<QString>(name: "backend");
264 QTest::addColumn<QStringList>(name: "testDirNames");
265 const QStringList testDirNames = {QStringLiteral("testdir"), QStringLiteral("testdir2")};
266
267 QTest::newRow(dataTag: "native backend") << "native" << testDirNames;
268 QTest::newRow(dataTag: "poller backend") << "poller" << testDirNames;
269}
270
271void tst_QFileSystemWatcher::watchDirectory()
272{
273 QFETCH(QString, backend);
274
275 QTemporaryDir temporaryDirectory(m_tempDirPattern);
276 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
277
278 QFETCH(QStringList, testDirNames);
279
280 QDir temporaryDir(temporaryDirectory.path());
281 QStringList testDirs;
282 QStringList testFiles;
283
284 for (const auto &testDirName : testDirNames) {
285 QVERIFY(temporaryDir.mkdir(testDirName));
286 QDir testDir = temporaryDir;
287 QVERIFY(testDir.cd(testDirName));
288
289 testFiles.append(t: testDir.filePath(fileName: "testFile.txt"));
290 QFile::remove(fileName: testFiles.last());
291 testDirs.append(t: testDir.absolutePath());
292 }
293
294 QFileSystemWatcher watcher;
295 watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
296 QVERIFY(watcher.addPaths(testDirs).isEmpty());
297
298 QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::directoryChanged);
299 QVERIFY(changedSpy.isValid());
300 QEventLoop eventLoop;
301 QTimer timer;
302 connect(sender: &timer, SIGNAL(timeout()), receiver: &eventLoop, SLOT(quit()));
303
304 // resolution of the modification time is system dependent, but it's at most 1 second when using
305 // the polling engine. From what I know, FAT32 has a 2 second resolution. So we have to
306 // wait before modifying the directory...
307 QTest::qWait(ms: 2000);
308 // remove the watch, should not get notification of a new file
309 QVERIFY(watcher.removePaths(testDirs).isEmpty());
310 for (const auto &testFileName : testFiles) {
311 QFile testFile(testFileName);
312 QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
313 testFile.close();
314 }
315
316 // waiting max 5 seconds for notification for file recreationg to trigger
317 timer.start(msec: 5000);
318 eventLoop.exec();
319
320 QCOMPARE(changedSpy.count(), 0);
321
322 QVERIFY(watcher.addPaths(testDirs).isEmpty());
323
324 // remove the file again, should get a signal from the watcher
325 for (const auto &testFileName : testFiles)
326 QVERIFY(QFile::remove(testFileName));
327
328 timer.start(msec: 5000);
329 eventLoop.exec();
330
331 // remove the directory, should get a signal from the watcher
332 for (const auto &testDirName : testDirs)
333 QVERIFY(temporaryDir.rmdir(testDirName));
334
335 QMap<QString, int> signalCounter;
336 for (const auto &testDirName : testDirs)
337 signalCounter[testDirName] = 0;
338
339 // waiting max 5 seconds for notification for directory removal to trigger
340 QTRY_COMPARE(changedSpy.count(), testDirs.size() * 2);
341 for (int i = 0; i < changedSpy.count(); i++) {
342 const auto &signal = changedSpy.at(i);
343 QCOMPARE(signal.count(), 1);
344
345 auto it = signalCounter.find(key: signal.at(i: 0).toString());
346 QVERIFY(it != signalCounter.end());
347 QVERIFY(it.value() < 2);
348 it.value()++;
349 }
350
351 for (const auto &count : signalCounter)
352 QCOMPARE(count, 2);
353
354 // flush pending signals (like the one from the rmdir above)
355 timer.start(msec: 5000);
356 eventLoop.exec();
357 changedSpy.clear();
358
359 // recreate the file, we should not get any notification
360 for (const auto &testDirName : testDirNames) {
361 if (!temporaryDir.mkdir(dirName: testDirName)) {
362 QSKIP(qPrintable(QString::fromLatin1("Failed to recreate directory '%1' under '%2', skipping final test.").
363 arg(testDirName, temporaryDir.absolutePath())));
364 }
365 }
366
367 // waiting max 5 seconds for notification for dir recreation to trigger
368 timer.start(msec: 5000);
369 eventLoop.exec();
370
371 QCOMPARE(changedSpy.count(), 0);
372
373 for (const auto &testDirName : testDirs)
374 QVERIFY(temporaryDir.rmdir(testDirName));
375}
376#endif // QT_BUILD_INTERNAL
377
378void tst_QFileSystemWatcher::addPath()
379{
380 QFileSystemWatcher watcher;
381 QString home = QDir::homePath();
382 QVERIFY(watcher.addPath(home));
383 QCOMPARE(watcher.directories().count(), 1);
384 QCOMPARE(watcher.directories().first(), home);
385
386 // second watch on an already-watched path should fail
387 QVERIFY(!watcher.addPath(home));
388 QCOMPARE(watcher.directories().count(), 1);
389
390 // With empty string
391 QTest::ignoreMessage(type: QtWarningMsg, message: "QFileSystemWatcher::addPath: path is empty");
392 QVERIFY(watcher.addPath(QString()));
393}
394
395void tst_QFileSystemWatcher::removePath()
396{
397 QFileSystemWatcher watcher;
398 QString home = QDir::homePath();
399 QVERIFY(watcher.addPath(home));
400 QVERIFY(watcher.removePath(home));
401 QCOMPARE(watcher.directories().count(), 0);
402 QVERIFY(!watcher.removePath(home));
403 QCOMPARE(watcher.directories().count(), 0);
404
405 // With empty string
406 QTest::ignoreMessage(type: QtWarningMsg, message: "QFileSystemWatcher::removePath: path is empty");
407 QVERIFY(watcher.removePath(QString()));
408}
409
410void tst_QFileSystemWatcher::addPaths()
411{
412 QFileSystemWatcher watcher;
413 QStringList paths;
414 paths << QDir::homePath() << QDir::currentPath();
415 QCOMPARE(watcher.addPaths(paths), QStringList());
416 QCOMPARE(watcher.directories().count(), 2);
417
418 // With empty list
419 paths.clear();
420 QTest::ignoreMessage(type: QtWarningMsg, message: "QFileSystemWatcher::addPaths: list is empty");
421 QCOMPARE(watcher.addPaths(paths), QStringList());
422}
423
424// A signal spy that records the paths and times received for better diagnostics.
425class FileSystemWatcherSpy : public QObject {
426 Q_OBJECT
427public:
428 enum Mode {
429 SpyOnDirectoryChanged,
430 SpyOnFileChanged
431 };
432
433 explicit FileSystemWatcherSpy(QFileSystemWatcher *watcher, Mode mode)
434 {
435 connect(sender: watcher, signal: mode == SpyOnDirectoryChanged ?
436 &QFileSystemWatcher::directoryChanged : &QFileSystemWatcher::fileChanged,
437 receiver: this, slot: &FileSystemWatcherSpy::spySlot);
438 m_elapsedTimer.start();
439 }
440
441 int count() const { return m_entries.size(); }
442 void clear()
443 {
444 m_entries.clear();
445 m_elapsedTimer.restart();
446 }
447
448 QByteArray receivedFilesMessage() const
449 {
450 QString result;
451 QTextStream str(&result);
452 str << "At " << m_elapsedTimer.elapsed() << "ms, received "
453 << count() << " changes: ";
454 for (int i =0, e = m_entries.size(); i < e; ++i) {
455 if (i)
456 str << ", ";
457 str << m_entries.at(i).timeStamp << "ms: " << QDir::toNativeSeparators(pathName: m_entries.at(i).path);
458 }
459 return result.toLocal8Bit();
460 }
461
462private slots:
463 void spySlot(const QString &p) { m_entries.append(t: Entry(m_elapsedTimer.elapsed(), p)); }
464
465private:
466 struct Entry {
467 Entry() : timeStamp(0) {}
468 Entry(qint64 t, const QString &p) : timeStamp(t), path(p) {}
469
470 qint64 timeStamp;
471 QString path;
472 };
473
474 QElapsedTimer m_elapsedTimer;
475 QList<Entry> m_entries;
476};
477
478void tst_QFileSystemWatcher::removePaths()
479{
480 QFileSystemWatcher watcher;
481 QStringList paths;
482 paths << QDir::homePath() << QDir::currentPath();
483 QCOMPARE(watcher.addPaths(paths), QStringList());
484 QCOMPARE(watcher.directories().count(), 2);
485 QCOMPARE(watcher.removePaths(paths), QStringList());
486 QCOMPARE(watcher.directories().count(), 0);
487
488 //With empty list
489 paths.clear();
490 QTest::ignoreMessage(type: QtWarningMsg, message: "QFileSystemWatcher::removePaths: list is empty");
491 watcher.removePaths(files: paths);
492}
493
494void tst_QFileSystemWatcher::removePathsFilesInSameDirectory()
495{
496 // QTBUG-46449/Windows: Check the return values of removePaths().
497 // When adding the 1st file, a thread is started to watch the temp path.
498 // After adding and removing the 2nd file, the thread is still running and
499 // success should be reported.
500 QTemporaryFile file1(m_tempDirPattern);
501 QTemporaryFile file2(m_tempDirPattern);
502 QVERIFY2(file1.open(), qPrintable(file1.errorString()));
503 QVERIFY2(file2.open(), qPrintable(file1.errorString()));
504 const QString path1 = file1.fileName();
505 const QString path2 = file2.fileName();
506 file1.close();
507 file2.close();
508 QFileSystemWatcher watcher;
509 QVERIFY(watcher.addPath(path1));
510 QCOMPARE(watcher.files().size(), 1);
511 QVERIFY(watcher.addPath(path2));
512 QCOMPARE(watcher.files().size(), 2);
513 QVERIFY(watcher.removePath(path1));
514 QCOMPARE(watcher.files().size(), 1);
515 QVERIFY(watcher.removePath(path2));
516 QCOMPARE(watcher.files().size(), 0);
517}
518
519#ifdef QT_BUILD_INTERNAL
520static QByteArray msgFileOperationFailed(const char *what, const QFile &f)
521{
522 return what + QByteArrayLiteral(" failed on \"")
523 + QDir::toNativeSeparators(pathName: f.fileName()).toLocal8Bit()
524 + QByteArrayLiteral("\": ") + f.errorString().toLocal8Bit();
525}
526
527void tst_QFileSystemWatcher::watchFileAndItsDirectory()
528{
529 QFETCH(QString, backend);
530
531 QTemporaryDir temporaryDirectory(m_tempDirPattern);
532 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
533
534 QDir temporaryDir(temporaryDirectory.path());
535 const QString testDirName = QStringLiteral("testDir");
536 QVERIFY(temporaryDir.mkdir(testDirName));
537 QDir testDir = temporaryDir;
538 QVERIFY(testDir.cd(testDirName));
539
540 QString testFileName = testDir.filePath(fileName: "testFile.txt");
541 QString secondFileName = testDir.filePath(fileName: "testFile2.txt");
542
543 QFile testFile(testFileName);
544 QVERIFY2(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", testFile));
545 QVERIFY2(testFile.write(QByteArrayLiteral("hello")) > 0, msgFileOperationFailed("write", testFile));
546 testFile.close();
547
548 QFileSystemWatcher watcher;
549 watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
550
551 QVERIFY(watcher.addPath(testDir.absolutePath()));
552 QVERIFY(watcher.addPath(testFileName));
553
554 QSignalSpy fileChangedSpy(&watcher, &QFileSystemWatcher::fileChanged);
555 FileSystemWatcherSpy dirChangedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged);
556 QVERIFY(fileChangedSpy.isValid());
557 QEventLoop eventLoop;
558 QTimer timer;
559 connect(sender: &timer, SIGNAL(timeout()), receiver: &eventLoop, SLOT(quit()));
560
561 // resolution of the modification time is system dependent, but it's at most 1 second when using
562 // the polling engine. From what I know, FAT32 has a 2 second resolution. So we have to
563 // wait before modifying the directory...
564 QTest::qWait(ms: 2000);
565
566 QVERIFY2(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", testFile));
567 QVERIFY2(testFile.write(QByteArrayLiteral("hello again")), msgFileOperationFailed("write", testFile));
568 testFile.close();
569
570#ifdef Q_OS_MAC
571 // wait again for the file's atime to be updated
572 QTest::qWait(2000);
573#endif
574
575 QTRY_VERIFY(fileChangedSpy.count() > 0);
576 QVERIFY2(dirChangedSpy.count() == 0, dirChangedSpy.receivedFilesMessage());
577
578 fileChangedSpy.clear();
579 QFile secondFile(secondFileName);
580 QVERIFY2(secondFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", secondFile));
581 QVERIFY2(secondFile.write(QByteArrayLiteral("Foo")) > 0, msgFileOperationFailed("write", secondFile));
582 secondFile.close();
583
584 timer.start(msec: 3000);
585 eventLoop.exec();
586 int fileChangedSpyCount = fileChangedSpy.count();
587#ifdef Q_OS_WIN
588 if (fileChangedSpyCount != 0)
589 QEXPECT_FAIL("", "See QTBUG-30943", Continue);
590#endif
591 QCOMPARE(fileChangedSpyCount, 0);
592 QCOMPARE(dirChangedSpy.count(), 1);
593
594 dirChangedSpy.clear();
595
596 QVERIFY(QFile::remove(testFileName));
597
598 QTRY_VERIFY(fileChangedSpy.count() > 0);
599 QTRY_COMPARE(dirChangedSpy.count(), 1);
600
601 fileChangedSpy.clear();
602 dirChangedSpy.clear();
603
604 // removing a deleted file should fail
605 QVERIFY(!watcher.removePath(testFileName));
606 QVERIFY(QFile::remove(secondFileName));
607
608 timer.start(msec: 3000);
609 eventLoop.exec();
610 QCOMPARE(fileChangedSpy.count(), 0);
611 QCOMPARE(dirChangedSpy.count(), 1);
612
613 // QTBUG-61792, removal should succeed (bug on Windows which uses one change
614 // notification per directory).
615 QVERIFY(watcher.removePath(testDir.absolutePath()));
616
617 QVERIFY(temporaryDir.rmdir(testDirName));
618}
619#endif // QT_BUILD_INTERNAL
620
621void tst_QFileSystemWatcher::nonExistingFile()
622{
623 // Don't crash...
624 QFileSystemWatcher watcher;
625 QVERIFY(!watcher.addPath("file_that_does_not_exist.txt"));
626
627 // Test that the paths returned in error aren't messed with
628 QCOMPARE(watcher.addPaths(QStringList() << "../..//./does-not-exist"),
629 QStringList() << "../..//./does-not-exist");
630
631 // empty path is not actually a failure
632 QCOMPARE(watcher.addPaths(QStringList() << QString()), QStringList());
633
634 // empty path is not actually a failure
635 QCOMPARE(watcher.removePaths(QStringList() << QString()), QStringList());
636}
637
638void tst_QFileSystemWatcher::removeFileAndUnWatch()
639{
640 QTemporaryDir temporaryDirectory(m_tempDirPattern);
641 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
642
643 const QString filename = temporaryDirectory.path() + QStringLiteral("/foo.txt");
644
645 QFileSystemWatcher watcher;
646
647 {
648 QFile testFile(filename);
649 QVERIFY2(testFile.open(QIODevice::WriteOnly),
650 qPrintable(QString::fromLatin1("Cannot open %1 for writing: %2").arg(filename, testFile.errorString())));
651 testFile.close();
652 }
653 QVERIFY(watcher.addPath(filename));
654
655 QFile::remove(fileName: filename);
656 /* There are potential race conditions here; the watcher thread might remove the file from its list
657 * before the call to watcher.removePath(), which then fails. When that happens, the auto-signal
658 * notification to remove the file from the watcher's main list will not be delivered before the next
659 * event loop such that the call to watcher.addPath() fails since the file is still in the main list. */
660 if (!watcher.removePath(file: filename))
661 QSKIP("Skipping remaining test due to race condition.");
662
663 {
664 QFile testFile(filename);
665 QVERIFY2(testFile.open(QIODevice::WriteOnly),
666 qPrintable(QString::fromLatin1("Cannot open %1 for writing: %2").arg(filename, testFile.errorString())));
667 testFile.close();
668 }
669 QVERIFY(watcher.addPath(filename));
670}
671
672class SomeSingleton : public QObject
673{
674public:
675 SomeSingleton() : mFsWatcher(new QFileSystemWatcher(this)) { mFsWatcher->addPath(file: QLatin1String("/usr/lib"));}
676 void bla() const {}
677 QFileSystemWatcher* mFsWatcher;
678};
679
680Q_GLOBAL_STATIC(SomeSingleton, someSingleton)
681
682// This is a regression test for QTBUG-15255, where a deadlock occurred if a
683// QFileSystemWatcher was destroyed after the QCoreApplication instance had
684// been destroyed. There are no explicit verification steps in this test --
685// it is sufficient that the test terminates.
686void tst_QFileSystemWatcher::destroyAfterQCoreApplication()
687{
688 someSingleton()->bla();
689 QTest::qWait(ms: 30);
690}
691
692#ifdef QT_BUILD_INTERNAL
693// regression test for QTBUG2331.
694// essentially, on windows, directories were not unwatched after being deleted
695// from the disk, causing all sorts of interesting problems.
696void tst_QFileSystemWatcher::QTBUG2331()
697{
698 QFETCH(QString, backend);
699
700 QTemporaryDir temporaryDirectory(m_tempDirPattern);
701 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
702 QFileSystemWatcher watcher;
703 watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
704 QVERIFY(watcher.addPath(temporaryDirectory.path()));
705
706 // watch signal
707 QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::directoryChanged);
708 QVERIFY(changedSpy.isValid());
709
710 // remove directory, we should get one change signal, and we should no longer
711 // be watching the directory.
712 QVERIFY(temporaryDirectory.remove());
713 QTRY_COMPARE(changedSpy.count(), 1);
714 QCOMPARE(watcher.directories(), QStringList());
715}
716#endif // QT_BUILD_INTERNAL
717
718class SignalReceiver : public QObject
719{
720 Q_OBJECT
721public:
722 SignalReceiver(const QDir &moveSrcDir,
723 const QString &moveDestination,
724 QFileSystemWatcher *watcher,
725 QObject *parent = 0)
726 : QObject(parent),
727 added(false),
728 moveSrcDir(moveSrcDir),
729 moveDestination(QDir(moveDestination)),
730 watcher(watcher)
731 {}
732
733public slots:
734 void fileChanged(const QString &path)
735 {
736 QFileInfo finfo(path);
737
738 QCOMPARE(finfo.absolutePath(), moveSrcDir.absolutePath());
739
740 if (!added) {
741 foreach (const QFileInfo &fi, moveDestination.entryInfoList(QDir::Files | QDir::NoSymLinks))
742 watcher->addPath(file: fi.absoluteFilePath());
743 added = true;
744 }
745 }
746
747private:
748 bool added;
749 QDir moveSrcDir;
750 QDir moveDestination;
751 QFileSystemWatcher *watcher;
752};
753
754// regression test for QTBUG-33211.
755// using inotify backend if a file is moved and then added to the watcher
756// before all the fileChanged signals are emitted the remaining signals are
757// emitted with the destination path instead of the starting path
758void tst_QFileSystemWatcher::signalsEmittedAfterFileMoved()
759{
760 const int fileCount = 10;
761 QTemporaryDir temporaryDirectory(m_tempDirPattern);
762 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
763
764 QDir testDir(temporaryDirectory.path());
765 QVERIFY(testDir.mkdir("movehere"));
766 QString movePath = testDir.filePath(fileName: "movehere");
767
768 for (int i = 0; i < fileCount; ++i) {
769 const QByteArray iB = QByteArray::number(i);
770 QFile f(testDir.filePath(fileName: QLatin1String("test") + QString::fromLatin1(str: iB) + QLatin1String(".txt")));
771 QVERIFY(f.open(QIODevice::WriteOnly));
772 f.write(data: QByteArray("i am ") + iB);
773 f.close();
774 }
775
776 QFileSystemWatcher watcher;
777 QVERIFY(watcher.addPath(testDir.path()));
778 QVERIFY(watcher.addPath(movePath));
779
780 // add files to watcher
781 QFileInfoList files = testDir.entryInfoList(filters: QDir::Files | QDir::NoSymLinks);
782 QCOMPARE(files.size(), fileCount);
783 foreach (const QFileInfo &finfo, files)
784 QVERIFY(watcher.addPath(finfo.absoluteFilePath()));
785
786 // create the signal receiver
787 SignalReceiver signalReceiver(testDir, movePath, &watcher);
788 connect(sender: &watcher, SIGNAL(fileChanged(QString)), receiver: &signalReceiver, SLOT(fileChanged(QString)));
789
790 // watch signals
791 FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnFileChanged);
792 QCOMPARE(changedSpy.count(), 0);
793
794 // move files to second directory
795 foreach (const QFileInfo &finfo, files)
796 QVERIFY(testDir.rename(finfo.fileName(), QString("movehere/%2").arg(finfo.fileName())));
797
798 QCoreApplication::processEvents();
799 QVERIFY2(changedSpy.count() <= fileCount, changedSpy.receivedFilesMessage());
800 QTRY_COMPARE(changedSpy.count(), fileCount);
801}
802
803void tst_QFileSystemWatcher::watchUnicodeCharacters()
804{
805 QTemporaryDir temporaryDirectory(m_tempDirPattern);
806 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
807
808 QDir testDir(temporaryDirectory.path());
809 const QString subDir(QString::fromLatin1(str: "caf\xe9"));
810 QVERIFY(testDir.mkdir(subDir));
811 testDir = QDir(temporaryDirectory.path() + QDir::separator() + subDir);
812
813 QFileSystemWatcher watcher;
814 QVERIFY(watcher.addPath(testDir.path()));
815
816 FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged);
817 QCOMPARE(changedSpy.count(), 0);
818 QVERIFY(testDir.mkdir("creme"));
819 QTRY_COMPARE(changedSpy.count(), 1);
820}
821
822#if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
823void tst_QFileSystemWatcher::watchDirectoryAttributeChanges()
824{
825 QTemporaryDir temporaryDirectory(m_tempDirPattern);
826 QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
827
828 QDir testDir(temporaryDirectory.path());
829 const QString subDir(QString::fromLatin1("attrib_test"));
830 QVERIFY(testDir.mkdir(subDir));
831 testDir = QDir(temporaryDirectory.path() + QDir::separator() + subDir);
832
833 QFileSystemWatcher watcher;
834 QVERIFY(watcher.addPath(temporaryDirectory.path()));
835 FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged);
836 QCOMPARE(changedSpy.count(), 0);
837 QVERIFY(SetFileAttributes(reinterpret_cast<LPCWSTR>(testDir.absolutePath().utf16()), FILE_ATTRIBUTE_HIDDEN) != 0);
838 QTRY_COMPARE(changedSpy.count(), 1);
839 QVERIFY(SetFileAttributes(reinterpret_cast<LPCWSTR>(testDir.absolutePath().utf16()), FILE_ATTRIBUTE_NORMAL) != 0);
840 QTRY_COMPARE(changedSpy.count(), 2);
841}
842#endif
843
844QTEST_MAIN(tst_QFileSystemWatcher)
845#include "tst_qfilesystemwatcher.moc"
846

source code of qtbase/tests/auto/corelib/io/qfilesystemwatcher/tst_qfilesystemwatcher.cpp