| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the test suite of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| #include <QtTest/QtTest> |
| |
| #include <QCoreApplication> |
| |
| #include <QTemporaryDir> |
| #include <QFileSystemWatcher> |
| #include <QElapsedTimer> |
| #include <QTextStream> |
| #include <QDir> |
| #if defined(Q_OS_WIN) && !defined(Q_OS_WINRT) |
| #include <windows.h> |
| #endif |
| |
| /* All tests need to run in temporary directories not used |
| * by the application to avoid non-deterministic failures on Windows |
| * due to locked directories and left-overs from previous tests. */ |
| |
| class tst_QFileSystemWatcher : public QObject |
| { |
| Q_OBJECT |
| public: |
| tst_QFileSystemWatcher(); |
| |
| private slots: |
| #ifdef QT_BUILD_INTERNAL |
| void basicTest_data(); |
| void basicTest(); |
| |
| void watchDirectory_data(); |
| void watchDirectory(); |
| #endif |
| |
| void addPath(); |
| void removePath(); |
| void addPaths(); |
| void removePaths(); |
| void removePathsFilesInSameDirectory(); |
| |
| #ifdef QT_BUILD_INTERNAL |
| void watchFileAndItsDirectory_data() { basicTest_data(); } |
| void watchFileAndItsDirectory(); |
| #endif |
| |
| void nonExistingFile(); |
| |
| void removeFileAndUnWatch(); |
| |
| void destroyAfterQCoreApplication(); |
| |
| #ifdef QT_BUILD_INTERNAL |
| void QTBUG2331(); |
| void QTBUG2331_data() { basicTest_data(); } |
| #endif |
| |
| void signalsEmittedAfterFileMoved(); |
| |
| void watchUnicodeCharacters(); |
| #if defined(Q_OS_WIN) && !defined(Q_OS_WINRT) |
| void watchDirectoryAttributeChanges(); |
| #endif |
| |
| private: |
| QString m_tempDirPattern; |
| }; |
| |
| tst_QFileSystemWatcher::tst_QFileSystemWatcher() |
| { |
| m_tempDirPattern = QDir::tempPath(); |
| if (!m_tempDirPattern.endsWith(QLatin1Char('/'))) |
| m_tempDirPattern += QLatin1Char('/'); |
| m_tempDirPattern += QStringLiteral("tst_qfilesystemwatcherXXXXXX"); |
| |
| #if defined(Q_OS_ANDROID) && !defined(Q_OS_ANDROID_EMBEDDED) |
| QDir::setCurrent(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); |
| #endif |
| } |
| |
| #ifdef QT_BUILD_INTERNAL |
| void tst_QFileSystemWatcher::basicTest_data() |
| { |
| QTest::addColumn<QString>("backend"); |
| QTest::addColumn<QString>("testFileName"); |
| const QString testFile = QStringLiteral("testfile.txt"); |
| // QTBUG-31341: Test the UNICODE capabilities; ensure no QString::toLower() |
| // is in the code path since that will lower case for example |
| // LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE with context, whereas the Windows file |
| // system will not. |
| const QString specialCharacterFile = |
| QString(QChar(ushort(0x130))) // LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE |
| + QChar(ushort(0x00DC)) // LATIN_CAPITAL_LETTER_U_WITH_DIAERESIS |
| + QStringLiteral(".txt"); |
| |
| #if !defined(Q_OS_QNX) || !defined(QT_NO_INOTIFY) |
| QTest::newRow("native backend-testfile") << "native" << testFile; |
| QTest::newRow("native backend-specialchars") << "native" << specialCharacterFile; |
| #endif |
| QTest::newRow("poller backend-testfile") << "poller" << testFile; |
| } |
| |
| void tst_QFileSystemWatcher::basicTest() |
| { |
| QFETCH(QString, backend); |
| QFETCH(QString, testFileName); |
| |
| // create test file |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| QFile testFile(temporaryDirectory.path() + QLatin1Char('/') + testFileName); |
| QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); |
| testFile.write(QByteArray("hello")); |
| testFile.close(); |
| |
| // set some file permissions |
| testFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner); |
| |
| // create watcher, forcing it to use a specific backend |
| QFileSystemWatcher watcher; |
| watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend); |
| QVERIFY(watcher.addPath(testFile.fileName())); |
| |
| QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::fileChanged); |
| QVERIFY(changedSpy.isValid()); |
| QEventLoop eventLoop; |
| QTimer timer; |
| connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit())); |
| |
| // modify the file, should get a signal from the watcher |
| |
| // resolution of the modification time is system dependent, but it's at most 1 second when using |
| // the polling engine. I've heard rumors that FAT32 has a 2 second resolution. So, we have to |
| // wait a bit before we can modify the file (hrmph)... |
| QTest::qWait(2000); |
| |
| testFile.open(QIODevice::WriteOnly | QIODevice::Append); |
| testFile.write(QByteArray("world")); |
| testFile.close(); |
| |
| // waiting max 5 seconds for notification for file modification to trigger |
| QTRY_COMPARE(changedSpy.count(), 1); |
| QCOMPARE(changedSpy.at(0).count(), 1); |
| |
| QString fileName = changedSpy.at(0).at(0).toString(); |
| QCOMPARE(fileName, testFile.fileName()); |
| |
| changedSpy.clear(); |
| |
| // remove the watch and modify the file, should not get a signal from the watcher |
| QVERIFY(watcher.removePath(testFile.fileName())); |
| testFile.open(QIODevice::WriteOnly | QIODevice::Truncate); |
| testFile.write(QByteArray("hello universe!")); |
| testFile.close(); |
| |
| // waiting max 5 seconds for notification for file modification to trigger |
| timer.start(5000); |
| eventLoop.exec(); |
| |
| QCOMPARE(changedSpy.count(), 0); |
| |
| // readd the file watch with a relative path |
| const QString relativeTestFileName = QDir::current().relativeFilePath(testFile.fileName()); |
| QVERIFY(!relativeTestFileName.isEmpty()); |
| QVERIFY(watcher.addPath(relativeTestFileName)); |
| testFile.open(QIODevice::WriteOnly | QIODevice::Truncate); |
| testFile.write(QByteArray("hello multiverse!")); |
| testFile.close(); |
| |
| QTRY_VERIFY(changedSpy.count() > 0); |
| |
| QVERIFY(watcher.removePath(relativeTestFileName)); |
| |
| changedSpy.clear(); |
| |
| // readd the file watch |
| QVERIFY(watcher.addPath(testFile.fileName())); |
| |
| // change the permissions, should get a signal from the watcher |
| testFile.setPermissions(QFile::ReadOwner); |
| |
| // IN_ATTRIB doesn't work on QNX, so skip this test |
| #if !defined(Q_OS_QNX) |
| |
| // waiting max 5 seconds for notification for file permission modification to trigger |
| QTRY_COMPARE(changedSpy.count(), 1); |
| QCOMPARE(changedSpy.at(0).count(), 1); |
| |
| fileName = changedSpy.at(0).at(0).toString(); |
| QCOMPARE(fileName, testFile.fileName()); |
| |
| #endif |
| |
| changedSpy.clear(); |
| |
| // remove the watch and modify file permissions, should not get a signal from the watcher |
| QVERIFY(watcher.removePath(testFile.fileName())); |
| testFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOther); |
| |
| // waiting max 5 seconds for notification for file modification to trigger |
| timer.start(5000); |
| eventLoop.exec(); |
| |
| QCOMPARE(changedSpy.count(), 0); |
| |
| // readd the file watch |
| QVERIFY(watcher.addPath(testFile.fileName())); |
| |
| // remove the file, should get a signal from the watcher |
| QVERIFY(testFile.remove()); |
| |
| // waiting max 5 seconds for notification for file removal to trigger |
| // > 0 && < 3 because some platforms may emit two changes |
| // XXX: which platforms? (QTBUG-23370) |
| QTRY_VERIFY(changedSpy.count() > 0 && changedSpy.count() < 3); |
| QCOMPARE(changedSpy.at(0).count(), 1); |
| |
| fileName = changedSpy.at(0).at(0).toString(); |
| QCOMPARE(fileName, testFile.fileName()); |
| |
| changedSpy.clear(); |
| |
| // recreate the file, we should not get any notification |
| QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); |
| testFile.write(QByteArray("hello")); |
| testFile.close(); |
| |
| // waiting max 5 seconds for notification for file recreation to trigger |
| timer.start(5000); |
| eventLoop.exec(); |
| |
| QCOMPARE(changedSpy.count(), 0); |
| |
| QVERIFY(testFile.remove()); |
| } |
| |
| void tst_QFileSystemWatcher::watchDirectory_data() |
| { |
| QTest::addColumn<QString>("backend"); |
| QTest::addColumn<QStringList>("testDirNames"); |
| const QStringList testDirNames = {QStringLiteral("testdir"), QStringLiteral("testdir2")}; |
| |
| QTest::newRow("native backend") << "native" << testDirNames; |
| QTest::newRow("poller backend") << "poller" << testDirNames; |
| } |
| |
| void tst_QFileSystemWatcher::watchDirectory() |
| { |
| QFETCH(QString, backend); |
| |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| |
| QFETCH(QStringList, testDirNames); |
| |
| QDir temporaryDir(temporaryDirectory.path()); |
| QStringList testDirs; |
| QStringList testFiles; |
| |
| for (const auto &testDirName : testDirNames) { |
| QVERIFY(temporaryDir.mkdir(testDirName)); |
| QDir testDir = temporaryDir; |
| QVERIFY(testDir.cd(testDirName)); |
| |
| testFiles.append(testDir.filePath("testFile.txt")); |
| QFile::remove(testFiles.last()); |
| testDirs.append(testDir.absolutePath()); |
| } |
| |
| QFileSystemWatcher watcher; |
| watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend); |
| QVERIFY(watcher.addPaths(testDirs).isEmpty()); |
| |
| QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::directoryChanged); |
| QVERIFY(changedSpy.isValid()); |
| QEventLoop eventLoop; |
| QTimer timer; |
| connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit())); |
| |
| // resolution of the modification time is system dependent, but it's at most 1 second when using |
| // the polling engine. From what I know, FAT32 has a 2 second resolution. So we have to |
| // wait before modifying the directory... |
| QTest::qWait(2000); |
| // remove the watch, should not get notification of a new file |
| QVERIFY(watcher.removePaths(testDirs).isEmpty()); |
| for (const auto &testFileName : testFiles) { |
| QFile testFile(testFileName); |
| QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); |
| testFile.close(); |
| } |
| |
| // waiting max 5 seconds for notification for file recreationg to trigger |
| timer.start(5000); |
| eventLoop.exec(); |
| |
| QCOMPARE(changedSpy.count(), 0); |
| |
| QVERIFY(watcher.addPaths(testDirs).isEmpty()); |
| |
| // remove the file again, should get a signal from the watcher |
| for (const auto &testFileName : testFiles) |
| QVERIFY(QFile::remove(testFileName)); |
| |
| timer.start(5000); |
| eventLoop.exec(); |
| |
| // remove the directory, should get a signal from the watcher |
| for (const auto &testDirName : testDirs) |
| QVERIFY(temporaryDir.rmdir(testDirName)); |
| |
| QMap<QString, int> signalCounter; |
| for (const auto &testDirName : testDirs) |
| signalCounter[testDirName] = 0; |
| |
| // waiting max 5 seconds for notification for directory removal to trigger |
| QTRY_COMPARE(changedSpy.count(), testDirs.size() * 2); |
| for (int i = 0; i < changedSpy.count(); i++) { |
| const auto &signal = changedSpy.at(i); |
| QCOMPARE(signal.count(), 1); |
| |
| auto it = signalCounter.find(signal.at(0).toString()); |
| QVERIFY(it != signalCounter.end()); |
| QVERIFY(it.value() < 2); |
| it.value()++; |
| } |
| |
| for (const auto &count : signalCounter) |
| QCOMPARE(count, 2); |
| |
| // flush pending signals (like the one from the rmdir above) |
| timer.start(5000); |
| eventLoop.exec(); |
| changedSpy.clear(); |
| |
| // recreate the file, we should not get any notification |
| for (const auto &testDirName : testDirNames) { |
| if (!temporaryDir.mkdir(testDirName)) { |
| QSKIP(qPrintable(QString::fromLatin1("Failed to recreate directory '%1' under '%2', skipping final test."). |
| arg(testDirName, temporaryDir.absolutePath()))); |
| } |
| } |
| |
| // waiting max 5 seconds for notification for dir recreation to trigger |
| timer.start(5000); |
| eventLoop.exec(); |
| |
| QCOMPARE(changedSpy.count(), 0); |
| |
| for (const auto &testDirName : testDirs) |
| QVERIFY(temporaryDir.rmdir(testDirName)); |
| } |
| #endif // QT_BUILD_INTERNAL |
| |
| void tst_QFileSystemWatcher::addPath() |
| { |
| QFileSystemWatcher watcher; |
| QString home = QDir::homePath(); |
| QVERIFY(watcher.addPath(home)); |
| QCOMPARE(watcher.directories().count(), 1); |
| QCOMPARE(watcher.directories().first(), home); |
| |
| // second watch on an already-watched path should fail |
| QVERIFY(!watcher.addPath(home)); |
| QCOMPARE(watcher.directories().count(), 1); |
| |
| // With empty string |
| QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::addPath: path is empty"); |
| QVERIFY(watcher.addPath(QString())); |
| } |
| |
| void tst_QFileSystemWatcher::removePath() |
| { |
| QFileSystemWatcher watcher; |
| QString home = QDir::homePath(); |
| QVERIFY(watcher.addPath(home)); |
| QVERIFY(watcher.removePath(home)); |
| QCOMPARE(watcher.directories().count(), 0); |
| QVERIFY(!watcher.removePath(home)); |
| QCOMPARE(watcher.directories().count(), 0); |
| |
| // With empty string |
| QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::removePath: path is empty"); |
| QVERIFY(watcher.removePath(QString())); |
| } |
| |
| void tst_QFileSystemWatcher::addPaths() |
| { |
| QFileSystemWatcher watcher; |
| QStringList paths; |
| paths << QDir::homePath() << QDir::currentPath(); |
| QCOMPARE(watcher.addPaths(paths), QStringList()); |
| QCOMPARE(watcher.directories().count(), 2); |
| |
| // With empty list |
| paths.clear(); |
| QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::addPaths: list is empty"); |
| QCOMPARE(watcher.addPaths(paths), QStringList()); |
| } |
| |
| // A signal spy that records the paths and times received for better diagnostics. |
| class FileSystemWatcherSpy : public QObject { |
| Q_OBJECT |
| public: |
| enum Mode { |
| SpyOnDirectoryChanged, |
| SpyOnFileChanged |
| }; |
| |
| explicit FileSystemWatcherSpy(QFileSystemWatcher *watcher, Mode mode) |
| { |
| connect(watcher, mode == SpyOnDirectoryChanged ? |
| &QFileSystemWatcher::directoryChanged : &QFileSystemWatcher::fileChanged, |
| this, &FileSystemWatcherSpy::spySlot); |
| m_elapsedTimer.start(); |
| } |
| |
| int count() const { return m_entries.size(); } |
| void clear() |
| { |
| m_entries.clear(); |
| m_elapsedTimer.restart(); |
| } |
| |
| QByteArray receivedFilesMessage() const |
| { |
| QString result; |
| QTextStream str(&result); |
| str << "At " << m_elapsedTimer.elapsed() << "ms, received " |
| << count() << " changes: "; |
| for (int i =0, e = m_entries.size(); i < e; ++i) { |
| if (i) |
| str << ", "; |
| str << m_entries.at(i).timeStamp << "ms: " << QDir::toNativeSeparators(m_entries.at(i).path); |
| } |
| return result.toLocal8Bit(); |
| } |
| |
| private slots: |
| void spySlot(const QString &p) { m_entries.append(Entry(m_elapsedTimer.elapsed(), p)); } |
| |
| private: |
| struct Entry { |
| Entry() : timeStamp(0) {} |
| Entry(qint64 t, const QString &p) : timeStamp(t), path(p) {} |
| |
| qint64 timeStamp; |
| QString path; |
| }; |
| |
| QElapsedTimer m_elapsedTimer; |
| QList<Entry> m_entries; |
| }; |
| |
| void tst_QFileSystemWatcher::removePaths() |
| { |
| QFileSystemWatcher watcher; |
| QStringList paths; |
| paths << QDir::homePath() << QDir::currentPath(); |
| QCOMPARE(watcher.addPaths(paths), QStringList()); |
| QCOMPARE(watcher.directories().count(), 2); |
| QCOMPARE(watcher.removePaths(paths), QStringList()); |
| QCOMPARE(watcher.directories().count(), 0); |
| |
| //With empty list |
| paths.clear(); |
| QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::removePaths: list is empty"); |
| watcher.removePaths(paths); |
| } |
| |
| void tst_QFileSystemWatcher::removePathsFilesInSameDirectory() |
| { |
| // QTBUG-46449/Windows: Check the return values of removePaths(). |
| // When adding the 1st file, a thread is started to watch the temp path. |
| // After adding and removing the 2nd file, the thread is still running and |
| // success should be reported. |
| QTemporaryFile file1(m_tempDirPattern); |
| QTemporaryFile file2(m_tempDirPattern); |
| QVERIFY2(file1.open(), qPrintable(file1.errorString())); |
| QVERIFY2(file2.open(), qPrintable(file1.errorString())); |
| const QString path1 = file1.fileName(); |
| const QString path2 = file2.fileName(); |
| file1.close(); |
| file2.close(); |
| QFileSystemWatcher watcher; |
| QVERIFY(watcher.addPath(path1)); |
| QCOMPARE(watcher.files().size(), 1); |
| QVERIFY(watcher.addPath(path2)); |
| QCOMPARE(watcher.files().size(), 2); |
| QVERIFY(watcher.removePath(path1)); |
| QCOMPARE(watcher.files().size(), 1); |
| QVERIFY(watcher.removePath(path2)); |
| QCOMPARE(watcher.files().size(), 0); |
| } |
| |
| #ifdef QT_BUILD_INTERNAL |
| static QByteArray msgFileOperationFailed(const char *what, const QFile &f) |
| { |
| return what + QByteArrayLiteral(" failed on \"") |
| + QDir::toNativeSeparators(f.fileName()).toLocal8Bit() |
| + QByteArrayLiteral("\": ") + f.errorString().toLocal8Bit(); |
| } |
| |
| void tst_QFileSystemWatcher::watchFileAndItsDirectory() |
| { |
| QFETCH(QString, backend); |
| |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| |
| QDir temporaryDir(temporaryDirectory.path()); |
| const QString testDirName = QStringLiteral("testDir"); |
| QVERIFY(temporaryDir.mkdir(testDirName)); |
| QDir testDir = temporaryDir; |
| QVERIFY(testDir.cd(testDirName)); |
| |
| QString testFileName = testDir.filePath("testFile.txt"); |
| QString secondFileName = testDir.filePath("testFile2.txt"); |
| |
| QFile testFile(testFileName); |
| QVERIFY2(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", testFile)); |
| QVERIFY2(testFile.write(QByteArrayLiteral("hello")) > 0, msgFileOperationFailed("write", testFile)); |
| testFile.close(); |
| |
| QFileSystemWatcher watcher; |
| watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend); |
| |
| QVERIFY(watcher.addPath(testDir.absolutePath())); |
| QVERIFY(watcher.addPath(testFileName)); |
| |
| QSignalSpy fileChangedSpy(&watcher, &QFileSystemWatcher::fileChanged); |
| FileSystemWatcherSpy dirChangedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged); |
| QVERIFY(fileChangedSpy.isValid()); |
| QEventLoop eventLoop; |
| QTimer timer; |
| connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit())); |
| |
| // resolution of the modification time is system dependent, but it's at most 1 second when using |
| // the polling engine. From what I know, FAT32 has a 2 second resolution. So we have to |
| // wait before modifying the directory... |
| QTest::qWait(2000); |
| |
| QVERIFY2(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", testFile)); |
| QVERIFY2(testFile.write(QByteArrayLiteral("hello again")), msgFileOperationFailed("write", testFile)); |
| testFile.close(); |
| |
| #ifdef Q_OS_MAC |
| // wait again for the file's atime to be updated |
| QTest::qWait(2000); |
| #endif |
| |
| QTRY_VERIFY(fileChangedSpy.count() > 0); |
| QVERIFY2(dirChangedSpy.count() == 0, dirChangedSpy.receivedFilesMessage()); |
| |
| fileChangedSpy.clear(); |
| QFile secondFile(secondFileName); |
| QVERIFY2(secondFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", secondFile)); |
| QVERIFY2(secondFile.write(QByteArrayLiteral("Foo")) > 0, msgFileOperationFailed("write", secondFile)); |
| secondFile.close(); |
| |
| timer.start(3000); |
| eventLoop.exec(); |
| int fileChangedSpyCount = fileChangedSpy.count(); |
| #ifdef Q_OS_WIN |
| if (fileChangedSpyCount != 0) |
| QEXPECT_FAIL("", "See QTBUG-30943", Continue); |
| #endif |
| QCOMPARE(fileChangedSpyCount, 0); |
| QCOMPARE(dirChangedSpy.count(), 1); |
| |
| dirChangedSpy.clear(); |
| |
| QVERIFY(QFile::remove(testFileName)); |
| |
| QTRY_VERIFY(fileChangedSpy.count() > 0); |
| QTRY_COMPARE(dirChangedSpy.count(), 1); |
| |
| fileChangedSpy.clear(); |
| dirChangedSpy.clear(); |
| |
| // removing a deleted file should fail |
| QVERIFY(!watcher.removePath(testFileName)); |
| QVERIFY(QFile::remove(secondFileName)); |
| |
| timer.start(3000); |
| eventLoop.exec(); |
| QCOMPARE(fileChangedSpy.count(), 0); |
| QCOMPARE(dirChangedSpy.count(), 1); |
| |
| // QTBUG-61792, removal should succeed (bug on Windows which uses one change |
| // notification per directory). |
| QVERIFY(watcher.removePath(testDir.absolutePath())); |
| |
| QVERIFY(temporaryDir.rmdir(testDirName)); |
| } |
| #endif // QT_BUILD_INTERNAL |
| |
| void tst_QFileSystemWatcher::nonExistingFile() |
| { |
| // Don't crash... |
| QFileSystemWatcher watcher; |
| QVERIFY(!watcher.addPath("file_that_does_not_exist.txt")); |
| |
| // Test that the paths returned in error aren't messed with |
| QCOMPARE(watcher.addPaths(QStringList() << "../..//./does-not-exist"), |
| QStringList() << "../..//./does-not-exist"); |
| |
| // empty path is not actually a failure |
| QCOMPARE(watcher.addPaths(QStringList() << QString()), QStringList()); |
| |
| // empty path is not actually a failure |
| QCOMPARE(watcher.removePaths(QStringList() << QString()), QStringList()); |
| } |
| |
| void tst_QFileSystemWatcher::removeFileAndUnWatch() |
| { |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| |
| const QString filename = temporaryDirectory.path() + QStringLiteral("/foo.txt"); |
| |
| QFileSystemWatcher watcher; |
| |
| { |
| QFile testFile(filename); |
| QVERIFY2(testFile.open(QIODevice::WriteOnly), |
| qPrintable(QString::fromLatin1("Cannot open %1 for writing: %2").arg(filename, testFile.errorString()))); |
| testFile.close(); |
| } |
| QVERIFY(watcher.addPath(filename)); |
| |
| QFile::remove(filename); |
| /* There are potential race conditions here; the watcher thread might remove the file from its list |
| * before the call to watcher.removePath(), which then fails. When that happens, the auto-signal |
| * notification to remove the file from the watcher's main list will not be delivered before the next |
| * event loop such that the call to watcher.addPath() fails since the file is still in the main list. */ |
| if (!watcher.removePath(filename)) |
| QSKIP("Skipping remaining test due to race condition."); |
| |
| { |
| QFile testFile(filename); |
| QVERIFY2(testFile.open(QIODevice::WriteOnly), |
| qPrintable(QString::fromLatin1("Cannot open %1 for writing: %2").arg(filename, testFile.errorString()))); |
| testFile.close(); |
| } |
| QVERIFY(watcher.addPath(filename)); |
| } |
| |
| class SomeSingleton : public QObject |
| { |
| public: |
| SomeSingleton() : mFsWatcher(new QFileSystemWatcher(this)) { mFsWatcher->addPath(QLatin1String("/usr/lib"));} |
| void bla() const {} |
| QFileSystemWatcher* mFsWatcher; |
| }; |
| |
| Q_GLOBAL_STATIC(SomeSingleton, someSingleton) |
| |
| // This is a regression test for QTBUG-15255, where a deadlock occurred if a |
| // QFileSystemWatcher was destroyed after the QCoreApplication instance had |
| // been destroyed. There are no explicit verification steps in this test -- |
| // it is sufficient that the test terminates. |
| void tst_QFileSystemWatcher::destroyAfterQCoreApplication() |
| { |
| someSingleton()->bla(); |
| QTest::qWait(30); |
| } |
| |
| #ifdef QT_BUILD_INTERNAL |
| // regression test for QTBUG2331. |
| // essentially, on windows, directories were not unwatched after being deleted |
| // from the disk, causing all sorts of interesting problems. |
| void tst_QFileSystemWatcher::QTBUG2331() |
| { |
| QFETCH(QString, backend); |
| |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| QFileSystemWatcher watcher; |
| watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend); |
| QVERIFY(watcher.addPath(temporaryDirectory.path())); |
| |
| // watch signal |
| QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::directoryChanged); |
| QVERIFY(changedSpy.isValid()); |
| |
| // remove directory, we should get one change signal, and we should no longer |
| // be watching the directory. |
| QVERIFY(temporaryDirectory.remove()); |
| QTRY_COMPARE(changedSpy.count(), 1); |
| QCOMPARE(watcher.directories(), QStringList()); |
| } |
| #endif // QT_BUILD_INTERNAL |
| |
| class SignalReceiver : public QObject |
| { |
| Q_OBJECT |
| public: |
| SignalReceiver(const QDir &moveSrcDir, |
| const QString &moveDestination, |
| QFileSystemWatcher *watcher, |
| QObject *parent = 0) |
| : QObject(parent), |
| added(false), |
| moveSrcDir(moveSrcDir), |
| moveDestination(QDir(moveDestination)), |
| watcher(watcher) |
| {} |
| |
| public slots: |
| void fileChanged(const QString &path) |
| { |
| QFileInfo finfo(path); |
| |
| QCOMPARE(finfo.absolutePath(), moveSrcDir.absolutePath()); |
| |
| if (!added) { |
| foreach (const QFileInfo &fi, moveDestination.entryInfoList(QDir::Files | QDir::NoSymLinks)) |
| watcher->addPath(fi.absoluteFilePath()); |
| added = true; |
| } |
| } |
| |
| private: |
| bool added; |
| QDir moveSrcDir; |
| QDir moveDestination; |
| QFileSystemWatcher *watcher; |
| }; |
| |
| // regression test for QTBUG-33211. |
| // using inotify backend if a file is moved and then added to the watcher |
| // before all the fileChanged signals are emitted the remaining signals are |
| // emitted with the destination path instead of the starting path |
| void tst_QFileSystemWatcher::signalsEmittedAfterFileMoved() |
| { |
| const int fileCount = 10; |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| |
| QDir testDir(temporaryDirectory.path()); |
| QVERIFY(testDir.mkdir("movehere")); |
| QString movePath = testDir.filePath("movehere"); |
| |
| for (int i = 0; i < fileCount; ++i) { |
| const QByteArray iB = QByteArray::number(i); |
| QFile f(testDir.filePath(QLatin1String("test") + QString::fromLatin1(iB) + QLatin1String(".txt"))); |
| QVERIFY(f.open(QIODevice::WriteOnly)); |
| f.write(QByteArray("i am ") + iB); |
| f.close(); |
| } |
| |
| QFileSystemWatcher watcher; |
| QVERIFY(watcher.addPath(testDir.path())); |
| QVERIFY(watcher.addPath(movePath)); |
| |
| // add files to watcher |
| QFileInfoList files = testDir.entryInfoList(QDir::Files | QDir::NoSymLinks); |
| QCOMPARE(files.size(), fileCount); |
| foreach (const QFileInfo &finfo, files) |
| QVERIFY(watcher.addPath(finfo.absoluteFilePath())); |
| |
| // create the signal receiver |
| SignalReceiver signalReceiver(testDir, movePath, &watcher); |
| connect(&watcher, SIGNAL(fileChanged(QString)), &signalReceiver, SLOT(fileChanged(QString))); |
| |
| // watch signals |
| FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnFileChanged); |
| QCOMPARE(changedSpy.count(), 0); |
| |
| // move files to second directory |
| foreach (const QFileInfo &finfo, files) |
| QVERIFY(testDir.rename(finfo.fileName(), QString("movehere/%2").arg(finfo.fileName()))); |
| |
| QCoreApplication::processEvents(); |
| QVERIFY2(changedSpy.count() <= fileCount, changedSpy.receivedFilesMessage()); |
| QTRY_COMPARE(changedSpy.count(), fileCount); |
| } |
| |
| void tst_QFileSystemWatcher::watchUnicodeCharacters() |
| { |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| |
| QDir testDir(temporaryDirectory.path()); |
| const QString subDir(QString::fromLatin1("caf\xe9")); |
| QVERIFY(testDir.mkdir(subDir)); |
| testDir = QDir(temporaryDirectory.path() + QDir::separator() + subDir); |
| |
| QFileSystemWatcher watcher; |
| QVERIFY(watcher.addPath(testDir.path())); |
| |
| FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged); |
| QCOMPARE(changedSpy.count(), 0); |
| QVERIFY(testDir.mkdir("creme")); |
| QTRY_COMPARE(changedSpy.count(), 1); |
| } |
| |
| #if defined(Q_OS_WIN) && !defined(Q_OS_WINRT) |
| void tst_QFileSystemWatcher::watchDirectoryAttributeChanges() |
| { |
| QTemporaryDir temporaryDirectory(m_tempDirPattern); |
| QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString())); |
| |
| QDir testDir(temporaryDirectory.path()); |
| const QString subDir(QString::fromLatin1("attrib_test")); |
| QVERIFY(testDir.mkdir(subDir)); |
| testDir = QDir(temporaryDirectory.path() + QDir::separator() + subDir); |
| |
| QFileSystemWatcher watcher; |
| QVERIFY(watcher.addPath(temporaryDirectory.path())); |
| FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged); |
| QCOMPARE(changedSpy.count(), 0); |
| QVERIFY(SetFileAttributes(reinterpret_cast<LPCWSTR>(testDir.absolutePath().utf16()), FILE_ATTRIBUTE_HIDDEN) != 0); |
| QTRY_COMPARE(changedSpy.count(), 1); |
| QVERIFY(SetFileAttributes(reinterpret_cast<LPCWSTR>(testDir.absolutePath().utf16()), FILE_ATTRIBUTE_NORMAL) != 0); |
| QTRY_COMPARE(changedSpy.count(), 2); |
| } |
| #endif |
| |
| QTEST_MAIN(tst_QFileSystemWatcher) |
| #include "tst_qfilesystemwatcher.moc" |