File indexing completed on 2024-04-14 03:52:38

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     void setNextReplyToConfirmDeletion(bool b)
0182     {
0183         m_nextReplyToConfirmDeletion = b;
0184     }
0185     QUrl dest() const
0186     {
0187         return m_dest;
0188     }
0189     int errorCode() const
0190     {
0191         return m_errorCode;
0192     }
0193     void clear()
0194     {
0195         m_dest = QUrl();
0196         m_errorCode = 0;
0197         m_mockAskUserInterface->clear();
0198     }
0199     MockAskUserInterface *askUserMockInterface() const
0200     {
0201         return m_mockAskUserInterface.get();
0202     }
0203     void virtual_hook(int id, void *data) override
0204     {
0205         if (id == HookGetAskUserActionInterface) {
0206             AskUserActionInterface **p = static_cast<AskUserActionInterface **>(data);
0207             *p = m_mockAskUserInterface.get();
0208             m_mockAskUserInterface->m_deleteResult = m_nextReplyToConfirmDeletion;
0209         }
0210     }
0211 
0212 private:
0213     QUrl m_dest;
0214     std::unique_ptr<MockAskUserInterface> m_mockAskUserInterface;
0215     int m_errorCode = 0;
0216     bool m_nextReplyToConfirmDeletion = true;
0217 };
0218 
0219 void FileUndoManagerTest::initTestCase()
0220 {
0221     qDebug("initTestCase");
0222 
0223     QStandardPaths::setTestModeEnabled(true);
0224 
0225     // Get kio_trash to share our environment so that it writes trashrc to the right kdehome
0226     qputenv("KIOWORKER_ENABLE_TESTMODE", "1");
0227 
0228     // Start with a clean base dir
0229     cleanupTestCase();
0230 
0231     if (!QFile::exists(homeTmpDir())) {
0232         bool ok = QDir().mkpath(homeTmpDir());
0233         if (!ok) {
0234             qFatal("Couldn't create %s", qPrintable(homeTmpDir()));
0235         }
0236     }
0237 
0238     createTestFile(srcFile(), "Hello world");
0239 #ifndef Q_OS_WIN
0240     createTestSymlink(srcLink());
0241 #endif
0242     createTestDirectory(srcSubDir());
0243 
0244     QDir().mkpath(destDir());
0245     QVERIFY(QFileInfo(destDir()).isDir());
0246 
0247     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0248     m_uiInterface = new TestUiInterface; // owned by FileUndoManager
0249     FileUndoManager::self()->setUiInterface(m_uiInterface);
0250 }
0251 
0252 void FileUndoManagerTest::cleanupTestCase()
0253 {
0254     KIO::Job *job = KIO::del(QUrl::fromLocalFile(homeTmpDir()), KIO::HideProgressInfo);
0255     job->exec();
0256 }
0257 
0258 void FileUndoManagerTest::doUndo()
0259 {
0260     QEventLoop eventLoop;
0261     connect(FileUndoManager::self(), &FileUndoManager::undoJobFinished, &eventLoop, &QEventLoop::quit);
0262     FileUndoManager::self()->undo();
0263     eventLoop.exec(QEventLoop::ExcludeUserInputEvents); // wait for undo job to finish
0264 }
0265 
0266 void FileUndoManagerTest::testCopyFiles()
0267 {
0268     // Initially inspired from JobTest::copyFileToSamePartition()
0269     const QString destdir = destDir();
0270     QList<QUrl> lst = sourceList();
0271     const QUrl d = QUrl::fromLocalFile(destdir);
0272     KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo);
0273     job->setUiDelegate(nullptr);
0274     FileUndoManager::self()->recordCopyJob(job);
0275 
0276     QSignalSpy spyUndoAvailable(FileUndoManager::self(), &FileUndoManager::undoAvailable);
0277     QVERIFY(spyUndoAvailable.isValid());
0278     QSignalSpy spyTextChanged(FileUndoManager::self(), &FileUndoManager::undoTextChanged);
0279     QVERIFY(spyTextChanged.isValid());
0280 
0281     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0282 
0283     QVERIFY(QFile::exists(destFile()));
0284 #ifndef Q_OS_WIN
0285     // Don't use QFile::exists, it's a broken symlink...
0286     QVERIFY(QFileInfo(destLink()).isSymLink());
0287 #endif
0288 
0289     // might have to wait for dbus signal here... but this is currently disabled.
0290     // QTest::qWait( 20 );
0291     QVERIFY(FileUndoManager::self()->isUndoAvailable());
0292     QCOMPARE(spyUndoAvailable.count(), 1);
0293     QCOMPARE(spyTextChanged.count(), 1);
0294     m_uiInterface->clear();
0295 
0296     m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm
0297     FileUndoManager::self()->undo();
0298     auto *lastMock = m_uiInterface->askUserMockInterface();
0299     QCOMPARE(lastMock->m_askUserDeleteCalled, 1);
0300     QVERIFY(QFile::exists(destFile())); // nothing happened yet
0301 
0302     // OK, now do it
0303     m_uiInterface->clear();
0304     m_uiInterface->setNextReplyToConfirmDeletion(true);
0305     doUndo();
0306 
0307     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0308     QVERIFY(spyUndoAvailable.count() >= 2); // it's in fact 3, due to lock/unlock emitting it as well
0309     QCOMPARE(spyTextChanged.count(), 2);
0310     QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);
0311 
0312     // Check that undo worked
0313     QVERIFY(!QFile::exists(destFile()));
0314 #ifndef Q_OS_WIN
0315     QVERIFY(!QFile::exists(destLink()));
0316     QVERIFY(!QFileInfo(destLink()).isSymLink());
0317 #endif
0318 }
0319 
0320 void FileUndoManagerTest::testMoveFiles()
0321 {
0322     const QString destdir = destDir();
0323     QList<QUrl> lst = sourceList();
0324     const QUrl d = QUrl::fromLocalFile(destdir);
0325     KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo);
0326     job->setUiDelegate(nullptr);
0327     FileUndoManager::self()->recordCopyJob(job);
0328 
0329     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0330 
0331     QVERIFY(!QFile::exists(srcFile())); // the source moved
0332     QVERIFY(QFile::exists(destFile()));
0333 #ifndef Q_OS_WIN
0334     QVERIFY(!QFileInfo(srcLink()).isSymLink());
0335     // Don't use QFile::exists, it's a broken symlink...
0336     QVERIFY(QFileInfo(destLink()).isSymLink());
0337 #endif
0338 
0339     doUndo();
0340 
0341     QVERIFY(QFile::exists(srcFile())); // the source is back
0342     QVERIFY(!QFile::exists(destFile()));
0343 #ifndef Q_OS_WIN
0344     QVERIFY(QFileInfo(srcLink()).isSymLink());
0345     QVERIFY(!QFileInfo(destLink()).isSymLink());
0346 #endif
0347 }
0348 
0349 void FileUndoManagerTest::testCopyDirectory()
0350 {
0351     const QString destdir = destDir();
0352     QList<QUrl> lst;
0353     lst << QUrl::fromLocalFile(srcSubDir());
0354     const QUrl d = QUrl::fromLocalFile(destdir);
0355     KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo);
0356     job->setUiDelegate(nullptr);
0357     FileUndoManager::self()->recordCopyJob(job);
0358 
0359     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0360 
0361     checkTestDirectory(srcSubDir()); // src untouched
0362     checkTestDirectory(destSubDir());
0363 
0364     doUndo();
0365 
0366     checkTestDirectory(srcSubDir());
0367     QVERIFY(!QFile::exists(destSubDir()));
0368 }
0369 
0370 void FileUndoManagerTest::testCopyEmptyDirectory()
0371 {
0372     const QString src = srcSubDir() + "/.emptydir";
0373     const QString destEmptyDir = destDir() + "/.emptydir";
0374     QDir().mkpath(src);
0375     KIO::CopyJob *job = KIO::copy({QUrl::fromLocalFile(src)}, QUrl::fromLocalFile(destEmptyDir), KIO::HideProgressInfo);
0376     job->setUiDelegate(nullptr);
0377     FileUndoManager::self()->recordCopyJob(job);
0378 
0379     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0380 
0381     QVERIFY(QFileInfo(src).isDir()); // untouched
0382     QVERIFY(QFileInfo(destEmptyDir).isDir());
0383 
0384     doUndo();
0385 
0386     QVERIFY(QFileInfo(src).isDir()); // untouched
0387     QVERIFY(!QFile::exists(destEmptyDir));
0388 }
0389 
0390 void FileUndoManagerTest::testMoveDirectory()
0391 {
0392     const QString destdir = destDir();
0393     QList<QUrl> lst;
0394     lst << QUrl::fromLocalFile(srcSubDir());
0395     const QUrl d = QUrl::fromLocalFile(destdir);
0396     KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo);
0397     job->setUiDelegate(nullptr);
0398     FileUndoManager::self()->recordCopyJob(job);
0399 
0400     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0401 
0402     QVERIFY(!QFile::exists(srcSubDir()));
0403     checkTestDirectory(destSubDir());
0404 
0405     doUndo();
0406 
0407     checkTestDirectory(srcSubDir());
0408     QVERIFY(!QFile::exists(destSubDir()));
0409 }
0410 
0411 void FileUndoManagerTest::testRenameFile()
0412 {
0413     const QUrl oldUrl = QUrl::fromLocalFile(srcFile());
0414     const QUrl newUrl = QUrl::fromLocalFile(srcFile() + ".new");
0415     QList<QUrl> lst;
0416     lst.append(oldUrl);
0417     QSignalSpy spyUndoAvailable(FileUndoManager::self(), &FileUndoManager::undoAvailable);
0418     QVERIFY(spyUndoAvailable.isValid());
0419     KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
0420     job->setUiDelegate(nullptr);
0421     FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job);
0422 
0423     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0424 
0425     QVERIFY(!QFile::exists(srcFile()));
0426     QVERIFY(QFileInfo(newUrl.toLocalFile()).isFile());
0427     QCOMPARE(spyUndoAvailable.count(), 1);
0428 
0429     doUndo();
0430 
0431     QVERIFY(QFile::exists(srcFile()));
0432     QVERIFY(!QFileInfo(newUrl.toLocalFile()).isFile());
0433 }
0434 
0435 void FileUndoManagerTest::testRenameDir()
0436 {
0437     const QUrl oldUrl = QUrl::fromLocalFile(srcSubDir());
0438     const QUrl newUrl = QUrl::fromLocalFile(srcSubDir() + ".new");
0439     QList<QUrl> lst;
0440     lst.append(oldUrl);
0441     KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
0442     job->setUiDelegate(nullptr);
0443     FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job);
0444 
0445     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0446 
0447     QVERIFY(!QFile::exists(srcSubDir()));
0448     QVERIFY(QFileInfo(newUrl.toLocalFile()).isDir());
0449 
0450     doUndo();
0451 
0452     QVERIFY(QFile::exists(srcSubDir()));
0453     QVERIFY(!QFileInfo(newUrl.toLocalFile()).isDir());
0454 }
0455 
0456 void FileUndoManagerTest::testCreateSymlink()
0457 {
0458 #ifdef Q_OS_WIN
0459     QSKIP("Test skipped on Windows for lack of proper symlink support");
0460 #endif
0461     const QUrl link = QUrl::fromLocalFile(homeTmpDir() + "newlink");
0462     const QString path = link.toLocalFile();
0463     QVERIFY(!QFile::exists(path));
0464 
0465     const QUrl target = QUrl::fromLocalFile(homeTmpDir() + "linktarget");
0466     const QString targetPath = target.toLocalFile();
0467     createTestFile(targetPath, "Link's Target");
0468     QVERIFY(QFile::exists(targetPath));
0469 
0470     KIO::CopyJob *job = KIO::link(target, link, KIO::HideProgressInfo);
0471     job->setUiDelegate(nullptr);
0472     FileUndoManager::self()->recordCopyJob(job);
0473     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0474     QVERIFY(QFile::exists(path));
0475     QVERIFY(QFileInfo(path).isSymLink());
0476 
0477     // For undoing symlinks no confirmation is required. We delete it straight away.
0478     doUndo();
0479 
0480     QVERIFY(!QFile::exists(path));
0481 }
0482 
0483 void FileUndoManagerTest::testCreateDir()
0484 {
0485     const QUrl url = QUrl::fromLocalFile(srcSubDir() + ".mkdir");
0486     const QString path = url.toLocalFile();
0487     QVERIFY(!QFile::exists(path));
0488 
0489     KIO::SimpleJob *job = KIO::mkdir(url);
0490     job->setUiDelegate(nullptr);
0491     FileUndoManager::self()->recordJob(FileUndoManager::Mkdir, QList<QUrl>(), url, job);
0492     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0493     QVERIFY(QFile::exists(path));
0494     QVERIFY(QFileInfo(path).isDir());
0495 
0496     m_uiInterface->clear();
0497     m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm
0498     FileUndoManager::self()->undo();
0499     QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);
0500     QVERIFY(QFile::exists(path)); // nothing happened yet
0501 
0502     // OK, now do it
0503     m_uiInterface->clear();
0504     m_uiInterface->setNextReplyToConfirmDeletion(true);
0505     doUndo();
0506 
0507     QVERIFY(!QFile::exists(path));
0508 }
0509 
0510 void FileUndoManagerTest::testMkpath()
0511 {
0512     const QString parent = srcSubDir() + "mkpath";
0513     const QString path = parent + "/subdir";
0514     QVERIFY(!QFile::exists(path));
0515     const QUrl url = QUrl::fromLocalFile(path);
0516 
0517     KIO::Job *job = KIO::mkpath(url, QUrl(), KIO::HideProgressInfo);
0518     job->setUiDelegate(nullptr);
0519     FileUndoManager::self()->recordJob(FileUndoManager::Mkpath, QList<QUrl>(), url, job);
0520     QVERIFY(job->exec());
0521     QVERIFY(QFileInfo(path).isDir());
0522 
0523     m_uiInterface->clear();
0524     m_uiInterface->setNextReplyToConfirmDeletion(true);
0525     doUndo();
0526 
0527     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0528     QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);
0529 
0530     QVERIFY(!QFile::exists(path));
0531 }
0532 
0533 void FileUndoManagerTest::testTrashFiles()
0534 {
0535     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) {
0536         QSKIP("kio_trash not installed");
0537     }
0538 
0539     // Trash it all at once: the file, the symlink, the subdir.
0540     QList<QUrl> lst = sourceList();
0541     lst.append(QUrl::fromLocalFile(srcSubDir()));
0542     KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo);
0543     job->setUiDelegate(nullptr);
0544     FileUndoManager::self()->recordJob(FileUndoManager::Trash, lst, QUrl(QStringLiteral("trash:/")), job);
0545 
0546     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0547 
0548     // Check that things got removed
0549     QVERIFY(!QFile::exists(srcFile()));
0550 #ifndef Q_OS_WIN
0551     QVERIFY(!QFileInfo(srcLink()).isSymLink());
0552 #endif
0553     QVERIFY(!QFile::exists(srcSubDir()));
0554 
0555     // check trash?
0556     // Let's just check that it's not empty. kio_trash has its own unit tests anyway.
0557     KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig);
0558     QVERIFY(cfg.hasGroup(QStringLiteral("Status")));
0559     QCOMPARE(cfg.group(QStringLiteral("Status")).readEntry("Empty", true), false);
0560 
0561     doUndo();
0562 
0563     QVERIFY(QFile::exists(srcFile()));
0564 #ifndef Q_OS_WIN
0565     QVERIFY(QFileInfo(srcLink()).isSymLink());
0566 #endif
0567     QVERIFY(QFile::exists(srcSubDir()));
0568 
0569     // We can't check that the trash is empty; other partitions might have their own trash
0570 }
0571 
0572 void FileUndoManagerTest::testRestoreTrashedFiles()
0573 {
0574     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) {
0575         QSKIP("kio_trash not installed");
0576     }
0577 
0578     // Trash it all at once: the file, the symlink, the subdir.
0579     const QFile::Permissions origPerms = QFileInfo(srcFile()).permissions();
0580     QList<QUrl> lst = sourceList();
0581     lst.append(QUrl::fromLocalFile(srcSubDir()));
0582     KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo);
0583     job->setUiDelegate(nullptr);
0584     QVERIFY(job->exec());
0585 
0586     const QMap<QString, QString> metaData = job->metaData();
0587     QList<QUrl> trashUrls;
0588     for (const QUrl &src : std::as_const(lst)) {
0589         QMap<QString, QString>::ConstIterator it = metaData.find("trashURL-" + src.path());
0590         QVERIFY(it != metaData.constEnd());
0591         trashUrls.append(QUrl(it.value()));
0592     }
0593 
0594     // Restore from trash
0595     KIO::RestoreJob *restoreJob = KIO::restoreFromTrash(trashUrls, KIO::HideProgressInfo);
0596     restoreJob->setUiDelegate(nullptr);
0597     QVERIFY(restoreJob->exec());
0598 
0599     QVERIFY(QFile::exists(srcFile()));
0600     QCOMPARE(QFileInfo(srcFile()).permissions(), origPerms);
0601 #ifndef Q_OS_WIN
0602     QVERIFY(QFileInfo(srcLink()).isSymLink());
0603 #endif
0604     QVERIFY(QFile::exists(srcSubDir()));
0605 
0606     // TODO support for RestoreJob in FileUndoManager !!!
0607 }
0608 
0609 static void setTimeStamp(const QString &path)
0610 {
0611 #ifdef Q_OS_UNIX
0612     // Put timestamp in the past so that we can check that the
0613     // copy actually preserves it.
0614     struct timeval tp;
0615     gettimeofday(&tp, nullptr);
0616     struct utimbuf utbuf;
0617     utbuf.actime = tp.tv_sec + 30; // 30 seconds in the future
0618     utbuf.modtime = tp.tv_sec + 60; // 60 second in the future
0619     utime(QFile::encodeName(path).constData(), &utbuf);
0620     qDebug("Time changed for %s", qPrintable(path));
0621 #endif
0622 }
0623 
0624 void FileUndoManagerTest::testModifyFileBeforeUndo()
0625 {
0626     // based on testCopyDirectory (so that we check that it works for files in subdirs too)
0627     const QString destdir = destDir();
0628     const QList<QUrl> lst{QUrl::fromLocalFile(srcSubDir())};
0629     const QUrl dest = QUrl::fromLocalFile(destdir);
0630     KIO::CopyJob *job = KIO::copy(lst, dest, KIO::HideProgressInfo);
0631     job->setUiDelegate(nullptr);
0632     FileUndoManager::self()->recordCopyJob(job);
0633 
0634     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0635 
0636     checkTestDirectory(srcSubDir()); // src untouched
0637     checkTestDirectory(destSubDir());
0638     const QString destFile = destSubDir() + "/fileindir";
0639     setTimeStamp(destFile); // simulate a modification of the file
0640 
0641     doUndo();
0642 
0643     // Check that TestUiInterface::copiedFileWasModified got called
0644     QCOMPARE(m_uiInterface->dest().toLocalFile(), destFile);
0645 
0646     checkTestDirectory(srcSubDir());
0647     QVERIFY(!QFile::exists(destSubDir()));
0648 }
0649 
0650 void FileUndoManagerTest::testPasteClipboardUndo()
0651 {
0652     const QList<QUrl> urls(sourceList());
0653     QMimeData *mimeData = new QMimeData();
0654     mimeData->setUrls(urls);
0655     KIO::setClipboardDataCut(mimeData, true);
0656     QClipboard *clipboard = QApplication::clipboard();
0657     clipboard->setMimeData(mimeData);
0658 
0659     // Paste the contents of the clipboard and check its status
0660     QUrl destDirUrl = QUrl::fromLocalFile(destDir());
0661     KIO::Job *job = KIO::paste(mimeData, destDirUrl, KIO::HideProgressInfo);
0662     QVERIFY(job);
0663     QVERIFY(job->exec());
0664 
0665     // Check if the clipboard was updated after paste operation
0666     QList<QUrl> urls2;
0667     for (const QUrl &url : urls) {
0668         QUrl dUrl = destDirUrl.adjusted(QUrl::StripTrailingSlash);
0669         dUrl.setPath(dUrl.path() + '/' + url.fileName());
0670         urls2 << dUrl;
0671     }
0672     QList<QUrl> clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData());
0673     QCOMPARE(clipboardUrls, urls2);
0674 
0675     // Check if the clipboard was updated after undo operation
0676     doUndo();
0677     clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData());
0678     QCOMPARE(clipboardUrls, urls);
0679 }
0680 
0681 void FileUndoManagerTest::testBatchRename()
0682 {
0683     auto createUrl = [](const QString &path) -> QUrl {
0684         return QUrl::fromLocalFile(homeTmpDir() + path);
0685     };
0686 
0687     QList<QUrl> srcList;
0688     srcList << createUrl("textfile.txt") << createUrl("mediafile.mkv") << createUrl("sourcefile.cpp");
0689 
0690     createTestFile(srcList.at(0).path(), "foo");
0691     createTestFile(srcList.at(1).path(), "foo");
0692     createTestFile(srcList.at(2).path(), "foo");
0693 
0694     KIO::Job *job = KIO::batchRename(srcList, QLatin1String("newfile###"), 1, QLatin1Char('#'), KIO::HideProgressInfo);
0695     job->setUiDelegate(nullptr);
0696     FileUndoManager::self()->recordJob(FileUndoManager::BatchRename, srcList, QUrl(), job);
0697     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0698 
0699     QVERIFY(QFile::exists(createUrl("newfile001.txt").path()));
0700     QVERIFY(QFile::exists(createUrl("newfile002.mkv").path()));
0701     QVERIFY(QFile::exists(createUrl("newfile003.cpp").path()));
0702     QVERIFY(!QFile::exists(srcList.at(0).path()));
0703     QVERIFY(!QFile::exists(srcList.at(1).path()));
0704     QVERIFY(!QFile::exists(srcList.at(2).path()));
0705 
0706     doUndo();
0707 
0708     QVERIFY(!QFile::exists(createUrl("newfile###.txt").path()));
0709     QVERIFY(!QFile::exists(createUrl("newfile###.mkv").path()));
0710     QVERIFY(!QFile::exists(createUrl("newfile###.cpp").path()));
0711     QVERIFY(QFile::exists(srcList.at(0).path()));
0712     QVERIFY(QFile::exists(srcList.at(1).path()));
0713     QVERIFY(QFile::exists(srcList.at(2).path()));
0714 }
0715 
0716 void FileUndoManagerTest::testUndoCopyOfDeletedFile()
0717 {
0718     const QUrl source = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("source.txt"));
0719     const QUrl dest = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("copy.txt"));
0720 
0721     createTestFile(source.toLocalFile(), "foo");
0722     QVERIFY(QFileInfo::exists(source.toLocalFile()));
0723 
0724     {
0725         auto copyJob = KIO::copy(source, dest, KIO::HideProgressInfo);
0726         copyJob->setUiDelegate(nullptr);
0727         FileUndoManager::self()->recordCopyJob(copyJob);
0728         QVERIFY2(copyJob->exec(), qPrintable(copyJob->errorString()));
0729         QVERIFY(QFileInfo::exists(dest.toLocalFile()));
0730     }
0731 
0732     {
0733         auto deleteJob = KIO::del(dest, KIO::HideProgressInfo);
0734         deleteJob->setUiDelegate(nullptr);
0735         QVERIFY2(deleteJob->exec(), qPrintable(deleteJob->errorString()));
0736         QVERIFY(!QFileInfo::exists(dest.toLocalFile()));
0737     }
0738 
0739     QVERIFY(FileUndoManager::self()->isUndoAvailable());
0740     QSignalSpy spyUndoAvailable(FileUndoManager::self(), &FileUndoManager::undoAvailable);
0741     QVERIFY(spyUndoAvailable.isValid());
0742     doUndo();
0743     QVERIFY(spyUndoAvailable.count() >= 2); // it's in fact 3, due to lock/unlock emitting it as well
0744     QVERIFY(!spyUndoAvailable.at(0).at(0).toBool());
0745     QVERIFY(!FileUndoManager::self()->isUndoAvailable());
0746 }
0747 
0748 void FileUndoManagerTest::testErrorDuringMoveUndo()
0749 {
0750     const QString destdir = destDir();
0751     QList<QUrl> lst{QUrl::fromLocalFile(srcFile())};
0752     KIO::CopyJob *job = KIO::move(lst, QUrl::fromLocalFile(destdir), KIO::HideProgressInfo);
0753     job->setUiDelegate(nullptr);
0754     FileUndoManager::self()->recordCopyJob(job);
0755 
0756     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0757 
0758     QVERIFY(!QFile::exists(srcFile())); // the source moved
0759     QVERIFY(QFile::exists(destFile()));
0760     createTestFile(srcFile(), "I'm back");
0761 
0762     doUndo();
0763 
0764     QCOMPARE(m_uiInterface->errorCode(), KIO::ERR_FILE_ALREADY_EXIST);
0765     QVERIFY(QFile::exists(destFile())); // still there
0766 }
0767 
0768 void FileUndoManagerTest::testNoUndoForSkipAll()
0769 {
0770     auto *undoManager = FileUndoManager::self();
0771 
0772     QTemporaryDir tempDir;
0773     const QString tempPath = tempDir.path();
0774 
0775     const QString destPath = tempPath + "/dest_dir";
0776     QVERIFY(QDir(tempPath).mkdir("dest_dir"));
0777     const QUrl destUrl = QUrl::fromLocalFile(destPath);
0778 
0779     const QList<QUrl> lst{QUrl::fromLocalFile(tempPath + "/file_a"), QUrl::fromLocalFile(tempPath + "/file_b")};
0780     for (const auto &url : lst) {
0781         createTestFile(url.toLocalFile(), "foo");
0782     }
0783 
0784     auto createJob = [&]() {
0785         return KIO::copy(lst, destUrl, KIO::HideProgressInfo);
0786     };
0787 
0788     KIO::CopyJob *job = createJob();
0789     job->setUiDelegate(nullptr);
0790     undoManager->recordCopyJob(job);
0791 
0792     QSignalSpy spyUndoAvailable(undoManager, &FileUndoManager::undoAvailable);
0793     QVERIFY(spyUndoAvailable.isValid());
0794     QSignalSpy spyTextChanged(undoManager, &FileUndoManager::undoTextChanged);
0795     QVERIFY(spyTextChanged.isValid());
0796 
0797     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0798 
0799     // Src files still exist
0800     for (const auto &url : lst) {
0801         QVERIFY(QFile::exists(url.toLocalFile()));
0802     }
0803 
0804     // Files copied to dest
0805     for (const auto &url : lst) {
0806         QVERIFY(QFile::exists(destPath + '/' + url.fileName()));
0807     }
0808 
0809     // An undo command was recorded
0810     QCOMPARE(spyUndoAvailable.count(), 1);
0811     QCOMPARE(spyTextChanged.count(), 1);
0812 
0813     KIO::CopyJob *repeatCopy = createJob();
0814     // Copying the same files again to the same dest, and setting skip all
0815     repeatCopy->setAutoSkip(true);
0816     undoManager->recordCopyJob(repeatCopy);
0817 
0818     QVERIFY2(repeatCopy->exec(), qPrintable(repeatCopy->errorString()));
0819 
0820     // No new undo command was added since the job didn't actually copy anything
0821     QCOMPARE(spyUndoAvailable.count(), 1);
0822     QCOMPARE(spyTextChanged.count(), 1);
0823 }
0824 
0825 // TODO: add test (and fix bug) for  DND of remote urls / "Link here" (creates .desktop files) // Undo (doesn't do anything)
0826 // TODO: add test for interrupting a moving operation and then using Undo - bug:91579
0827 
0828 #include "moc_fileundomanagertest.cpp"