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 | |
45 | class tst_QFileSystemWatcher : public QObject |
46 | { |
47 | Q_OBJECT |
48 | public: |
49 | tst_QFileSystemWatcher(); |
50 | |
51 | private 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 | |
89 | private: |
90 | QString m_tempDirPattern; |
91 | }; |
92 | |
93 | tst_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 |
106 | void 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 | |
127 | void 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 | |
261 | void 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 | |
271 | void 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 | |
378 | void 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 | |
395 | void 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 | |
410 | void 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. |
425 | class FileSystemWatcherSpy : public QObject { |
426 | Q_OBJECT |
427 | public: |
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 | |
462 | private slots: |
463 | void spySlot(const QString &p) { m_entries.append(t: Entry(m_elapsedTimer.elapsed(), p)); } |
464 | |
465 | private: |
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 | |
478 | void 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 | |
494 | void 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 |
520 | static 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 | |
527 | void 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 | |
621 | void 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 | |
638 | void 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 | |
672 | class SomeSingleton : public QObject |
673 | { |
674 | public: |
675 | SomeSingleton() : mFsWatcher(new QFileSystemWatcher(this)) { mFsWatcher->addPath(file: QLatin1String("/usr/lib" ));} |
676 | void bla() const {} |
677 | QFileSystemWatcher* mFsWatcher; |
678 | }; |
679 | |
680 | Q_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. |
686 | void 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. |
696 | void 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 | |
718 | class SignalReceiver : public QObject |
719 | { |
720 | Q_OBJECT |
721 | public: |
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 | |
733 | public 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 | |
747 | private: |
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 |
758 | void 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 | |
803 | void 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) |
823 | void 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 | |
844 | QTEST_MAIN(tst_QFileSystemWatcher) |
845 | #include "tst_qfilesystemwatcher.moc" |
846 | |