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"