init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
767
Telegram/ThirdParty/kcoreaddons/autotests/kdirwatch_unittest.cpp
vendored
Normal file
767
Telegram/ThirdParty/kcoreaddons/autotests/kdirwatch_unittest.cpp
vendored
Normal file
@@ -0,0 +1,767 @@
|
||||
/*
|
||||
This file is part of the KDE libraries
|
||||
|
||||
SPDX-FileCopyrightText: 2009 David Faure <faure@kde.org>
|
||||
SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include <kdirwatch.h>
|
||||
#include <kdirwatch_p.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QSignalSpy>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
#include <QThread>
|
||||
#include <sys/stat.h>
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <unistd.h> // ::link()
|
||||
#endif
|
||||
|
||||
#include "kcoreaddons_debug.h"
|
||||
#include "kdirwatch_test_utils.h"
|
||||
|
||||
using namespace KDirWatchTestUtils;
|
||||
|
||||
// Debugging notes: to see which inotify signals are emitted, either set s_verboseDebug=true
|
||||
// at the top of kdirwatch.cpp, or use the command-line tool "inotifywait -m /path"
|
||||
|
||||
class KDirWatch_UnitTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
KDirWatch_UnitTest()
|
||||
{
|
||||
// Speed up the test by making the kdirwatch timer (to compress changes) faster
|
||||
qputenv("KDIRWATCH_POLLINTERVAL", "50");
|
||||
qputenv("KDIRWATCH_METHOD", KDIRWATCH_TEST_METHOD);
|
||||
s_staticObjectUsingSelf();
|
||||
|
||||
m_path = m_tempDir.path() + QLatin1Char('/');
|
||||
KDirWatch *dirW = &s_staticObject()->m_dirWatch;
|
||||
m_stat = dirW->internalMethod() == KDirWatch::Stat;
|
||||
m_slow = (dirW->internalMethod() == KDirWatch::FAM || m_stat);
|
||||
qCDebug(KCOREADDONS_DEBUG) << "Using method" << methodToString(dirW->internalMethod());
|
||||
}
|
||||
|
||||
private Q_SLOTS: // test methods
|
||||
void initTestCase()
|
||||
{
|
||||
QFileInfo pathInfo(m_path);
|
||||
QVERIFY(pathInfo.isDir() && pathInfo.isWritable());
|
||||
|
||||
// By creating the files upfront, we save waiting a full second for an mtime change
|
||||
createFile(m_path + QLatin1String("ExistingFile"));
|
||||
createFile(m_path + QLatin1String("TestFile"));
|
||||
createFile(m_path + QLatin1String("nested_0"));
|
||||
createFile(m_path + QLatin1String("nested_1"));
|
||||
|
||||
s_staticObject()->m_dirWatch.addFile(m_path + QLatin1String("ExistingFile"));
|
||||
}
|
||||
void touchOneFile();
|
||||
void touch1000Files();
|
||||
void watchAndModifyOneFile();
|
||||
void removeAndReAdd();
|
||||
void watchNonExistent();
|
||||
void watchNonExistentWithSingleton();
|
||||
void testDelete();
|
||||
void testDeleteAndRecreateFile();
|
||||
void testDeleteAndRecreateDir();
|
||||
void testMoveTo();
|
||||
void nestedEventLoop();
|
||||
void testHardlinkChange();
|
||||
void stopAndRestart();
|
||||
void testRefcounting();
|
||||
void testRelativeRefcounting();
|
||||
void testMoveToThread();
|
||||
|
||||
protected Q_SLOTS: // internal slots
|
||||
void nestedEventLoopSlot();
|
||||
|
||||
private:
|
||||
QList<QVariantList> waitForDirtySignal(KDirWatch &watch, int expected);
|
||||
QList<QVariantList> waitForDeletedSignal(KDirWatch &watch, int expected);
|
||||
bool waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path);
|
||||
bool waitForRecreationSignal(KDirWatch &watch, const QString &path);
|
||||
bool verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath);
|
||||
QString createFile(int num);
|
||||
void createFile(const QString &file)
|
||||
{
|
||||
KDirWatchTestUtils::createFile(file, m_slow);
|
||||
}
|
||||
void removeFile(int num);
|
||||
void appendToFile(const QString &path);
|
||||
void appendToFile(int num);
|
||||
|
||||
QTemporaryDir m_tempDir;
|
||||
QString m_path;
|
||||
bool m_slow;
|
||||
bool m_stat;
|
||||
};
|
||||
|
||||
QTEST_MAIN(KDirWatch_UnitTest)
|
||||
|
||||
static const int s_maxTries = 50;
|
||||
|
||||
// helper method: create a file (identified by number)
|
||||
QString KDirWatch_UnitTest::createFile(int num)
|
||||
{
|
||||
const QString fileName = QLatin1String(s_filePrefix) + QString::number(num);
|
||||
KDirWatchTestUtils::createFile(m_path + fileName, m_slow);
|
||||
return m_path + fileName;
|
||||
}
|
||||
|
||||
// helper method: delete a file (identified by number)
|
||||
void KDirWatch_UnitTest::removeFile(int num)
|
||||
{
|
||||
const QString fileName = QLatin1String(s_filePrefix) + QString::number(num);
|
||||
QFile::remove(m_path + fileName);
|
||||
}
|
||||
|
||||
// helper method: modifies a file
|
||||
void KDirWatch_UnitTest::appendToFile(const QString &path)
|
||||
{
|
||||
QVERIFY(QFile::exists(path));
|
||||
waitUntilMTimeChange(path);
|
||||
|
||||
QFile file(path);
|
||||
QVERIFY(file.open(QIODevice::Append | QIODevice::WriteOnly));
|
||||
file.write(QByteArray("foobar"));
|
||||
file.close();
|
||||
}
|
||||
|
||||
// helper method: modifies a file (identified by number)
|
||||
void KDirWatch_UnitTest::appendToFile(int num)
|
||||
{
|
||||
const QString fileName = QLatin1String(s_filePrefix) + QString::number(num);
|
||||
appendToFile(m_path + fileName);
|
||||
}
|
||||
|
||||
static QString removeTrailingSlash(const QString &path)
|
||||
{
|
||||
if (path.endsWith(QLatin1Char('/'))) {
|
||||
return path.left(path.length() - 1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// helper method
|
||||
QList<QVariantList> KDirWatch_UnitTest::waitForDirtySignal(KDirWatch &watch, int expected)
|
||||
{
|
||||
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
|
||||
int numTries = 0;
|
||||
// Give time for KDirWatch to notify us
|
||||
while (spyDirty.count() < expected) {
|
||||
if (++numTries > s_maxTries) {
|
||||
qWarning() << "Timeout waiting for KDirWatch. Got" << spyDirty.count() << "dirty() signals, expected" << expected;
|
||||
return std::move(spyDirty);
|
||||
}
|
||||
spyDirty.wait(50);
|
||||
}
|
||||
return std::move(spyDirty);
|
||||
}
|
||||
|
||||
bool KDirWatch_UnitTest::waitForOneSignal(KDirWatch &watch, const char *sig, const QString &path)
|
||||
{
|
||||
const QString expectedPath = removeTrailingSlash(path);
|
||||
while (true) {
|
||||
QSignalSpy spyDirty(&watch, sig);
|
||||
int numTries = 0;
|
||||
// Give time for KDirWatch to notify us
|
||||
while (spyDirty.isEmpty()) {
|
||||
if (++numTries > s_maxTries) {
|
||||
qWarning() << "Timeout waiting for KDirWatch signal" << QByteArray(sig).mid(1) << "(" << path << ")";
|
||||
return false;
|
||||
}
|
||||
spyDirty.wait(50);
|
||||
}
|
||||
return verifySignalPath(spyDirty, sig, expectedPath);
|
||||
}
|
||||
}
|
||||
|
||||
bool KDirWatch_UnitTest::verifySignalPath(QSignalSpy &spy, const char *sig, const QString &expectedPath)
|
||||
{
|
||||
for (int i = 0; i < spy.count(); ++i) {
|
||||
const QString got = spy[i][0].toString();
|
||||
if (got == expectedPath) {
|
||||
return true;
|
||||
}
|
||||
if (got.startsWith(expectedPath + QLatin1Char('/'))) {
|
||||
qCDebug(KCOREADDONS_DEBUG) << "Ignoring (inotify) notification of" << (sig + 1) << '(' << got << ')';
|
||||
continue;
|
||||
}
|
||||
qWarning() << "Expected" << sig << '(' << expectedPath << ')' << "but got" << sig << '(' << got << ')';
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool KDirWatch_UnitTest::waitForRecreationSignal(KDirWatch &watch, const QString &path)
|
||||
{
|
||||
// When watching for a deleted + created signal pair, the two might come so close that
|
||||
// using waitForOneSignal will miss the created signal. This function monitors both all
|
||||
// the time to ensure both are received.
|
||||
//
|
||||
// In addition, it allows dirty() to be emitted (for that same path) as an alternative
|
||||
|
||||
const QString expectedPath = removeTrailingSlash(path);
|
||||
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
|
||||
QSignalSpy spyDeleted(&watch, &KDirWatch::deleted);
|
||||
QSignalSpy spyCreated(&watch, &KDirWatch::created);
|
||||
|
||||
int numTries = 0;
|
||||
while (spyDeleted.isEmpty() && spyDirty.isEmpty()) {
|
||||
if (++numTries > s_maxTries) {
|
||||
return false;
|
||||
}
|
||||
spyDeleted.wait(50);
|
||||
while (!spyDirty.isEmpty()) {
|
||||
if (spyDirty.at(0).at(0).toString() != expectedPath) { // unrelated
|
||||
spyDirty.removeFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!spyDirty.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't bother waiting for the created signal if the signal spy already received a signal.
|
||||
if (spyCreated.isEmpty() && !spyCreated.wait(50 * s_maxTries)) {
|
||||
qWarning() << "Timeout waiting for KDirWatch signal created(QString) (" << path << ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
return verifySignalPath(spyDeleted, "deleted(QString)", expectedPath) && verifySignalPath(spyCreated, "created(QString)", expectedPath);
|
||||
}
|
||||
|
||||
QList<QVariantList> KDirWatch_UnitTest::waitForDeletedSignal(KDirWatch &watch, int expected)
|
||||
{
|
||||
QSignalSpy spyDeleted(&watch, &KDirWatch::created);
|
||||
int numTries = 0;
|
||||
// Give time for KDirWatch to notify us
|
||||
while (spyDeleted.count() < expected) {
|
||||
if (++numTries > s_maxTries) {
|
||||
qWarning() << "Timeout waiting for KDirWatch. Got" << spyDeleted.count() << "deleted() signals, expected" << expected;
|
||||
return std::move(spyDeleted);
|
||||
}
|
||||
spyDeleted.wait(50);
|
||||
}
|
||||
return std::move(spyDeleted);
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::touchOneFile() // watch a dir, create a file in it
|
||||
{
|
||||
KDirWatch watch;
|
||||
watch.addDir(m_path);
|
||||
watch.startScan();
|
||||
|
||||
waitUntilMTimeChange(m_path);
|
||||
|
||||
// dirty(the directory) should be emitted.
|
||||
QSignalSpy spyCreated(&watch, &KDirWatch::created);
|
||||
createFile(0);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
|
||||
QCOMPARE(spyCreated.count(), 0); // "This is not emitted when creating a file is created in a watched directory."
|
||||
|
||||
removeFile(0);
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::touch1000Files()
|
||||
{
|
||||
KDirWatch watch;
|
||||
watch.addDir(m_path);
|
||||
watch.startScan();
|
||||
|
||||
waitUntilMTimeChange(m_path);
|
||||
|
||||
const int fileCount = 100;
|
||||
for (int i = 0; i < fileCount; ++i) {
|
||||
createFile(i);
|
||||
}
|
||||
|
||||
QList<QVariantList> spy = waitForDirtySignal(watch, fileCount);
|
||||
if (watch.internalMethod() == KDirWatch::INotify) {
|
||||
QVERIFY(spy.count() >= fileCount);
|
||||
} else {
|
||||
// More stupid backends just see one mtime change on the directory
|
||||
QVERIFY(spy.count() >= 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < fileCount; ++i) {
|
||||
removeFile(i);
|
||||
}
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::watchAndModifyOneFile() // watch a specific file, and modify it
|
||||
{
|
||||
KDirWatch watch;
|
||||
const QString existingFile = m_path + QLatin1String("ExistingFile");
|
||||
watch.addFile(existingFile);
|
||||
watch.startScan();
|
||||
if (m_slow) {
|
||||
waitUntilNewSecond();
|
||||
}
|
||||
appendToFile(existingFile);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile));
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::removeAndReAdd()
|
||||
{
|
||||
KDirWatch watch;
|
||||
watch.addDir(m_path);
|
||||
// This triggers bug #374075.
|
||||
watch.addDir(QStringLiteral(":/kio5/newfile-templates"));
|
||||
watch.startScan();
|
||||
if (watch.internalMethod() != KDirWatch::INotify) {
|
||||
waitUntilNewSecond(); // necessary for mtime checks in scanEntry
|
||||
}
|
||||
createFile(0);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
|
||||
|
||||
// Just like KDirLister does: remove the watch, then re-add it.
|
||||
watch.removeDir(m_path);
|
||||
watch.addDir(m_path);
|
||||
if (watch.internalMethod() != KDirWatch::INotify) {
|
||||
waitUntilMTimeChange(m_path); // necessary for FAM and QFSWatcher
|
||||
}
|
||||
createFile(1);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::watchNonExistent()
|
||||
{
|
||||
KDirWatch watch;
|
||||
// Watch "subdir", that doesn't exist yet
|
||||
const QString subdir = m_path + QLatin1String("subdir");
|
||||
QVERIFY(!QFile::exists(subdir));
|
||||
watch.addDir(subdir);
|
||||
watch.startScan();
|
||||
|
||||
if (m_slow) {
|
||||
waitUntilNewSecond();
|
||||
}
|
||||
|
||||
// Now create it, KDirWatch should emit created()
|
||||
qCDebug(KCOREADDONS_DEBUG) << "Creating" << subdir;
|
||||
QDir().mkdir(subdir);
|
||||
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), subdir));
|
||||
|
||||
KDirWatch::statistics();
|
||||
|
||||
// Play with addDir/removeDir, just for fun
|
||||
watch.addDir(subdir);
|
||||
watch.removeDir(subdir);
|
||||
watch.addDir(subdir);
|
||||
|
||||
// Now watch files that don't exist yet
|
||||
const QString file = subdir + QLatin1String("/0");
|
||||
watch.addFile(file); // doesn't exist yet
|
||||
const QString file1 = subdir + QLatin1String("/1");
|
||||
watch.addFile(file1); // doesn't exist yet
|
||||
watch.removeFile(file1); // forget it again
|
||||
|
||||
KDirWatch::statistics();
|
||||
|
||||
QVERIFY(!QFile::exists(file));
|
||||
// Now create it, KDirWatch should emit created
|
||||
qCDebug(KCOREADDONS_DEBUG) << "Creating" << file;
|
||||
createFile(file);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), file));
|
||||
|
||||
appendToFile(file);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file));
|
||||
|
||||
// Create the file after all; we're not watching for it, but the dir will emit dirty
|
||||
createFile(file1);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), subdir));
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::watchNonExistentWithSingleton()
|
||||
{
|
||||
const QString file = QLatin1String("/root/.ssh/authorized_keys");
|
||||
KDirWatch::self()->addFile(file);
|
||||
// When running this test in KDIRWATCH_METHOD=QFSWatch, or when FAM is not available
|
||||
// and we fallback to qfswatch when inotify fails above, we end up creating the fsWatch
|
||||
// in the kdirwatch singleton. Bug 261541 discovered that Qt hanged when deleting fsWatch
|
||||
// once QCoreApp was gone, this is what this test is about.
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testDelete()
|
||||
{
|
||||
const QString file1 = m_path + QLatin1String("del");
|
||||
if (!QFile::exists(file1)) {
|
||||
createFile(file1);
|
||||
}
|
||||
waitUntilMTimeChange(file1);
|
||||
|
||||
// Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed)
|
||||
KDirWatch watch;
|
||||
watch.addFile(file1);
|
||||
|
||||
KDirWatch::statistics();
|
||||
|
||||
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
|
||||
QFile::remove(file1);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), file1));
|
||||
QTest::qWait(40); // just in case delayed processing would emit it
|
||||
QCOMPARE(spyDirty.count(), 0);
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testDeleteAndRecreateFile() // Useful for /etc/localtime for instance
|
||||
{
|
||||
const QString subdir = m_path + QLatin1String("subdir");
|
||||
QDir().mkdir(subdir);
|
||||
const QString file1 = subdir + QLatin1String("/1");
|
||||
if (!QFile::exists(file1)) {
|
||||
createFile(file1);
|
||||
}
|
||||
waitUntilMTimeChange(file1);
|
||||
|
||||
// Watch the file, then delete it, KDirWatch will emit deleted (and possibly dirty for the dir, if mtime changed)
|
||||
KDirWatch watch;
|
||||
watch.addFile(file1);
|
||||
|
||||
// Make sure this even works multiple times, as needed for ksycoca
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (m_slow || watch.internalMethod() == KDirWatch::QFSWatch) {
|
||||
waitUntilNewSecond();
|
||||
}
|
||||
|
||||
qCDebug(KCOREADDONS_DEBUG) << "Attempt #" << (i + 1) << "removing+recreating" << file1;
|
||||
|
||||
// When watching for a deleted + created signal pair, the two might come so close that
|
||||
// using waitForOneSignal will miss the created signal. This function monitors both all
|
||||
// the time to ensure both are received.
|
||||
//
|
||||
// In addition, allow dirty() to be emitted (for that same path) as an alternative
|
||||
|
||||
const QString expectedPath = file1;
|
||||
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
|
||||
QSignalSpy spyDeleted(&watch, &KDirWatch::deleted);
|
||||
QSignalSpy spyCreated(&watch, &KDirWatch::created);
|
||||
|
||||
// WHEN
|
||||
QFile::remove(file1);
|
||||
// And recreate immediately, to try and fool KDirWatch with unchanged ctime/mtime ;)
|
||||
// (This emulates the /etc/localtime case)
|
||||
createFile(file1);
|
||||
|
||||
// THEN
|
||||
int numTries = 0;
|
||||
while (spyDeleted.isEmpty() && spyDirty.isEmpty()) {
|
||||
if (++numTries > s_maxTries) {
|
||||
QFAIL("Failed to detect file deletion and recreation through either a deleted/created signal pair or through a dirty signal!");
|
||||
return;
|
||||
}
|
||||
spyDeleted.wait(50);
|
||||
while (!spyDirty.isEmpty()) {
|
||||
if (spyDirty.at(0).at(0).toString() != expectedPath) { // unrelated
|
||||
spyDirty.removeFirst();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!spyDirty.isEmpty()) {
|
||||
continue; // all ok
|
||||
}
|
||||
|
||||
// Don't bother waiting for the created signal if the signal spy already received a signal.
|
||||
if (spyCreated.isEmpty() && !spyCreated.wait(50 * s_maxTries)) {
|
||||
qWarning() << "Timeout waiting for KDirWatch signal created(QString) (" << expectedPath << ")";
|
||||
QFAIL("Timeout waiting for KDirWatch signal created, after deleted was emitted");
|
||||
return;
|
||||
}
|
||||
|
||||
QVERIFY(verifySignalPath(spyDeleted, "deleted(QString)", expectedPath) && verifySignalPath(spyCreated, "created(QString)", expectedPath));
|
||||
}
|
||||
|
||||
waitUntilMTimeChange(file1);
|
||||
|
||||
appendToFile(file1);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1));
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testDeleteAndRecreateDir()
|
||||
{
|
||||
// Like KDirModelTest::testOverwriteFileWithDir does at the end.
|
||||
// The linux-2.6.31 bug made kdirwatch emit deletion signals about the -new- dir!
|
||||
QTemporaryDir *tempDir1 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("olddir-"));
|
||||
KDirWatch watch;
|
||||
const QString path1 = tempDir1->path() + QLatin1Char('/');
|
||||
watch.addDir(path1);
|
||||
|
||||
delete tempDir1;
|
||||
QTemporaryDir *tempDir2 = new QTemporaryDir(QDir::tempPath() + QLatin1Char('/') + QLatin1String("newdir-"));
|
||||
const QString path2 = tempDir2->path() + QLatin1Char('/');
|
||||
watch.addDir(path2);
|
||||
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), path1));
|
||||
|
||||
delete tempDir2;
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testMoveTo()
|
||||
{
|
||||
// This reproduces the famous digikam crash, #222974
|
||||
// A watched file was being rewritten (overwritten by ksavefile),
|
||||
// which gives inotify notifications "moved_to" followed by "delete_self"
|
||||
//
|
||||
// What happened then was that the delayed slotRescan
|
||||
// would adjust things, making it status==Normal but the entry was
|
||||
// listed as a "non-existent sub-entry" for the parent directory.
|
||||
// That's inconsistent, and after removeFile() a dangling sub-entry would be left.
|
||||
|
||||
// Initial data: creating file subdir/1
|
||||
const QString file1 = m_path + QLatin1String("moveTo");
|
||||
createFile(file1);
|
||||
|
||||
KDirWatch watch;
|
||||
watch.addDir(m_path);
|
||||
watch.addFile(file1);
|
||||
watch.startScan();
|
||||
|
||||
if (watch.internalMethod() != KDirWatch::INotify) {
|
||||
waitUntilMTimeChange(m_path);
|
||||
}
|
||||
|
||||
// Atomic rename of "temp" to "file1", much like KAutoSave would do when saving file1 again
|
||||
// ### TODO: this isn't an atomic rename anymore. We need ::rename for that, or API from Qt.
|
||||
const QString filetemp = m_path + QLatin1String("temp");
|
||||
createFile(filetemp);
|
||||
QFile::remove(file1);
|
||||
QVERIFY(QFile::rename(filetemp, file1)); // overwrite file1 with the tempfile
|
||||
qCDebug(KCOREADDONS_DEBUG) << "Overwrite file1 with tempfile";
|
||||
|
||||
QSignalSpy spyCreated(&watch, &KDirWatch::created);
|
||||
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
|
||||
|
||||
// Getting created() on an unwatched file is an inotify bonus, it's not part of the requirements.
|
||||
if (watch.internalMethod() == KDirWatch::INotify) {
|
||||
QCOMPARE(spyCreated.count(), 1);
|
||||
QCOMPARE(spyCreated[0][0].toString(), file1);
|
||||
|
||||
QCOMPARE(spyDirty.size(), 2);
|
||||
QCOMPARE(spyDirty[1][0].toString(), filetemp);
|
||||
}
|
||||
|
||||
// make sure we're still watching it
|
||||
appendToFile(file1);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1));
|
||||
|
||||
watch.removeFile(file1); // now we remove it
|
||||
|
||||
// Just touch another file to trigger a findSubEntry - this where the crash happened
|
||||
waitUntilMTimeChange(m_path);
|
||||
createFile(filetemp);
|
||||
#ifdef Q_OS_WIN
|
||||
if (watch.internalMethod() == KDirWatch::QFSWatch) {
|
||||
QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue);
|
||||
}
|
||||
#endif
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::nestedEventLoop() // #220153: watch two files, and modify 2nd while in slot for 1st
|
||||
{
|
||||
KDirWatch watch;
|
||||
|
||||
const QString file0 = m_path + QLatin1String("nested_0");
|
||||
watch.addFile(file0);
|
||||
const QString file1 = m_path + QLatin1String("nested_1");
|
||||
watch.addFile(file1);
|
||||
watch.startScan();
|
||||
|
||||
if (m_slow) {
|
||||
waitUntilNewSecond();
|
||||
}
|
||||
|
||||
appendToFile(file0);
|
||||
|
||||
// use own spy, to connect it before nestedEventLoopSlot, otherwise it reverses order
|
||||
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
|
||||
connect(&watch, &KDirWatch::dirty, this, &KDirWatch_UnitTest::nestedEventLoopSlot);
|
||||
waitForDirtySignal(watch, 1);
|
||||
QVERIFY(spyDirty.count() >= 2);
|
||||
QCOMPARE(spyDirty[0][0].toString(), file0);
|
||||
QCOMPARE(spyDirty[spyDirty.count() - 1][0].toString(), file1);
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::nestedEventLoopSlot()
|
||||
{
|
||||
const KDirWatch *const_watch = qobject_cast<const KDirWatch *>(sender());
|
||||
KDirWatch *watch = const_cast<KDirWatch *>(const_watch);
|
||||
// let's not come in this slot again
|
||||
disconnect(watch, &KDirWatch::dirty, this, &KDirWatch_UnitTest::nestedEventLoopSlot);
|
||||
|
||||
const QString file1 = m_path + QLatin1String("nested_1");
|
||||
appendToFile(file1);
|
||||
// The nested event processing here was from a messagebox in #220153
|
||||
QList<QVariantList> spy = waitForDirtySignal(*watch, 1);
|
||||
QVERIFY(spy.count() >= 1);
|
||||
QCOMPARE(spy[spy.count() - 1][0].toString(), file1);
|
||||
|
||||
// Now the user pressed reload...
|
||||
const QString file0 = m_path + QLatin1String("nested_0");
|
||||
watch->removeFile(file0);
|
||||
watch->addFile(file0);
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testHardlinkChange()
|
||||
{
|
||||
#ifdef Q_OS_UNIX
|
||||
|
||||
// The unittest for the "detecting hardlink change to /etc/localtime" problem
|
||||
// described on kde-core-devel (2009-07-03).
|
||||
// It shows that watching a specific file doesn't inform us that the file is
|
||||
// being recreated. Better watch the directory, for that.
|
||||
// Well, it works with inotify (and fam - which uses inotify I guess?)
|
||||
|
||||
const QString existingFile = m_path + QLatin1String("ExistingFile");
|
||||
KDirWatch watch;
|
||||
watch.addFile(existingFile);
|
||||
watch.startScan();
|
||||
|
||||
QFile::remove(existingFile);
|
||||
const QString testFile = m_path + QLatin1String("TestFile");
|
||||
QVERIFY(::link(QFile::encodeName(testFile).constData(), QFile::encodeName(existingFile).constData()) == 0); // make ExistingFile "point" to TestFile
|
||||
QVERIFY(QFile::exists(existingFile));
|
||||
|
||||
QVERIFY(waitForRecreationSignal(watch, existingFile));
|
||||
|
||||
// The mtime of the existing file is the one of "TestFile", so it's old.
|
||||
// We won't detect the change then, if we use that as baseline for waiting.
|
||||
// We really need msec granularity, but that requires using statx which isn't available everywhere...
|
||||
waitUntilNewSecond();
|
||||
appendToFile(existingFile);
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), existingFile));
|
||||
#else
|
||||
QSKIP("Unix-specific");
|
||||
#endif
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::stopAndRestart()
|
||||
{
|
||||
KDirWatch watch;
|
||||
watch.addDir(m_path);
|
||||
watch.startScan();
|
||||
|
||||
waitUntilMTimeChange(m_path);
|
||||
|
||||
watch.stopDirScan(m_path);
|
||||
|
||||
qCDebug(KCOREADDONS_DEBUG) << "create file 2 at" << QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
const QString file2 = createFile(2);
|
||||
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
|
||||
QTest::qWait(200);
|
||||
QCOMPARE(spyDirty.count(), 0); // suspended -> no signal
|
||||
|
||||
watch.restartDirScan(m_path);
|
||||
|
||||
QTest::qWait(200);
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
QCOMPARE(spyDirty.count(), 0); // as documented by restartDirScan: no signal
|
||||
// On Windows, however, signals will get emitted, due to the ifdef Q_OS_WIN in the timestamp
|
||||
// comparison ("trust QFSW since the mtime of dirs isn't modified")
|
||||
#endif
|
||||
|
||||
KDirWatch::statistics();
|
||||
|
||||
waitUntilMTimeChange(m_path); // necessary for the mtime comparison in scanEntry
|
||||
|
||||
qCDebug(KCOREADDONS_DEBUG) << "create file 3 at" << QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
const QString file3 = createFile(3);
|
||||
#ifdef Q_OS_WIN
|
||||
if (watch.internalMethod() == KDirWatch::QFSWatch) {
|
||||
QEXPECT_FAIL(nullptr, "QFSWatch fails here on Windows!", Continue);
|
||||
}
|
||||
#endif
|
||||
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
|
||||
|
||||
QFile::remove(file2);
|
||||
QFile::remove(file3);
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testRefcounting()
|
||||
{
|
||||
#if QT_CONFIG(cxx11_future)
|
||||
bool initialExists = false;
|
||||
bool secondExists = true; // the expectation is it will be set false
|
||||
auto thread = QThread::create([&] {
|
||||
QTemporaryDir dir;
|
||||
{
|
||||
KDirWatch watch;
|
||||
watch.addFile(dir.path());
|
||||
initialExists = KDirWatch::exists();
|
||||
} // out of scope, the internal private should have been unset
|
||||
secondExists = KDirWatch::exists();
|
||||
});
|
||||
thread->start();
|
||||
thread->wait();
|
||||
delete thread;
|
||||
QVERIFY(initialExists);
|
||||
QVERIFY(!secondExists);
|
||||
#endif
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testRelativeRefcounting()
|
||||
{
|
||||
// Relative files aren't supported but should still result in correct ref
|
||||
// handling. Specifically when watch1 gets destroyed it should take all
|
||||
// its entries with it regardless of whether the entry was well-formed
|
||||
KDirWatch watch0;
|
||||
|
||||
if (watch0.internalMethod() != KDirWatch::INotify) {
|
||||
// Only test on inotify. Otherwise Entry count expectations may diverge.
|
||||
return;
|
||||
}
|
||||
|
||||
const auto initialSize = watch0.d->m_mapEntries.size();
|
||||
{
|
||||
KDirWatch watch1;
|
||||
watch1.addFile(QStringLiteral("AVeryRelativePath.txt"));
|
||||
// NOTE: addFile actually adds two entires: one for '.' to watch for the appearance of the file and one for the file.
|
||||
QCOMPARE(watch0.d->m_mapEntries.size(), initialSize + 2);
|
||||
}
|
||||
// NOTE: we leak the directory entry from above but it has no clients so it's mostly harmless
|
||||
QCOMPARE(watch0.d->m_mapEntries.size(), initialSize + 1);
|
||||
}
|
||||
|
||||
void KDirWatch_UnitTest::testMoveToThread()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
{
|
||||
const QRegularExpression expression(QStringLiteral("KDirwatch is moving its thread. This is not supported at this time;.+"));
|
||||
QTest::ignoreMessage(QtCriticalMsg, expression);
|
||||
|
||||
auto watch = new KDirWatch;
|
||||
watch->addDir(dir.path());
|
||||
|
||||
auto thread = new QThread;
|
||||
watch->moveToThread(thread);
|
||||
thread->start();
|
||||
|
||||
waitUntilMTimeChange(dir.path());
|
||||
|
||||
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||
QObject::connect(thread, &QThread::finished, watch, &QObject::deleteLater);
|
||||
|
||||
thread->quit();
|
||||
thread->wait();
|
||||
}
|
||||
// trigger an event on the now deleted watch. This should not crash!
|
||||
const QString file = dir.path() + QLatin1String("/bar");
|
||||
createFile(file);
|
||||
waitUntilMTimeChange(file);
|
||||
}
|
||||
|
||||
#include "kdirwatch_unittest.moc"
|
||||
Reference in New Issue
Block a user