File indexing completed on 2024-04-21 14:59:35

0001 /*
0002     This file is part of KDE
0003     SPDX-FileCopyrightText: 2006, 2008 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "fileundomanagertest.h"
0009 
0010 #include "../src/utils_p.h"
0011 
0012 #include "mockcoredelegateextensions.h"
0013 #include <kio/batchrenamejob.h>
0014 #include <kio/copyjob.h>
0015 #include <kio/deletejob.h>
0016 #include <kio/fileundomanager.h>
0017 #include <kio/mkdirjob.h>
0018 #include <kio/mkpathjob.h>
0019 #include <kio/paste.h>
0020 #include <kio/pastejob.h>
0021 #include <kio/restorejob.h>
0022 #include <kioglobal_p.h>
0023 #include <kprotocolinfo.h>
0024 
0025 #include <QApplication>
0026 #include <QClipboard>
0027 #include <QDateTime>
0028 #include <QDebug>
0029 #include <QDir>
0030 #include <QFileInfo>
0031 #include <QMimeData>
0032 #include <QSignalSpy>
0033 #include <QTest>
0034 #include <qplatformdefs.h>
0035 
0036 #include <KConfig>
0037 #include <KConfigGroup>
0038 #include <KUrlMimeData>
0039 
0040 #include <cerrno>
0041 #include <time.h>
0042 #ifdef Q_OS_WIN
0043 #include <sys/utime.h>
0044 #else
0045 #include <sys/time.h>
0046 #include <utime.h>
0047 #endif
0048 
0049 QTEST_MAIN(FileUndoManagerTest)
0050 
0051 using namespace KIO;
0052 
0053 static QString homeTmpDir()
0054 {
0055     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator();
0056 }
0057 static QString destDir()
0058 {
0059     return homeTmpDir() + "destdir/";
0060 }
0061 
0062 static QString srcFile()
0063 {
0064     return homeTmpDir() + "testfile";
0065 }
0066 static QString destFile()
0067 {
0068     return destDir() + "testfile";
0069 }
0070 
0071 #ifndef Q_OS_WIN
0072 static QString srcLink()
0073 {
0074     return homeTmpDir() + "symlink";
0075 }
0076 static QString destLink()
0077 {
0078     return destDir() + "symlink";
0079 }
0080 #endif
0081 
0082 static QString srcSubDir()
0083 {
0084     return homeTmpDir() + "subdir";
0085 }
0086 static QString destSubDir()
0087 {
0088     return destDir() + "subdir";
0089 }
0090 
0091 static QList<QUrl> sourceList()
0092 {
0093     QList<QUrl> lst;
0094     lst << QUrl::fromLocalFile(srcFile());
0095 #ifndef Q_OS_WIN
0096     lst << QUrl::fromLocalFile(srcLink());
0097 #endif
0098     return lst;
0099 }
0100 
0101 static void createTestFile(const QString &path, const char *contents)
0102 {
0103     QFile f(path);
0104     if (!f.open(QIODevice::WriteOnly)) {
0105         qFatal("Couldn't create %s", qPrintable(path));
0106     }
0107     f.write(QByteArray(contents));
0108     f.close();
0109 }
0110 
0111 static void createTestSymlink(const QString &path)
0112 {
0113     // Create symlink if it doesn't exist yet
0114     QT_STATBUF buf;
0115     if (QT_LSTAT(QFile::encodeName(path).constData(), &buf) != 0) {
0116         bool ok = KIOPrivate::createSymlink(QStringLiteral("/IDontExist"), path); // broken symlink
0117         if (!ok) {
0118             qFatal("couldn't create symlink: %s", strerror(errno));
0119         }
0120         QVERIFY(QT_LSTAT(QFile::encodeName(path).constData(), &buf) == 0);
0121         QVERIFY(Utils::isLinkMask(buf.st_mode));
0122     } else {
0123         QVERIFY(Utils::isLinkMask(buf.st_mode));
0124     }
0125     qDebug("symlink %s created", qPrintable(path));
0126     QVERIFY(QFileInfo(path).isSymLink());
0127 }
0128 
0129 static void checkTestDirectory(const QString &path)
0130 {
0131     QVERIFY(QFileInfo(path).isDir());
0132     QVERIFY(QFileInfo(path + "/fileindir").isFile());
0133 #ifndef Q_OS_WIN
0134     QVERIFY(QFileInfo(path + "/testlink").isSymLink());
0135 #endif
0136     QVERIFY(QFileInfo(path + "/dirindir").isDir());
0137     QVERIFY(QFileInfo(path + "/dirindir/nested").isFile());
0138 }
0139 
0140 static void createTestDirectory(const QString &path)
0141 {
0142     QDir dir;
0143     bool ok = dir.mkpath(path);
0144     if (!ok) {
0145         qFatal("couldn't create %s", qPrintable(path));
0146     }
0147     createTestFile(path + "/fileindir", "File in dir");
0148 #ifndef Q_OS_WIN
0149     createTestSymlink(path + "/testlink");
0150 #endif
0151     ok = dir.mkdir(path + "/dirindir");
0152     if (!ok) {
0153         qFatal("couldn't create %s", qPrintable(path));
0154     }
0155     createTestFile(path + "/dirindir/nested", "Nested");
0156     checkTestDirectory(path);
0157 }
0158 
0159 class TestUiInterface : public FileUndoManager::UiInterface
0160 {
0161 public:
0162     TestUiInterface()
0163         : FileUndoManager::UiInterface()
0164         , m_mockAskUserInterface(new MockAskUserInterface)
0165     {
0166         setShowProgressInfo(false);
0167     }
0168     void jobError(KIO::Job *job) override
0169     {
0170         m_errorCode = job->error();
0171         qWarning() << job->errorString();
0172     }
0173     bool copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime) override
0174     {
0175         Q_UNUSED(src);
0176         m_dest = dest;
0177         Q_UNUSED(srcTime);
0178         Q_UNUSED(destTime);
0179         return true;
0180     }
0181     bool confirmDeletion(const QList<QUrl> &) override
0182     {
0183         Q_ASSERT(false); // no longer called
0184         return false;
0185     }
0186     void setNextReplyToConfirmDeletion(bool b)
0187     {
0188         m_nextReplyToConfirmDeletion = b;
0189     }
0190     QUrl dest() const
0191     {
0192         return m_dest;
0193     }
0194     int errorCode() const
0195     {
0196         return m_errorCode;
0197     }
0198     void clear()
0199     {
0200         m_dest = QUrl();
0201         m_errorCode = 0;
0202         m_mockAskUserInterface->clear();
0203     }
0204     MockAskUserInterface *askUserMockInterface() const
0205     {
0206         return m_mockAskUserInterface.get();
0207     }
0208     void virtual_hook(int id, void *data) override
0209     {
0210         if (id == HookGetAskUserActionInterface) {
0211             AskUserActionInterface **p = static_cast<AskUserActionInterface **>(data);
0212             *p = m_mockAskUserInterface.get();
0213             m_mockAskUserInterface->m_deleteResult = m_nextReplyToConfirmDeletion;
0214         }
0215     }
0216 
0217 private:
0218     QUrl m_dest;
0219     std::unique_ptr<MockAskUserInterface> m_mockAskUserInterface;
0220     int m_errorCode = 0;
0221     bool m_nextReplyToConfirmDeletion = true;
0222 };
0223 
0224 void FileUndoManagerTest::initTestCase()
0225 {
0226     qDebug("initTestCase");
0227 
0228     QStandardPaths::setTestModeEnabled(true);
0229 
0230     // Get kio_trash to share our environment so that it writes trashrc to the right kdehome
0231     qputenv("KIOSLAVE_ENABLE_TESTMODE", "1");
0232 
0233     // Start with a clean base dir
0234     cleanupTestCase();
0235 
0236     if (!QFile::exists(homeTmpDir())) {
0237         bool ok = QDir().mkpath(homeTmpDir());
0238         if (!ok) {
0239             qFatal("Couldn't create %s", qPrintable(homeTmpDir()));
0240         }
0241     }
0242 
0243     createTestFile(srcFile(), "Hello world");
0244 #ifndef Q_OS_WIN
0245     createTestSymlink(srcLink());
0246 #endif
0247     createTestDirectory(srcSubDir());
0248 
0249     QDir().mkpath(destDir());
0250     QVERIFY(QFileInfo(destDir()).isDir());
0251 
0252     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0253     m_uiInterface = new TestUiInterface; // owned by FileUndoManager
0254     FileUndoManager::self()->setUiInterface(m_uiInterface);
0255 }
0256 
0257 void FileUndoManagerTest::cleanupTestCase()
0258 {
0259     KIO::Job *job = KIO::del(QUrl::fromLocalFile(homeTmpDir()), KIO::HideProgressInfo);
0260     job->exec();
0261 }
0262 
0263 void FileUndoManagerTest::doUndo()
0264 {
0265     QEventLoop eventLoop;
0266     connect(FileUndoManager::self(), &FileUndoManager::undoJobFinished, &eventLoop, &QEventLoop::quit);
0267     FileUndoManager::self()->undo();
0268     eventLoop.exec(QEventLoop::ExcludeUserInputEvents); // wait for undo job to finish
0269 }
0270 
0271 void FileUndoManagerTest::testCopyFiles()
0272 {
0273     // Initially inspired from JobTest::copyFileToSamePartition()
0274     const QString destdir = destDir();
0275     QList<QUrl> lst = sourceList();
0276     const QUrl d = QUrl::fromLocalFile(destdir);
0277     KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo);
0278     job->setUiDelegate(nullptr);
0279     FileUndoManager::self()->recordCopyJob(job);
0280 
0281     QSignalSpy spyUndoAvailable(FileUndoManager::self(), qOverload<bool>(&FileUndoManager::undoAvailable));
0282     QVERIFY(spyUndoAvailable.isValid());
0283     QSignalSpy spyTextChanged(FileUndoManager::self(), &FileUndoManager::undoTextChanged);
0284     QVERIFY(spyTextChanged.isValid());
0285 
0286     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0287 
0288     QVERIFY(QFile::exists(destFile()));
0289 #ifndef Q_OS_WIN
0290     // Don't use QFile::exists, it's a broken symlink...
0291     QVERIFY(QFileInfo(destLink()).isSymLink());
0292 #endif
0293 
0294     // might have to wait for dbus signal here... but this is currently disabled.
0295     // QTest::qWait( 20 );
0296     QVERIFY(FileUndoManager::self()->isUndoAvailable());
0297     QCOMPARE(spyUndoAvailable.count(), 1);
0298     QCOMPARE(spyTextChanged.count(), 1);
0299     m_uiInterface->clear();
0300 
0301     m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm
0302     FileUndoManager::self()->undo();
0303     auto *lastMock = m_uiInterface->askUserMockInterface();
0304     QCOMPARE(lastMock->m_askUserDeleteCalled, 1);
0305     QVERIFY(QFile::exists(destFile())); // nothing happened yet
0306 
0307     // OK, now do it
0308     m_uiInterface->clear();
0309     m_uiInterface->setNextReplyToConfirmDeletion(true);
0310     doUndo();
0311 
0312     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0313     QVERIFY(spyUndoAvailable.count() >= 2); // it's in fact 3, due to lock/unlock emitting it as well
0314     QCOMPARE(spyTextChanged.count(), 2);
0315     QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);
0316 
0317     // Check that undo worked
0318     QVERIFY(!QFile::exists(destFile()));
0319 #ifndef Q_OS_WIN
0320     QVERIFY(!QFile::exists(destLink()));
0321     QVERIFY(!QFileInfo(destLink()).isSymLink());
0322 #endif
0323 }
0324 
0325 void FileUndoManagerTest::testMoveFiles()
0326 {
0327     const QString destdir = destDir();
0328     QList<QUrl> lst = sourceList();
0329     const QUrl d = QUrl::fromLocalFile(destdir);
0330     KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo);
0331     job->setUiDelegate(nullptr);
0332     FileUndoManager::self()->recordCopyJob(job);
0333 
0334     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0335 
0336     QVERIFY(!QFile::exists(srcFile())); // the source moved
0337     QVERIFY(QFile::exists(destFile()));
0338 #ifndef Q_OS_WIN
0339     QVERIFY(!QFileInfo(srcLink()).isSymLink());
0340     // Don't use QFile::exists, it's a broken symlink...
0341     QVERIFY(QFileInfo(destLink()).isSymLink());
0342 #endif
0343 
0344     doUndo();
0345 
0346     QVERIFY(QFile::exists(srcFile())); // the source is back
0347     QVERIFY(!QFile::exists(destFile()));
0348 #ifndef Q_OS_WIN
0349     QVERIFY(QFileInfo(srcLink()).isSymLink());
0350     QVERIFY(!QFileInfo(destLink()).isSymLink());
0351 #endif
0352 }
0353 
0354 void FileUndoManagerTest::testCopyDirectory()
0355 {
0356     const QString destdir = destDir();
0357     QList<QUrl> lst;
0358     lst << QUrl::fromLocalFile(srcSubDir());
0359     const QUrl d = QUrl::fromLocalFile(destdir);
0360     KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo);
0361     job->setUiDelegate(nullptr);
0362     FileUndoManager::self()->recordCopyJob(job);
0363 
0364     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0365 
0366     checkTestDirectory(srcSubDir()); // src untouched
0367     checkTestDirectory(destSubDir());
0368 
0369     doUndo();
0370 
0371     checkTestDirectory(srcSubDir());
0372     QVERIFY(!QFile::exists(destSubDir()));
0373 }
0374 
0375 void FileUndoManagerTest::testCopyEmptyDirectory()
0376 {
0377     const QString src = srcSubDir() + "/.emptydir";
0378     const QString destEmptyDir = destDir() + "/.emptydir";
0379     QDir().mkpath(src);
0380     KIO::CopyJob *job = KIO::copy({QUrl::fromLocalFile(src)}, QUrl::fromLocalFile(destEmptyDir), KIO::HideProgressInfo);
0381     job->setUiDelegate(nullptr);
0382     FileUndoManager::self()->recordCopyJob(job);
0383 
0384     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0385 
0386     QVERIFY(QFileInfo(src).isDir()); // untouched
0387     QVERIFY(QFileInfo(destEmptyDir).isDir());
0388 
0389     doUndo();
0390 
0391     QVERIFY(QFileInfo(src).isDir()); // untouched
0392     QVERIFY(!QFile::exists(destEmptyDir));
0393 }
0394 
0395 void FileUndoManagerTest::testMoveDirectory()
0396 {
0397     const QString destdir = destDir();
0398     QList<QUrl> lst;
0399     lst << QUrl::fromLocalFile(srcSubDir());
0400     const QUrl d = QUrl::fromLocalFile(destdir);
0401     KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo);
0402     job->setUiDelegate(nullptr);
0403     FileUndoManager::self()->recordCopyJob(job);
0404 
0405     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0406 
0407     QVERIFY(!QFile::exists(srcSubDir()));
0408     checkTestDirectory(destSubDir());
0409 
0410     doUndo();
0411 
0412     checkTestDirectory(srcSubDir());
0413     QVERIFY(!QFile::exists(destSubDir()));
0414 }
0415 
0416 void FileUndoManagerTest::testRenameFile()
0417 {
0418     const QUrl oldUrl = QUrl::fromLocalFile(srcFile());
0419     const QUrl newUrl = QUrl::fromLocalFile(srcFile() + ".new");
0420     QList<QUrl> lst;
0421     lst.append(oldUrl);
0422     QSignalSpy spyUndoAvailable(FileUndoManager::self(), qOverload<bool>(&FileUndoManager::undoAvailable));
0423     QVERIFY(spyUndoAvailable.isValid());
0424     KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
0425     job->setUiDelegate(nullptr);
0426     FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job);
0427 
0428     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0429 
0430     QVERIFY(!QFile::exists(srcFile()));
0431     QVERIFY(QFileInfo(newUrl.toLocalFile()).isFile());
0432     QCOMPARE(spyUndoAvailable.count(), 1);
0433 
0434     doUndo();
0435 
0436     QVERIFY(QFile::exists(srcFile()));
0437     QVERIFY(!QFileInfo(newUrl.toLocalFile()).isFile());
0438 }
0439 
0440 void FileUndoManagerTest::testRenameDir()
0441 {
0442     const QUrl oldUrl = QUrl::fromLocalFile(srcSubDir());
0443     const QUrl newUrl = QUrl::fromLocalFile(srcSubDir() + ".new");
0444     QList<QUrl> lst;
0445     lst.append(oldUrl);
0446     KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
0447     job->setUiDelegate(nullptr);
0448     FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job);
0449 
0450     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0451 
0452     QVERIFY(!QFile::exists(srcSubDir()));
0453     QVERIFY(QFileInfo(newUrl.toLocalFile()).isDir());
0454 
0455     doUndo();
0456 
0457     QVERIFY(QFile::exists(srcSubDir()));
0458     QVERIFY(!QFileInfo(newUrl.toLocalFile()).isDir());
0459 }
0460 
0461 void FileUndoManagerTest::testCreateSymlink()
0462 {
0463 #ifdef Q_OS_WIN
0464     QSKIP("Test skipped on Windows for lack of proper symlink support");
0465 #endif
0466     const QUrl link = QUrl::fromLocalFile(homeTmpDir() + "newlink");
0467     const QString path = link.toLocalFile();
0468     QVERIFY(!QFile::exists(path));
0469 
0470     const QUrl target = QUrl::fromLocalFile(homeTmpDir() + "linktarget");
0471     const QString targetPath = target.toLocalFile();
0472     createTestFile(targetPath, "Link's Target");
0473     QVERIFY(QFile::exists(targetPath));
0474 
0475     KIO::CopyJob *job = KIO::link(target, link, KIO::HideProgressInfo);
0476     job->setUiDelegate(nullptr);
0477     FileUndoManager::self()->recordCopyJob(job);
0478     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0479     QVERIFY(QFile::exists(path));
0480     QVERIFY(QFileInfo(path).isSymLink());
0481 
0482     // For undoing symlinks no confirmation is required. We delete it straight away.
0483     doUndo();
0484 
0485     QVERIFY(!QFile::exists(path));
0486 }
0487 
0488 void FileUndoManagerTest::testCreateDir()
0489 {
0490     const QUrl url = QUrl::fromLocalFile(srcSubDir() + ".mkdir");
0491     const QString path = url.toLocalFile();
0492     QVERIFY(!QFile::exists(path));
0493 
0494     KIO::SimpleJob *job = KIO::mkdir(url);
0495     job->setUiDelegate(nullptr);
0496     FileUndoManager::self()->recordJob(FileUndoManager::Mkdir, QList<QUrl>(), url, job);
0497     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0498     QVERIFY(QFile::exists(path));
0499     QVERIFY(QFileInfo(path).isDir());
0500 
0501     m_uiInterface->clear();
0502     m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm
0503     FileUndoManager::self()->undo();
0504     QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);
0505     QVERIFY(QFile::exists(path)); // nothing happened yet
0506 
0507     // OK, now do it
0508     m_uiInterface->clear();
0509     m_uiInterface->setNextReplyToConfirmDeletion(true);
0510     doUndo();
0511 
0512     QVERIFY(!QFile::exists(path));
0513 }
0514 
0515 void FileUndoManagerTest::testMkpath()
0516 {
0517     const QString parent = srcSubDir() + "mkpath";
0518     const QString path = parent + "/subdir";
0519     QVERIFY(!QFile::exists(path));
0520     const QUrl url = QUrl::fromLocalFile(path);
0521 
0522     KIO::Job *job = KIO::mkpath(url, QUrl(), KIO::HideProgressInfo);
0523     job->setUiDelegate(nullptr);
0524     FileUndoManager::self()->recordJob(FileUndoManager::Mkpath, QList<QUrl>(), url, job);
0525     QVERIFY(job->exec());
0526     QVERIFY(QFileInfo(path).isDir());
0527 
0528     m_uiInterface->clear();
0529     m_uiInterface->setNextReplyToConfirmDeletion(true);
0530     doUndo();
0531 
0532     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0533     QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);
0534 
0535     QVERIFY(!QFile::exists(path));
0536 }
0537 
0538 void FileUndoManagerTest::testTrashFiles()
0539 {
0540     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) {
0541         QSKIP("kio_trash not installed");
0542     }
0543 
0544     // Trash it all at once: the file, the symlink, the subdir.
0545     QList<QUrl> lst = sourceList();
0546     lst.append(QUrl::fromLocalFile(srcSubDir()));
0547     KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo);
0548     job->setUiDelegate(nullptr);
0549     FileUndoManager::self()->recordJob(FileUndoManager::Trash, lst, QUrl(QStringLiteral("trash:/")), job);
0550 
0551     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0552 
0553     // Check that things got removed
0554     QVERIFY(!QFile::exists(srcFile()));
0555 #ifndef Q_OS_WIN
0556     QVERIFY(!QFileInfo(srcLink()).isSymLink());
0557 #endif
0558     QVERIFY(!QFile::exists(srcSubDir()));
0559 
0560     // check trash?
0561     // Let's just check that it's not empty. kio_trash has its own unit tests anyway.
0562     KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig);
0563     QVERIFY(cfg.hasGroup("Status"));
0564     QCOMPARE(cfg.group("Status").readEntry("Empty", true), false);
0565 
0566     doUndo();
0567 
0568     QVERIFY(QFile::exists(srcFile()));
0569 #ifndef Q_OS_WIN
0570     QVERIFY(QFileInfo(srcLink()).isSymLink());
0571 #endif
0572     QVERIFY(QFile::exists(srcSubDir()));
0573 
0574     // We can't check that the trash is empty; other partitions might have their own trash
0575 }
0576 
0577 void FileUndoManagerTest::testRestoreTrashedFiles()
0578 {
0579     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) {
0580         QSKIP("kio_trash not installed");
0581     }
0582 
0583     // Trash it all at once: the file, the symlink, the subdir.
0584     const QFile::Permissions origPerms = QFileInfo(srcFile()).permissions();
0585     QList<QUrl> lst = sourceList();
0586     lst.append(QUrl::fromLocalFile(srcSubDir()));
0587     KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo);
0588     job->setUiDelegate(nullptr);
0589     QVERIFY(job->exec());
0590 
0591     const QMap<QString, QString> metaData = job->metaData();
0592     QList<QUrl> trashUrls;
0593     for (const QUrl &src : std::as_const(lst)) {
0594         QMap<QString, QString>::ConstIterator it = metaData.find("trashURL-" + src.path());
0595         QVERIFY(it != metaData.constEnd());
0596         trashUrls.append(QUrl(it.value()));
0597     }
0598 
0599     // Restore from trash
0600     KIO::RestoreJob *restoreJob = KIO::restoreFromTrash(trashUrls, KIO::HideProgressInfo);
0601     restoreJob->setUiDelegate(nullptr);
0602     QVERIFY(restoreJob->exec());
0603 
0604     QVERIFY(QFile::exists(srcFile()));
0605     QCOMPARE(QFileInfo(srcFile()).permissions(), origPerms);
0606 #ifndef Q_OS_WIN
0607     QVERIFY(QFileInfo(srcLink()).isSymLink());
0608 #endif
0609     QVERIFY(QFile::exists(srcSubDir()));
0610 
0611     // TODO support for RestoreJob in FileUndoManager !!!
0612 }
0613 
0614 static void setTimeStamp(const QString &path)
0615 {
0616 #ifdef Q_OS_UNIX
0617     // Put timestamp in the past so that we can check that the
0618     // copy actually preserves it.
0619     struct timeval tp;
0620     gettimeofday(&tp, nullptr);
0621     struct utimbuf utbuf;
0622     utbuf.actime = tp.tv_sec + 30; // 30 seconds in the future
0623     utbuf.modtime = tp.tv_sec + 60; // 60 second in the future
0624     utime(QFile::encodeName(path).constData(), &utbuf);
0625     qDebug("Time changed for %s", qPrintable(path));
0626 #endif
0627 }
0628 
0629 void FileUndoManagerTest::testModifyFileBeforeUndo()
0630 {
0631     // based on testCopyDirectory (so that we check that it works for files in subdirs too)
0632     const QString destdir = destDir();
0633     const QList<QUrl> lst{QUrl::fromLocalFile(srcSubDir())};
0634     const QUrl dest = QUrl::fromLocalFile(destdir);
0635     KIO::CopyJob *job = KIO::copy(lst, dest, KIO::HideProgressInfo);
0636     job->setUiDelegate(nullptr);
0637     FileUndoManager::self()->recordCopyJob(job);
0638 
0639     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0640 
0641     checkTestDirectory(srcSubDir()); // src untouched
0642     checkTestDirectory(destSubDir());
0643     const QString destFile = destSubDir() + "/fileindir";
0644     setTimeStamp(destFile); // simulate a modification of the file
0645 
0646     doUndo();
0647 
0648     // Check that TestUiInterface::copiedFileWasModified got called
0649     QCOMPARE(m_uiInterface->dest().toLocalFile(), destFile);
0650 
0651     checkTestDirectory(srcSubDir());
0652     QVERIFY(!QFile::exists(destSubDir()));
0653 }
0654 
0655 void FileUndoManagerTest::testPasteClipboardUndo()
0656 {
0657     const QList<QUrl> urls(sourceList());
0658     QMimeData *mimeData = new QMimeData();
0659     mimeData->setUrls(urls);
0660     KIO::setClipboardDataCut(mimeData, true);
0661     QClipboard *clipboard = QApplication::clipboard();
0662     clipboard->setMimeData(mimeData);
0663 
0664     // Paste the contents of the clipboard and check its status
0665     QUrl destDirUrl = QUrl::fromLocalFile(destDir());
0666     KIO::Job *job = KIO::paste(mimeData, destDirUrl, KIO::HideProgressInfo);
0667     QVERIFY(job);
0668     QVERIFY(job->exec());
0669 
0670     // Check if the clipboard was updated after paste operation
0671     QList<QUrl> urls2;
0672     for (const QUrl &url : urls) {
0673         QUrl dUrl = destDirUrl.adjusted(QUrl::StripTrailingSlash);
0674         dUrl.setPath(dUrl.path() + '/' + url.fileName());
0675         urls2 << dUrl;
0676     }
0677     QList<QUrl> clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData());
0678     QCOMPARE(clipboardUrls, urls2);
0679 
0680     // Check if the clipboard was updated after undo operation
0681     doUndo();
0682     clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData());
0683     QCOMPARE(clipboardUrls, urls);
0684 }
0685 
0686 void FileUndoManagerTest::testBatchRename()
0687 {
0688     auto createUrl = [](const QString &path) -> QUrl {
0689         return QUrl::fromLocalFile(homeTmpDir() + path);
0690     };
0691 
0692     QList<QUrl> srcList;
0693     srcList << createUrl("textfile.txt") << createUrl("mediafile.mkv") << createUrl("sourcefile.cpp");
0694 
0695     createTestFile(srcList.at(0).path(), "foo");
0696     createTestFile(srcList.at(1).path(), "foo");
0697     createTestFile(srcList.at(2).path(), "foo");
0698 
0699     KIO::Job *job = KIO::batchRename(srcList, QLatin1String("newfile###"), 1, QLatin1Char('#'), KIO::HideProgressInfo);
0700     job->setUiDelegate(nullptr);
0701     FileUndoManager::self()->recordJob(FileUndoManager::BatchRename, srcList, QUrl(), job);
0702     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0703 
0704     QVERIFY(QFile::exists(createUrl("newfile001.txt").path()));
0705     QVERIFY(QFile::exists(createUrl("newfile002.mkv").path()));
0706     QVERIFY(QFile::exists(createUrl("newfile003.cpp").path()));
0707     QVERIFY(!QFile::exists(srcList.at(0).path()));
0708     QVERIFY(!QFile::exists(srcList.at(1).path()));
0709     QVERIFY(!QFile::exists(srcList.at(2).path()));
0710 
0711     doUndo();
0712 
0713     QVERIFY(!QFile::exists(createUrl("newfile###.txt").path()));
0714     QVERIFY(!QFile::exists(createUrl("newfile###.mkv").path()));
0715     QVERIFY(!QFile::exists(createUrl("newfile###.cpp").path()));
0716     QVERIFY(QFile::exists(srcList.at(0).path()));
0717     QVERIFY(QFile::exists(srcList.at(1).path()));
0718     QVERIFY(QFile::exists(srcList.at(2).path()));
0719 }
0720 
0721 void FileUndoManagerTest::testUndoCopyOfDeletedFile()
0722 {
0723     const QUrl source = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("source.txt"));
0724     const QUrl dest = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("copy.txt"));
0725 
0726     createTestFile(source.toLocalFile(), "foo");
0727     QVERIFY(QFileInfo::exists(source.toLocalFile()));
0728 
0729     {
0730         auto copyJob = KIO::copy(source, dest, KIO::HideProgressInfo);
0731         copyJob->setUiDelegate(nullptr);
0732         FileUndoManager::self()->recordCopyJob(copyJob);
0733         QVERIFY2(copyJob->exec(), qPrintable(copyJob->errorString()));
0734         QVERIFY(QFileInfo::exists(dest.toLocalFile()));
0735     }
0736 
0737     {
0738         auto deleteJob = KIO::del(dest, KIO::HideProgressInfo);
0739         deleteJob->setUiDelegate(nullptr);
0740         QVERIFY2(deleteJob->exec(), qPrintable(deleteJob->errorString()));
0741         QVERIFY(!QFileInfo::exists(dest.toLocalFile()));
0742     }
0743 
0744     QVERIFY(FileUndoManager::self()->isUndoAvailable());
0745     QSignalSpy spyUndoAvailable(FileUndoManager::self(), qOverload<bool>(&FileUndoManager::undoAvailable));
0746     QVERIFY(spyUndoAvailable.isValid());
0747     doUndo();
0748     QVERIFY(spyUndoAvailable.count() >= 2); // it's in fact 3, due to lock/unlock emitting it as well
0749     QVERIFY(!spyUndoAvailable.at(0).at(0).toBool());
0750     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0751 }
0752 
0753 void FileUndoManagerTest::testErrorDuringMoveUndo()
0754 {
0755     const QString destdir = destDir();
0756     QList<QUrl> lst{QUrl::fromLocalFile(srcFile())};
0757     KIO::CopyJob *job = KIO::move(lst, QUrl::fromLocalFile(destdir), KIO::HideProgressInfo);
0758     job->setUiDelegate(nullptr);
0759     FileUndoManager::self()->recordCopyJob(job);
0760 
0761     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0762 
0763     QVERIFY(!QFile::exists(srcFile())); // the source moved
0764     QVERIFY(QFile::exists(destFile()));
0765     createTestFile(srcFile(), "I'm back");
0766 
0767     doUndo();
0768 
0769     QCOMPARE(m_uiInterface->errorCode(), KIO::ERR_FILE_ALREADY_EXIST);
0770     QVERIFY(QFile::exists(destFile())); // still there
0771 }
0772 
0773 void FileUndoManagerTest::testNoUndoForSkipAll()
0774 {
0775     auto *undoManager = FileUndoManager::self();
0776 
0777     QTemporaryDir tempDir;
0778     const QString tempPath = tempDir.path();
0779 
0780     const QString destPath = tempPath + "/dest_dir";
0781     QVERIFY(QDir(tempPath).mkdir("dest_dir"));
0782     const QUrl destUrl = QUrl::fromLocalFile(destPath);
0783 
0784     const QList<QUrl> lst{QUrl::fromLocalFile(tempPath + "/file_a"), QUrl::fromLocalFile(tempPath + "/file_b")};
0785     for (const auto &url : lst) {
0786         createTestFile(url.toLocalFile(), "foo");
0787     }
0788 
0789     auto createJob = [&]() {
0790         return KIO::copy(lst, destUrl, KIO::HideProgressInfo);
0791     };
0792 
0793     KIO::CopyJob *job = createJob();
0794     job->setUiDelegate(nullptr);
0795     undoManager->recordCopyJob(job);
0796 
0797     QSignalSpy spyUndoAvailable(undoManager, qOverload<bool>(&FileUndoManager::undoAvailable));
0798     QVERIFY(spyUndoAvailable.isValid());
0799     QSignalSpy spyTextChanged(undoManager, &FileUndoManager::undoTextChanged);
0800     QVERIFY(spyTextChanged.isValid());
0801 
0802     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0803 
0804     // Src files still exist
0805     for (const auto &url : lst) {
0806         QVERIFY(QFile::exists(url.toLocalFile()));
0807     }
0808 
0809     // Files copied to dest
0810     for (const auto &url : lst) {
0811         QVERIFY(QFile::exists(destPath + '/' + url.fileName()));
0812     }
0813 
0814     // An undo command was recorded
0815     QCOMPARE(spyUndoAvailable.count(), 1);
0816     QCOMPARE(spyTextChanged.count(), 1);
0817 
0818     KIO::CopyJob *repeatCopy = createJob();
0819     // Copying the same files again to the same dest, and setting skip all
0820     repeatCopy->setAutoSkip(true);
0821     undoManager->recordCopyJob(repeatCopy);
0822 
0823     QVERIFY2(repeatCopy->exec(), qPrintable(repeatCopy->errorString()));
0824 
0825     // No new undo command was added since the job didn't actually copy anything
0826     QCOMPARE(spyUndoAvailable.count(), 1);
0827     QCOMPARE(spyTextChanged.count(), 1);
0828 }
0829 
0830 // TODO: add test (and fix bug) for  DND of remote urls / "Link here" (creates .desktop files) // Undo (doesn't do anything)
0831 // TODO: add test for interrupting a moving operation and then using Undo - bug:91579
0832 
0833 #include "moc_fileundomanagertest.cpp"