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 <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
#include <QThread>
#include <sys/stat.h>
#ifdef Q_OS_UNIX
#include <unistd.h>
#endif
#include "kcoreaddons_debug.h"
#include "kdirwatch_test_utils.h"
using namespace KDirWatchTestUtils;
class KDirWatch_UnitTest : public QObject
{
Q_OBJECT
public:
KDirWatch_UnitTest()
{
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 = m_stat;
qCDebug(KCOREADDONS_DEBUG) << "Using method" << methodToString(dirW->internalMethod());
}
private Q_SLOTS:
void initTestCase()
{
QFileInfo pathInfo(m_path);
QVERIFY(pathInfo.isDir() && pathInfo.isWritable());
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:
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;
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;
}
void KDirWatch_UnitTest::removeFile(int num)
{
const QString fileName = QLatin1String(s_filePrefix) + QString::number(num);
QFile::remove(m_path + fileName);
}
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();
}
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;
}
}
QList<QVariantList> KDirWatch_UnitTest::waitForDirtySignal(KDirWatch &watch, int expected)
{
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
int numTries = 0;
while (spyDirty.count() < expected) {
if (++numTries > s_maxTries) {
qWarning() << "Timeout waiting for KDirWatch. Got" << spyDirty.count() << "dirty() signals, expected" << expected;
return spyDirty;
}
spyDirty.wait(50);
}
return 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;
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)
{
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) {
spyDirty.removeFirst();
}
}
}
if (!spyDirty.isEmpty()) {
return true;
}
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;
while (spyDeleted.count() < expected) {
if (++numTries > s_maxTries) {
qWarning() << "Timeout waiting for KDirWatch. Got" << spyDeleted.count() << "deleted() signals, expected" << expected;
return spyDeleted;
}
spyDeleted.wait(50);
}
return spyDeleted;
}
void KDirWatch_UnitTest::touchOneFile()
{
KDirWatch watch;
watch.addDir(m_path);
watch.startScan();
waitUntilMTimeChange(m_path);
QSignalSpy spyCreated(&watch, &KDirWatch::created);
createFile(0);
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
QCOMPARE(spyCreated.count(), 0);
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 {
QVERIFY(spy.count() >= 1);
}
for (int i = 0; i < fileCount; ++i) {
removeFile(i);
}
}
void KDirWatch_UnitTest::watchAndModifyOneFile()
{
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);
watch.addDir(QStringLiteral(":/kio5/newfile-templates"));
watch.startScan();
if (watch.internalMethod() != KDirWatch::INotify) {
waitUntilNewSecond();
}
createFile(0);
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
watch.removeDir(m_path);
watch.addDir(m_path);
if (watch.internalMethod() != KDirWatch::INotify) {
waitUntilMTimeChange(m_path);
}
createFile(1);
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), m_path));
}
void KDirWatch_UnitTest::watchNonExistent()
{
KDirWatch watch;
const QString subdir = m_path + QLatin1String("subdir");
QVERIFY(!QFile::exists(subdir));
watch.addDir(subdir);
watch.startScan();
if (m_slow) {
waitUntilNewSecond();
}
qCDebug(KCOREADDONS_DEBUG) << "Creating" << subdir;
QDir().mkdir(subdir);
QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), subdir));
qCDebug(KCOREADDONS_DEBUG) << &watch;
watch.addDir(subdir);
watch.removeDir(subdir);
watch.addDir(subdir);
const QString file = subdir + QLatin1String("/0");
watch.addFile(file);
const QString file1 = subdir + QLatin1String("/1");
watch.addFile(file1);
watch.removeFile(file1);
qCDebug(KCOREADDONS_DEBUG) << &watch;
QVERIFY(!QFile::exists(file));
qCDebug(KCOREADDONS_DEBUG) << "Creating" << file;
createFile(file);
QVERIFY(waitForOneSignal(watch, SIGNAL(created(QString)), file));
appendToFile(file);
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file));
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);
}
void KDirWatch_UnitTest::testDelete()
{
const QString file1 = m_path + QLatin1String("del");
if (!QFile::exists(file1)) {
createFile(file1);
}
waitUntilMTimeChange(file1);
KDirWatch watch;
watch.addFile(file1);
qCDebug(KCOREADDONS_DEBUG) << &watch;
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
QFile::remove(file1);
QVERIFY(waitForOneSignal(watch, SIGNAL(deleted(QString)), file1));
QTest::qWait(40);
QCOMPARE(spyDirty.count(), 0);
}
void KDirWatch_UnitTest::testDeleteAndRecreateFile()
{
const QString subdir = m_path + QLatin1String("subdir");
QDir().mkdir(subdir);
const QString file1 = subdir + QLatin1String("/1");
if (!QFile::exists(file1)) {
createFile(file1);
}
waitUntilMTimeChange(file1);
KDirWatch watch;
watch.addFile(file1);
for (int i = 0; i < 5; ++i) {
if (m_slow || watch.internalMethod() == KDirWatch::QFSWatch) {
waitUntilNewSecond();
}
qCDebug(KCOREADDONS_DEBUG) << "Attempt #" << (i + 1) << "removing+recreating" << file1;
const QString expectedPath = file1;
QSignalSpy spyDirty(&watch, &KDirWatch::dirty);
QSignalSpy spyDeleted(&watch, &KDirWatch::deleted);
QSignalSpy spyCreated(&watch, &KDirWatch::created);
QFile::remove(file1);
createFile(file1);
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) {
spyDirty.removeFirst();
} else {
break;
}
}
}
if (!spyDirty.isEmpty()) {
continue;
}
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()
{
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()
{
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);
}
const QString filetemp = m_path + QLatin1String("temp");
createFile(filetemp);
QFile::remove(file1);
QVERIFY(QFile::rename(filetemp, file1));
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));
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);
}
appendToFile(file1);
QVERIFY(waitForOneSignal(watch, SIGNAL(dirty(QString)), file1));
watch.removeFile(file1);
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()
{
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);
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);
disconnect(watch, &KDirWatch::dirty, this, &KDirWatch_UnitTest::nestedEventLoopSlot);
const QString file1 = m_path + QLatin1String("nested_1");
appendToFile(file1);
QList<QVariantList> spy = waitForDirtySignal(*watch, 1);
QVERIFY(spy.count() >= 1);
QCOMPARE(spy[spy.count() - 1][0].toString(), file1);
const QString file0 = m_path + QLatin1String("nested_0");
watch->removeFile(file0);
watch->addFile(file0);
}
void KDirWatch_UnitTest::testHardlinkChange()
{
#ifdef Q_OS_UNIX
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);
QVERIFY(QFile::exists(existingFile));
QVERIFY(waitForRecreationSignal(watch, existingFile));
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);
watch.restartDirScan(m_path);
QTest::qWait(200);
#ifndef Q_OS_WIN
QCOMPARE(spyDirty.count(), 0);
#endif
qCDebug(KCOREADDONS_DEBUG) << &watch;
waitUntilMTimeChange(m_path);
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;
auto thread = QThread::create([&] {
QTemporaryDir dir;
{
KDirWatch watch;
watch.addFile(dir.path());
initialExists = KDirWatch::exists();
}
secondExists = KDirWatch::exists();
});
thread->start();
thread->wait();
delete thread;
QVERIFY(initialExists);
QVERIFY(!secondExists);
#endif
}
void KDirWatch_UnitTest::testRelativeRefcounting()
{
KDirWatch watch0;
if (watch0.internalMethod() != KDirWatch::INotify) {
return;
}
const auto initialSize = watch0.d->m_mapEntries.size();
{
KDirWatch watch1;
watch1.addFile(QStringLiteral("AVeryRelativePath.txt"));
QCOMPARE(watch0.d->m_mapEntries.size(), initialSize + 2);
}
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();
}
const QString file = dir.path() + QLatin1String("/bar");
createFile(file);
waitUntilMTimeChange(file);
}
#include "kdirwatch_unittest.moc"