File indexing completed on 2024-06-16 10:05:01

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2004-2006 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "jobtest.h"
0009 #include "mockcoredelegateextensions.h"
0010 
0011 #include "kio/job.h"
0012 #include "kiotesthelper.h" // createTestFile etc.
0013 #include <kio/chmodjob.h>
0014 #include <kio/copyjob.h>
0015 #include <kio/deletejob.h>
0016 #include <kio/directorysizejob.h>
0017 #include <kio/statjob.h>
0018 #include <kmountpoint.h>
0019 #include <kprotocolinfo.h>
0020 
0021 #include <KJobUiDelegate>
0022 #include <KLocalizedString>
0023 
0024 #include <QBuffer>
0025 #include <QDebug>
0026 #include <QDir>
0027 #include <QElapsedTimer>
0028 #include <QEventLoop>
0029 #include <QFileInfo>
0030 #include <QHash>
0031 #include <QPointer>
0032 #include <QProcess>
0033 #include <QSignalSpy>
0034 #include <QTemporaryFile>
0035 #include <QTest>
0036 #include <QTimer>
0037 #include <QUrl>
0038 #include <QVariant>
0039 
0040 #ifndef Q_OS_WIN
0041 #include <unistd.h> // for readlink
0042 #endif
0043 
0044 QTEST_MAIN(JobTest)
0045 
0046 // The code comes partly from kdebase/kioslave/trash/testtrash.cpp
0047 
0048 static QString otherTmpDir()
0049 {
0050 #ifdef Q_OS_WIN
0051     return QDir::tempPath() + "/jobtest/";
0052 #else
0053     // This one needs to be on another partition, but we can't guarantee that it is
0054     // On CI, it typically isn't...
0055     return QStringLiteral("/tmp/jobtest/");
0056 #endif
0057 }
0058 
0059 static bool otherTmpDirIsOnSamePartition() // true on CI because it's a LXC container
0060 {
0061     KMountPoint::Ptr srcMountPoint = KMountPoint::currentMountPoints().findByPath(homeTmpDir());
0062     KMountPoint::Ptr destMountPoint = KMountPoint::currentMountPoints().findByPath(otherTmpDir());
0063     Q_ASSERT(srcMountPoint);
0064     Q_ASSERT(destMountPoint);
0065     return srcMountPoint->mountedFrom() == destMountPoint->mountedFrom();
0066 }
0067 
0068 void JobTest::initTestCase()
0069 {
0070     QStandardPaths::setTestModeEnabled(true);
0071     QCoreApplication::instance()->setApplicationName("kio/jobtest"); // testing for #357499
0072 
0073     // to make sure io is not too fast
0074     qputenv("KIOSLAVE_FILE_ENABLE_TESTMODE", "1");
0075 
0076     s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago
0077 
0078     // Start with a clean base dir
0079     cleanupTestCase();
0080     homeTmpDir(); // create it
0081     if (!QFile::exists(otherTmpDir())) {
0082         bool ok = QDir().mkdir(otherTmpDir());
0083         if (!ok) {
0084             qFatal("couldn't create %s", qPrintable(otherTmpDir()));
0085         }
0086     }
0087 
0088     /*****
0089      * Set platform xattr related commands.
0090      * Linux commands: setfattr, getfattr
0091      * BSD commands: setextattr, getextattr
0092      * MacOS commands: xattr -w, xattr -p
0093      ****/
0094     m_getXattrCmd = QStandardPaths::findExecutable("getfattr");
0095     if (m_getXattrCmd.endsWith("getfattr")) {
0096         m_setXattrCmd = QStandardPaths::findExecutable("setfattr");
0097         m_setXattrFormatArgs = [](const QString &attrName, const QString &value, const QString &fileName) {
0098             return QStringList{QLatin1String("-n"), attrName, QLatin1String("-v"), value, fileName};
0099         };
0100     } else {
0101         // On BSD there is lsextattr to list all xattrs and getextattr to get a value
0102         // for specific xattr. For test purposes lsextattr is more suitable to be used
0103         // as m_getXattrCmd, so search for it instead of getextattr.
0104         m_getXattrCmd = QStandardPaths::findExecutable("lsextattr");
0105         if (m_getXattrCmd.endsWith("lsextattr")) {
0106             m_setXattrCmd = QStandardPaths::findExecutable("setextattr");
0107             m_setXattrFormatArgs = [](const QString &attrName, const QString &value, const QString &fileName) {
0108                 return QStringList{QLatin1String("user"), attrName, value, fileName};
0109             };
0110         } else {
0111             m_getXattrCmd = QStandardPaths::findExecutable("xattr");
0112             m_setXattrFormatArgs = [](const QString &attrName, const QString &value, const QString &fileName) {
0113                 return QStringList{QLatin1String("-w"), attrName, value, fileName};
0114             };
0115             if (!m_getXattrCmd.endsWith("xattr")) {
0116                 qWarning() << "Neither getfattr, getextattr nor xattr was found.";
0117             }
0118         }
0119     }
0120 
0121     qRegisterMetaType<KJob *>("KJob*");
0122     qRegisterMetaType<KIO::Job *>("KIO::Job*");
0123     qRegisterMetaType<QDateTime>("QDateTime");
0124 }
0125 
0126 void JobTest::cleanupTestCase()
0127 {
0128     QDir(homeTmpDir()).removeRecursively();
0129     QDir(otherTmpDir()).removeRecursively();
0130 }
0131 
0132 struct ScopedCleaner {
0133     using Func = std::function<void()>;
0134     explicit ScopedCleaner(Func f)
0135         : m_f(std::move(f))
0136     {
0137     }
0138     ~ScopedCleaner()
0139     {
0140         m_f();
0141     }
0142 
0143 private:
0144     const Func m_f;
0145 };
0146 
0147 void JobTest::enterLoop()
0148 {
0149     QEventLoop eventLoop;
0150     connect(this, &JobTest::exitLoop, &eventLoop, &QEventLoop::quit);
0151     eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
0152 }
0153 
0154 void JobTest::storedGet()
0155 {
0156     // qDebug();
0157     const QString filePath = homeTmpDir() + "fileFromHome";
0158     createTestFile(filePath);
0159     QUrl u = QUrl::fromLocalFile(filePath);
0160     m_result = -1;
0161 
0162     KIO::StoredTransferJob *job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0163 
0164     QSignalSpy spyPercent(job, &KJob::percentChanged);
0165     QVERIFY(spyPercent.isValid());
0166     job->setUiDelegate(nullptr);
0167     connect(job, &KJob::result, this, &JobTest::slotGetResult);
0168     enterLoop();
0169     QCOMPARE(m_result, 0); // no error
0170     QCOMPARE(m_data, QByteArray("Hello\0world", 11));
0171     QCOMPARE(m_data.size(), 11);
0172     QVERIFY(!spyPercent.isEmpty());
0173 }
0174 
0175 void JobTest::slotGetResult(KJob *job)
0176 {
0177     m_result = job->error();
0178     m_data = static_cast<KIO::StoredTransferJob *>(job)->data();
0179     Q_EMIT exitLoop();
0180 }
0181 
0182 void JobTest::put()
0183 {
0184     const QString filePath = homeTmpDir() + "fileFromHome";
0185     QUrl u = QUrl::fromLocalFile(filePath);
0186     KIO::TransferJob *job = KIO::put(u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0187     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0188     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0189     job->setModificationTime(mtime);
0190     job->setUiDelegate(nullptr);
0191     connect(job, &KJob::result, this, &JobTest::slotResult);
0192     connect(job, &KIO::TransferJob::dataReq, this, &JobTest::slotDataReq);
0193     m_result = -1;
0194     m_dataReqCount = 0;
0195     enterLoop();
0196     QVERIFY(m_result == 0); // no error
0197 
0198     QFileInfo fileInfo(filePath);
0199     QVERIFY(fileInfo.exists());
0200     QCOMPARE(fileInfo.size(), 30LL); // "This is a test for KIO::put()\n"
0201     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0202     QCOMPARE(fileInfo.lastModified(), mtime);
0203 }
0204 
0205 void JobTest::slotDataReq(KIO::Job *, QByteArray &data)
0206 {
0207     // Really not the way you'd write a slotDataReq usually :)
0208     switch (m_dataReqCount++) {
0209     case 0:
0210         data = "This is a test for ";
0211         break;
0212     case 1:
0213         data = "KIO::put()\n";
0214         break;
0215     case 2:
0216         data = QByteArray();
0217         break;
0218     }
0219 }
0220 
0221 void JobTest::slotResult(KJob *job)
0222 {
0223     m_result = job->error();
0224     Q_EMIT exitLoop();
0225 }
0226 
0227 void JobTest::storedPut()
0228 {
0229     const QString filePath = homeTmpDir() + "fileFromHome";
0230     QUrl u = QUrl::fromLocalFile(filePath);
0231     QByteArray putData = "This is the put data";
0232     KIO::TransferJob *job = KIO::storedPut(putData, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0233 
0234     QSignalSpy spyPercent(job, &KJob::percentChanged);
0235     QVERIFY(spyPercent.isValid());
0236     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0237     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0238     job->setModificationTime(mtime);
0239     job->setUiDelegate(nullptr);
0240     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0241     QFileInfo fileInfo(filePath);
0242     QVERIFY(fileInfo.exists());
0243     QCOMPARE(fileInfo.size(), (long long)putData.size());
0244     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0245     QCOMPARE(fileInfo.lastModified(), mtime);
0246     QVERIFY(!spyPercent.isEmpty());
0247 }
0248 
0249 void JobTest::storedPutIODevice()
0250 {
0251     const QString filePath = homeTmpDir() + "fileFromHome";
0252     QBuffer putData;
0253     putData.setData("This is the put data");
0254     QVERIFY(putData.open(QIODevice::ReadOnly));
0255     KIO::TransferJob *job = KIO::storedPut(&putData, QUrl::fromLocalFile(filePath), 0600, KIO::Overwrite | KIO::HideProgressInfo);
0256 
0257     QSignalSpy spyPercent(job, &KJob::percentChanged);
0258     QVERIFY(spyPercent.isValid());
0259     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0260     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0261     job->setModificationTime(mtime);
0262     job->setUiDelegate(nullptr);
0263     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0264     QFileInfo fileInfo(filePath);
0265     QVERIFY(fileInfo.exists());
0266     QCOMPARE(fileInfo.size(), (long long)putData.size());
0267     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0268     QCOMPARE(fileInfo.lastModified(), mtime);
0269     QVERIFY(!spyPercent.isEmpty());
0270 }
0271 
0272 void JobTest::storedPutIODeviceFile()
0273 {
0274     // Given a source file and a destination file
0275     const QString src = homeTmpDir() + "fileFromHome";
0276     createTestFile(src);
0277     QVERIFY(QFile::exists(src));
0278     QFile srcFile(src);
0279     QVERIFY(srcFile.open(QIODevice::ReadOnly));
0280     const QString dest = homeTmpDir() + "fileFromHome_copied";
0281     QFile::remove(dest);
0282     const QUrl destUrl = QUrl::fromLocalFile(dest);
0283 
0284     // When using storedPut with the QFile as argument
0285     KIO::StoredTransferJob *job = KIO::storedPut(&srcFile, destUrl, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0286 
0287     // Then the copy should succeed and the dest file exist
0288     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0289     QVERIFY(QFile::exists(dest));
0290     QCOMPARE(QFileInfo(src).size(), QFileInfo(dest).size());
0291     QFile::remove(dest);
0292 }
0293 
0294 void JobTest::storedPutIODeviceTempFile()
0295 {
0296     // Create a temp file in the current dir.
0297     QTemporaryFile tempFile(QStringLiteral("jobtest-tmp"));
0298     QVERIFY(tempFile.open());
0299 
0300     // Write something into the file.
0301     QTextStream stream(&tempFile);
0302     stream << QStringLiteral("This is the put data");
0303     stream.flush();
0304     QVERIFY(QFileInfo(tempFile).size() > 0);
0305 
0306     const QString dest = homeTmpDir() + QLatin1String("tmpfile-dest");
0307     const QUrl destUrl = QUrl::fromLocalFile(dest);
0308 
0309     // QTemporaryFiles are open in ReadWrite mode,
0310     // so we don't need to close and reopen,
0311     // but we need to rewind to the beginning.
0312     tempFile.seek(0);
0313     auto job = KIO::storedPut(&tempFile, destUrl, -1);
0314 
0315     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0316     QVERIFY(QFileInfo::exists(dest));
0317     QCOMPARE(QFileInfo(dest).size(), QFileInfo(tempFile).size());
0318     QVERIFY(QFile::remove(dest));
0319 }
0320 
0321 void JobTest::storedPutIODeviceFastDevice()
0322 {
0323     const QString filePath = homeTmpDir() + "fileFromHome";
0324     const QUrl u = QUrl::fromLocalFile(filePath);
0325     const QByteArray putDataContents = "This is the put data";
0326     QBuffer putDataBuffer;
0327     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0328 
0329     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0330 
0331     QSignalSpy spyPercent(job, &KJob::percentChanged);
0332     QVERIFY(spyPercent.isValid());
0333     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0334     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0335     job->setModificationTime(mtime);
0336     job->setTotalSize(putDataContents.size());
0337     job->setUiDelegate(nullptr);
0338     job->setAsyncDataEnabled(true);
0339 
0340     // Emit the readChannelFinished even before the job has had time to start
0341     const auto pos = putDataBuffer.pos();
0342     int size = putDataBuffer.write(putDataContents);
0343     putDataBuffer.seek(pos);
0344     Q_EMIT putDataBuffer.readChannelFinished();
0345 
0346     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0347     QCOMPARE(size, putDataContents.size());
0348     QCOMPARE(putDataBuffer.bytesAvailable(), 0);
0349 
0350     QFileInfo fileInfo(filePath);
0351     QVERIFY(fileInfo.exists());
0352     QCOMPARE(fileInfo.size(), (long long)putDataContents.size());
0353     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0354     QCOMPARE(fileInfo.lastModified(), mtime);
0355     QVERIFY(!spyPercent.isEmpty());
0356 }
0357 
0358 void JobTest::storedPutIODeviceSlowDevice()
0359 {
0360     const QString filePath = homeTmpDir() + "fileFromHome";
0361     const QUrl u = QUrl::fromLocalFile(filePath);
0362     const QByteArray putDataContents = "This is the put data";
0363     QBuffer putDataBuffer;
0364     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0365 
0366     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0367 
0368     QSignalSpy spyPercent(job, &KJob::percentChanged);
0369     QVERIFY(spyPercent.isValid());
0370     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0371     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0372     job->setModificationTime(mtime);
0373     job->setTotalSize(putDataContents.size());
0374     job->setUiDelegate(nullptr);
0375     job->setAsyncDataEnabled(true);
0376 
0377     int size = 0;
0378     const auto writeOnce = [&putDataBuffer, &size, putDataContents]() {
0379         const auto pos = putDataBuffer.pos();
0380         size += putDataBuffer.write(putDataContents);
0381         putDataBuffer.seek(pos);
0382         //         qDebug() << "written" << size;
0383     };
0384 
0385     QTimer::singleShot(200, this, writeOnce);
0386     QTimer::singleShot(400, this, writeOnce);
0387     // Simulate the transfer is done
0388     QTimer::singleShot(450, this, [&putDataBuffer]() {
0389         Q_EMIT putDataBuffer.readChannelFinished();
0390     });
0391 
0392     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0393     QCOMPARE(size, putDataContents.size() * 2);
0394     QCOMPARE(putDataBuffer.bytesAvailable(), 0);
0395 
0396     QFileInfo fileInfo(filePath);
0397     QVERIFY(fileInfo.exists());
0398     QCOMPARE(fileInfo.size(), (long long)putDataContents.size() * 2);
0399     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0400     QCOMPARE(fileInfo.lastModified(), mtime);
0401     QVERIFY(!spyPercent.isEmpty());
0402 }
0403 
0404 void JobTest::storedPutIODeviceSlowDeviceBigChunk()
0405 {
0406     const QString filePath = homeTmpDir() + "fileFromHome";
0407     const QUrl u = QUrl::fromLocalFile(filePath);
0408     const QByteArray putDataContents(300000, 'K'); // Make sure the 300000 is bigger than MAX_READ_BUF_SIZE
0409     QBuffer putDataBuffer;
0410     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0411 
0412     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0413 
0414     QSignalSpy spyPercent(job, &KJob::percentChanged);
0415     QVERIFY(spyPercent.isValid());
0416     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0417     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0418     job->setModificationTime(mtime);
0419     job->setTotalSize(putDataContents.size());
0420     job->setUiDelegate(nullptr);
0421     job->setAsyncDataEnabled(true);
0422 
0423     int size = 0;
0424     const auto writeOnce = [&putDataBuffer, &size, putDataContents]() {
0425         const auto pos = putDataBuffer.pos();
0426         size += putDataBuffer.write(putDataContents);
0427         putDataBuffer.seek(pos);
0428         //         qDebug() << "written" << size;
0429     };
0430 
0431     QTimer::singleShot(200, this, writeOnce);
0432     // Simulate the transfer is done
0433     QTimer::singleShot(450, this, [&putDataBuffer]() {
0434         Q_EMIT putDataBuffer.readChannelFinished();
0435     });
0436 
0437     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0438     QCOMPARE(size, putDataContents.size());
0439     QCOMPARE(putDataBuffer.bytesAvailable(), 0);
0440 
0441     QFileInfo fileInfo(filePath);
0442     QVERIFY(fileInfo.exists());
0443     QCOMPARE(fileInfo.size(), (long long)putDataContents.size());
0444     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0445     QCOMPARE(fileInfo.lastModified(), mtime);
0446     QVERIFY(!spyPercent.isEmpty());
0447 }
0448 
0449 void JobTest::asyncStoredPutReadyReadAfterFinish()
0450 {
0451     const QString filePath = homeTmpDir() + "fileFromHome";
0452     const QUrl u = QUrl::fromLocalFile(filePath);
0453 
0454     QBuffer putDataBuffer;
0455     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0456 
0457     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0458     job->setAsyncDataEnabled(true);
0459 
0460     bool jobFinished = false;
0461 
0462     connect(job, &KJob::finished, this, [&jobFinished, &putDataBuffer] {
0463         putDataBuffer.readyRead();
0464         jobFinished = true;
0465     });
0466 
0467     QTimer::singleShot(200, this, [job]() {
0468         job->kill();
0469     });
0470 
0471     QTRY_VERIFY(jobFinished);
0472 }
0473 
0474 static QHash<QString, QString> getSampleXattrs()
0475 {
0476     QHash<QString, QString> attrs;
0477     attrs["user.name with space"] = "value with spaces";
0478     attrs["user.baloo.rating"] = "1";
0479     attrs["user.fnewLine"] = "line1\\nline2";
0480     attrs["user.flistNull"] = "item1\\0item2";
0481     attrs["user.fattr.with.a.lot.of.namespaces"] = "true";
0482     attrs["user.fempty"] = "";
0483     return attrs;
0484 }
0485 
0486 bool JobTest::checkXattrFsSupport(const QString &dir)
0487 {
0488     const QString writeTest = dir + "/fsXattrTestFile";
0489     createTestFile(writeTest);
0490     bool ret = setXattr(writeTest);
0491     QFile::remove(writeTest);
0492     return ret;
0493 }
0494 
0495 bool JobTest::setXattr(const QString &dest)
0496 {
0497     QProcess xattrWriter;
0498     xattrWriter.setProcessChannelMode(QProcess::MergedChannels);
0499 
0500     QHash<QString, QString> attrs = getSampleXattrs();
0501     QHashIterator<QString, QString> i(attrs);
0502     while (i.hasNext()) {
0503         i.next();
0504         QStringList arguments = m_setXattrFormatArgs(i.key(), i.value(), dest);
0505         xattrWriter.start(m_setXattrCmd, arguments);
0506         xattrWriter.waitForStarted();
0507         xattrWriter.waitForFinished(-1);
0508         if (xattrWriter.exitStatus() != QProcess::NormalExit) {
0509             return false;
0510         }
0511         QList<QByteArray> resultdest = xattrWriter.readAllStandardOutput().split('\n');
0512         if (!resultdest[0].isEmpty()) {
0513             QWARN("Error writing user xattr. Xattr copy tests will be disabled.");
0514             qDebug() << resultdest;
0515             return false;
0516         }
0517     }
0518 
0519     return true;
0520 }
0521 
0522 QList<QByteArray> JobTest::readXattr(const QString &src)
0523 {
0524     QProcess xattrReader;
0525     xattrReader.setProcessChannelMode(QProcess::MergedChannels);
0526 
0527     QStringList arguments;
0528     char outputSeparator = '\n';
0529     // Linux
0530     if (m_getXattrCmd.endsWith("getfattr")) {
0531         arguments = QStringList{"-d", src};
0532     }
0533     // BSD
0534     else if (m_getXattrCmd.endsWith("lsextattr")) {
0535         arguments = QStringList{"-q", "user", src};
0536         outputSeparator = '\t';
0537     }
0538     // MacOS
0539     else {
0540         arguments = QStringList{"-l", src};
0541     }
0542 
0543     xattrReader.start(m_getXattrCmd, arguments);
0544     xattrReader.waitForFinished();
0545     QList<QByteArray> result = xattrReader.readAllStandardOutput().split(outputSeparator);
0546     if (m_getXattrCmd.endsWith("getfattr")) {
0547         if (result.size() > 1) {
0548             // Line 1 is the file name
0549             result.removeAt(1);
0550         }
0551     } else if (m_getXattrCmd.endsWith("lsextattr")) {
0552         // cut off trailing \n
0553         result.last().chop(1);
0554         // lsextattr does not sort its output
0555         std::sort(result.begin(), result.end());
0556     }
0557 
0558     return result;
0559 }
0560 
0561 void JobTest::compareXattr(const QString &src, const QString &dest)
0562 {
0563     auto srcAttrs = readXattr(src);
0564     auto dstAttrs = readXattr(dest);
0565     QCOMPARE(dstAttrs, srcAttrs);
0566 }
0567 
0568 void JobTest::copyLocalFile(const QString &src, const QString &dest)
0569 {
0570     const QUrl u = QUrl::fromLocalFile(src);
0571     const QUrl d = QUrl::fromLocalFile(dest);
0572 
0573     const int perms = 0666;
0574     // copy the file with file_copy
0575     KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo);
0576     job->setUiDelegate(nullptr);
0577     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0578     QVERIFY(QFile::exists(dest));
0579     QVERIFY(QFile::exists(src)); // still there
0580     QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666));
0581     compareXattr(src, dest);
0582 
0583     {
0584         // check that the timestamp is the same (#24443)
0585         // Note: this only works because of copy() in kio_file.
0586         // The datapump solution ignores mtime, the app has to call FileCopyJob::setModificationTime()
0587         QFileInfo srcInfo(src);
0588         QFileInfo destInfo(dest);
0589 #ifdef Q_OS_WIN
0590         // win32 time may differs in msec part
0591         QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm"));
0592 #else
0593         QCOMPARE(srcInfo.lastModified(), destInfo.lastModified());
0594 #endif
0595     }
0596 
0597     // cleanup and retry with KIO::copy()
0598     QFile::remove(dest);
0599     auto *copyjob = KIO::copy(u, d, KIO::HideProgressInfo);
0600     QSignalSpy spyCopyingDone(copyjob, &KIO::CopyJob::copyingDone);
0601     copyjob->setUiDelegate(nullptr);
0602     copyjob->setUiDelegateExtension(nullptr);
0603     QVERIFY2(copyjob->exec(), qPrintable(copyjob->errorString()));
0604     QVERIFY(QFile::exists(dest));
0605     QVERIFY(QFile::exists(src)); // still there
0606     compareXattr(src, dest);
0607     {
0608         // check that the timestamp is the same (#24443)
0609         QFileInfo srcInfo(src);
0610         QFileInfo destInfo(dest);
0611 #ifdef Q_OS_WIN
0612         // win32 time may differs in msec part
0613         QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm"));
0614 #else
0615         QCOMPARE(srcInfo.lastModified(), destInfo.lastModified());
0616 #endif
0617     }
0618     QCOMPARE(spyCopyingDone.count(), 1);
0619 
0620     QCOMPARE(copyjob->totalAmount(KJob::Files), 1);
0621     QCOMPARE(copyjob->totalAmount(KJob::Directories), 0);
0622     QCOMPARE(copyjob->processedAmount(KJob::Files), 1);
0623     QCOMPARE(copyjob->processedAmount(KJob::Directories), 0);
0624     QCOMPARE(copyjob->percent(), 100);
0625 
0626     // cleanup and retry with KIO::copyAs()
0627     QFile::remove(dest);
0628     job = KIO::copyAs(u, d, KIO::HideProgressInfo);
0629     job->setUiDelegate(nullptr);
0630     job->setUiDelegateExtension(nullptr);
0631     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0632     QVERIFY(QFile::exists(dest));
0633     QVERIFY(QFile::exists(src)); // still there
0634     compareXattr(src, dest);
0635 
0636     // Do it again, with Overwrite.
0637     job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo);
0638     job->setUiDelegate(nullptr);
0639     job->setUiDelegateExtension(nullptr);
0640     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0641     QVERIFY(QFile::exists(dest));
0642     QVERIFY(QFile::exists(src)); // still there
0643     compareXattr(src, dest);
0644 
0645     // Do it again, without Overwrite (should fail).
0646     job = KIO::copyAs(u, d, KIO::HideProgressInfo);
0647     job->setUiDelegate(nullptr);
0648     job->setUiDelegateExtension(nullptr);
0649     QVERIFY(!job->exec());
0650 
0651     // Clean up
0652     QFile::remove(src);
0653     QFile::remove(dest);
0654 }
0655 
0656 void JobTest::copyLocalDirectory(const QString &src, const QString &_dest, int flags)
0657 {
0658     QVERIFY(QFileInfo(src).isDir());
0659     QVERIFY(QFileInfo(src + "/testfile").isFile());
0660     QUrl u = QUrl::fromLocalFile(src);
0661     QString dest(_dest);
0662     QUrl d = QUrl::fromLocalFile(dest);
0663     if (flags & AlreadyExists) {
0664         QVERIFY(QFile::exists(dest));
0665     } else {
0666         QVERIFY(!QFile::exists(dest));
0667     }
0668 
0669     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
0670     job->setUiDelegate(nullptr);
0671     job->setUiDelegateExtension(nullptr);
0672     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0673     QVERIFY(QFile::exists(dest));
0674     QVERIFY(QFileInfo(dest).isDir());
0675     QVERIFY(QFileInfo(dest + "/testfile").isFile());
0676     QVERIFY(QFile::exists(src)); // still there
0677 
0678     if (flags & AlreadyExists) {
0679         dest += '/' + u.fileName();
0680         // qDebug() << "Expecting dest=" << dest;
0681     }
0682 
0683     // CopyJob::setNextDirAttribute isn't implemented for Windows currently.
0684 #ifndef Q_OS_WIN
0685     {
0686         // Check that the timestamp is the same (#24443)
0687         QFileInfo srcInfo(src);
0688         QFileInfo destInfo(dest);
0689         QCOMPARE(srcInfo.lastModified(), destInfo.lastModified());
0690     }
0691 #endif
0692 
0693     QCOMPARE(job->totalAmount(KJob::Files), 2); // testfile and testlink
0694     QCOMPARE(job->totalAmount(KJob::Directories), 1);
0695     QCOMPARE(job->processedAmount(KJob::Files), 2);
0696     QCOMPARE(job->processedAmount(KJob::Directories), 1);
0697     QCOMPARE(job->percent(), 100);
0698 
0699     // Do it again, with Overwrite.
0700     // Use copyAs, we don't want a subdir inside d.
0701     job = KIO::copyAs(u, d, KIO::HideProgressInfo | KIO::Overwrite);
0702     job->setUiDelegate(nullptr);
0703     job->setUiDelegateExtension(nullptr);
0704     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0705 
0706     QCOMPARE(job->totalAmount(KJob::Files), 2); // testfile and testlink
0707     QCOMPARE(job->totalAmount(KJob::Directories), 1);
0708     QCOMPARE(job->processedAmount(KJob::Files), 2);
0709     QCOMPARE(job->processedAmount(KJob::Directories), 1);
0710     QCOMPARE(job->percent(), 100);
0711 
0712     // Do it again, without Overwrite (should fail).
0713     job = KIO::copyAs(u, d, KIO::HideProgressInfo);
0714     job->setUiDelegate(nullptr);
0715     job->setUiDelegateExtension(nullptr);
0716     QVERIFY(!job->exec());
0717 }
0718 
0719 #ifndef Q_OS_WIN
0720 static QString linkTarget(const QString &path)
0721 {
0722     // Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927)
0723     char linkTargetBuffer[4096];
0724     const int n = readlink(QFile::encodeName(path).constData(), linkTargetBuffer, sizeof(linkTargetBuffer) - 1);
0725     if (n != -1) {
0726         linkTargetBuffer[n] = 0;
0727     }
0728     return QFile::decodeName(linkTargetBuffer);
0729 }
0730 
0731 static void copyLocalSymlink(const QString &src, const QString &dest, const QString &expectedLinkTarget)
0732 {
0733     QT_STATBUF buf;
0734     QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0);
0735     QUrl u = QUrl::fromLocalFile(src);
0736     QUrl d = QUrl::fromLocalFile(dest);
0737 
0738     // copy the symlink
0739     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
0740     job->setUiDelegate(nullptr);
0741     job->setUiDelegateExtension(nullptr);
0742     QVERIFY2(job->exec(), qPrintable(QString::number(job->error())));
0743     QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0); // dest exists
0744     QCOMPARE(linkTarget(dest), expectedLinkTarget);
0745 
0746     // cleanup
0747     QFile::remove(dest);
0748 }
0749 #endif
0750 
0751 void JobTest::copyFileToSamePartition()
0752 {
0753     const QString homeDir = homeTmpDir();
0754     const QString filePath = homeDir + "fileFromHome";
0755     const QString dest = homeDir + "fileFromHome_copied";
0756     createTestFile(filePath);
0757     if (checkXattrFsSupport(homeDir)) {
0758         setXattr(filePath);
0759     }
0760     copyLocalFile(filePath, dest);
0761 }
0762 
0763 void JobTest::copyDirectoryToSamePartition()
0764 {
0765     // qDebug();
0766     const QString src = homeTmpDir() + "dirFromHome";
0767     const QString dest = homeTmpDir() + "dirFromHome_copied";
0768     createTestDirectory(src);
0769     copyLocalDirectory(src, dest);
0770 }
0771 
0772 void JobTest::copyDirectoryToExistingDirectory()
0773 {
0774     // qDebug();
0775     // just the same as copyDirectoryToSamePartition, but this time dest exists.
0776     // So we get a subdir, "dirFromHome_copy/dirFromHome"
0777     const QString src = homeTmpDir() + "dirFromHome";
0778     const QString dest = homeTmpDir() + "dirFromHome_copied";
0779     createTestDirectory(src);
0780     createTestDirectory(dest);
0781     copyLocalDirectory(src, dest, AlreadyExists);
0782 }
0783 
0784 void JobTest::copyDirectoryToExistingSymlinkedDirectory()
0785 {
0786     // qDebug();
0787     // just the same as copyDirectoryToSamePartition, but this time dest is a symlink.
0788     // So we get a file in the symlink dir, "dirFromHome_symlink/dirFromHome" and
0789     // "dirFromHome_symOrigin/dirFromHome"
0790     const QString src = homeTmpDir() + "dirFromHome";
0791     const QString origSymlink = homeTmpDir() + "dirFromHome_symOrigin";
0792     const QString targetSymlink = homeTmpDir() + "dirFromHome_symlink";
0793     createTestDirectory(src);
0794     createTestDirectory(origSymlink);
0795 
0796     bool ok = KIOPrivate::createSymlink(origSymlink, targetSymlink);
0797     if (!ok) {
0798         qFatal("couldn't create symlink: %s", strerror(errno));
0799     }
0800     QVERIFY(QFileInfo(targetSymlink).isSymLink());
0801     QVERIFY(QFileInfo(targetSymlink).isDir());
0802 
0803     KIO::Job *job = KIO::copy(QUrl::fromLocalFile(src), QUrl::fromLocalFile(targetSymlink), KIO::HideProgressInfo);
0804     job->setUiDelegate(nullptr);
0805     job->setUiDelegateExtension(nullptr);
0806     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0807     QVERIFY(QFile::exists(src)); // still there
0808 
0809     // file is visible in both places due to symlink
0810     QVERIFY(QFileInfo(origSymlink + "/dirFromHome").isDir());
0811     ;
0812     QVERIFY(QFileInfo(targetSymlink + "/dirFromHome").isDir());
0813     QVERIFY(QDir(origSymlink).removeRecursively());
0814     QVERIFY(QFile::remove(targetSymlink));
0815 }
0816 
0817 void JobTest::copyFileToOtherPartition()
0818 {
0819     // qDebug();
0820     const QString homeDir = homeTmpDir();
0821     const QString otherHomeDir = otherTmpDir();
0822     const QString filePath = homeDir + "fileFromHome";
0823     const QString dest = otherHomeDir + "fileFromHome_copied";
0824     bool canRead = checkXattrFsSupport(homeDir);
0825     bool canWrite = checkXattrFsSupport(otherHomeDir);
0826     createTestFile(filePath);
0827     if (canRead && canWrite) {
0828         setXattr(filePath);
0829     }
0830     copyLocalFile(filePath, dest);
0831 }
0832 
0833 // Same partition doesn't matter as much as copying to the same filesystem type
0834 // to be sure it supports the same permissions
0835 void JobTest::testCopyFilePermissionsToSamePartition()
0836 {
0837 #if defined(Q_OS_UNIX)
0838     const QString homeDir = homeTmpDir();
0839     const QString src = homeDir + "fileFromHome";
0840     const QUrl u = QUrl::fromLocalFile(src);
0841     createTestFile(src);
0842 
0843     const QByteArray src_c = QFile::encodeName(src).constData();
0844     QT_STATBUF src_buff;
0845     QCOMPARE(QT_LSTAT(src_c.constData(), &src_buff), 0); // Exists
0846     // Default system permissions for newly created files, umask et al.
0847     const mode_t systemDefaultPerms = src_buff.st_mode;
0848 
0849     const QString dest = homeDir + "fileFromHome_copied";
0850     const QUrl d = QUrl::fromLocalFile(dest);
0851 
0852     const QByteArray dest_c = QFile::encodeName(dest).constData();
0853     QT_STATBUF dest_buff;
0854 
0855     // Copy the file, with -1 permissions (i.e. don't touch dest permissions)
0856     auto copyStat = [&](const mode_t perms) {
0857         KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo);
0858         job->setUiDelegate(nullptr);
0859         QVERIFY2(job->exec(), qPrintable(job->errorString()));
0860         QCOMPARE(QT_LSTAT(dest_c.constData(), &dest_buff), 0);
0861     };
0862 
0863     copyStat(-1);
0864     // dest should have system default permissions
0865     QCOMPARE(dest_buff.st_mode, systemDefaultPerms);
0866 
0867     QVERIFY(QFile::remove(dest));
0868     // Change src permissions to non-default
0869     QCOMPARE(::chmod(src_c.constData(), S_IRUSR), 0);
0870     // Copy the file again, permissions -1 (i.e. don't touch dest permissions)
0871     copyStat(-1);
0872     // dest should have system default permissions, not src's ones
0873     QCOMPARE(dest_buff.st_mode, systemDefaultPerms);
0874 
0875     QVERIFY(QFile::remove(dest));
0876     // Copy the file again, explicitly setting the permissions to the src ones
0877     copyStat(src_buff.st_mode);
0878     // dest should have same permissions as src
0879     QCOMPARE(dest_buff.st_mode, dest_buff.st_mode);
0880 
0881     QVERIFY(QFile::remove(dest));
0882     // Copy the file again, explicitly setting some other permissions
0883     copyStat(S_IWUSR);
0884     // dest should have S_IWUSR
0885     QCOMPARE(dest_buff.st_mode & 0777, S_IWUSR);
0886 
0887     // Clean up, the weird permissions used above mess with the next
0888     // unit tests
0889     QVERIFY(QFile::remove(dest));
0890     QVERIFY(QFile::remove(src));
0891 #endif
0892 }
0893 
0894 void JobTest::copyDirectoryToOtherPartition()
0895 {
0896     // qDebug();
0897     const QString src = homeTmpDir() + "dirFromHome";
0898     const QString dest = otherTmpDir() + "dirFromHome_copied";
0899     createTestDirectory(src);
0900     copyLocalDirectory(src, dest);
0901 }
0902 
0903 void JobTest::copyRelativeSymlinkToSamePartition() // #352927
0904 {
0905 #ifdef Q_OS_WIN
0906     QSKIP("Skipping symlink test on Windows");
0907 #else
0908     const QString filePath = homeTmpDir() + "testlink";
0909     const QString dest = homeTmpDir() + "testlink_copied";
0910     createTestSymlink(filePath, "relative");
0911     copyLocalSymlink(filePath, dest, QStringLiteral("relative"));
0912     QFile::remove(filePath);
0913 #endif
0914 }
0915 
0916 void JobTest::copyAbsoluteSymlinkToOtherPartition()
0917 {
0918 #ifdef Q_OS_WIN
0919     QSKIP("Skipping symlink test on Windows");
0920 #else
0921     const QString filePath = homeTmpDir() + "testlink";
0922     const QString dest = otherTmpDir() + "testlink_copied";
0923     createTestSymlink(filePath, QFile::encodeName(homeTmpDir()));
0924     copyLocalSymlink(filePath, dest, homeTmpDir());
0925     QFile::remove(filePath);
0926 #endif
0927 }
0928 
0929 void JobTest::copyFolderWithUnaccessibleSubfolder()
0930 {
0931 #ifdef Q_OS_WIN
0932     QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder");
0933 #endif
0934     QTemporaryDir dir(homeTmpDir() + "UnaccessibleSubfolderTest");
0935     QVERIFY(dir.isValid());
0936     const QString src_dir = dir.path() + "srcHome";
0937     const QString dst_dir = dir.path() + "dstHome";
0938 
0939     QDir().remove(src_dir);
0940     QDir().remove(dst_dir);
0941 
0942     createTestDirectory(src_dir);
0943     createTestDirectory(src_dir + "/folder1");
0944     QString inaccessible = src_dir + "/folder1/inaccessible";
0945 
0946     createTestDirectory(inaccessible);
0947 
0948     QFile(inaccessible).setPermissions(QFile::Permissions()); // Make it inaccessible
0949     // Copying should throw some warnings, as it cannot access some folders
0950 
0951     ScopedCleaner cleaner([&] {
0952         QFile(inaccessible).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner));
0953 
0954         qDebug() << "Cleaning up" << src_dir << "and" << dst_dir;
0955         KIO::DeleteJob *deljob1 = KIO::del(QUrl::fromLocalFile(src_dir), KIO::HideProgressInfo);
0956         deljob1->setUiDelegate(nullptr); // no skip dialog, thanks
0957         const bool job1OK = deljob1->exec();
0958         QVERIFY(job1OK);
0959 
0960         KIO::DeleteJob *deljob2 = KIO::del(QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo);
0961         deljob2->setUiDelegate(nullptr); // no skip dialog, thanks
0962         const bool job2OK = deljob2->exec();
0963         QVERIFY(job2OK);
0964 
0965         qDebug() << "Result:" << job1OK << job2OK;
0966     });
0967 
0968     KIO::CopyJob *job = KIO::copy(QUrl::fromLocalFile(src_dir), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo);
0969 
0970     QSignalSpy spy(job, &KJob::warning);
0971     job->setUiDelegate(nullptr); // no skip dialog, thanks
0972     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0973 
0974     QCOMPARE(job->totalAmount(KJob::Files), 4); // testfile, testlink, folder1/testlink, folder1/testfile
0975     QCOMPARE(job->totalAmount(KJob::Directories), 3); // srcHome, srcHome/folder1, srcHome/folder1/inaccessible
0976     QCOMPARE(job->processedAmount(KJob::Files), 4);
0977     QCOMPARE(job->processedAmount(KJob::Directories), 3);
0978     QCOMPARE(job->percent(), 100);
0979 
0980     QCOMPARE(spy.count(), 1); // one warning should be emitted by the copy job
0981 }
0982 
0983 void JobTest::copyDataUrl()
0984 {
0985     // GIVEN
0986     const QString dst_dir = homeTmpDir();
0987     QVERIFY(!QFileInfo::exists(dst_dir + "/data"));
0988     // WHEN
0989     KIO::CopyJob *job = KIO::copy(QUrl("data:,Hello%2C%20World!"), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo);
0990     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0991     // THEN
0992     const QFileInfo fileInfo(dst_dir + "/data");
0993     QVERIFY(fileInfo.isFile());
0994     QCOMPARE(fileInfo.size(), 13);
0995     QFile::remove(dst_dir + "/data");
0996 }
0997 
0998 void JobTest::suspendFileCopy()
0999 {
1000     const QString filePath = homeTmpDir() + "fileFromHome";
1001     const QString dest = homeTmpDir() + "fileFromHome_copied";
1002     createTestFile(filePath);
1003 
1004     const QUrl u = QUrl::fromLocalFile(filePath);
1005     const QUrl d = QUrl::fromLocalFile(dest);
1006     KIO::Job *job = KIO::file_copy(u, d, KIO::HideProgressInfo);
1007     QSignalSpy spyResult(job, &KJob::result);
1008     job->setUiDelegate(nullptr);
1009     job->setUiDelegateExtension(nullptr);
1010     QVERIFY(job->suspend());
1011     QVERIFY(!spyResult.wait(300));
1012     QVERIFY(job->resume());
1013     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1014     QVERIFY(QFile::exists(dest));
1015     QFile::remove(dest);
1016 }
1017 
1018 void JobTest::suspendCopy()
1019 {
1020     const QString filePath = homeTmpDir() + "fileFromHome";
1021     const QString dest = homeTmpDir() + "fileFromHome_copied";
1022     createTestFile(filePath);
1023 
1024     const QUrl u = QUrl::fromLocalFile(filePath);
1025     const QUrl d = QUrl::fromLocalFile(dest);
1026     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
1027     QSignalSpy spyResult(job, &KJob::result);
1028     job->setUiDelegate(nullptr);
1029     job->setUiDelegateExtension(nullptr);
1030     QVERIFY(job->suspend());
1031     QVERIFY(!spyResult.wait(300));
1032     QVERIFY(job->resume());
1033     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1034     QVERIFY(QFile::exists(dest));
1035     QFile::remove(dest);
1036 }
1037 
1038 void JobTest::moveLocalFile(const QString &src, const QString &dest)
1039 {
1040     QVERIFY(QFile::exists(src));
1041     QUrl u = QUrl::fromLocalFile(src);
1042     QUrl d = QUrl::fromLocalFile(dest);
1043 
1044     // move the file with file_move
1045     KIO::Job *job = KIO::file_move(u, d, 0666, KIO::HideProgressInfo);
1046     job->setUiDelegate(nullptr);
1047     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1048     QVERIFY(QFile::exists(dest));
1049     QVERIFY(!QFile::exists(src)); // not there anymore
1050     QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666));
1051 
1052     // move it back with KIO::move()
1053     job = KIO::move(d, u, KIO::HideProgressInfo);
1054     job->setUiDelegate(nullptr);
1055     job->setUiDelegateExtension(nullptr);
1056     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1057     QVERIFY(!QFile::exists(dest));
1058     QVERIFY(QFile::exists(src)); // it's back
1059 }
1060 
1061 static void moveLocalSymlink(const QString &src, const QString &dest)
1062 {
1063     QT_STATBUF buf;
1064     QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0);
1065     QUrl u = QUrl::fromLocalFile(src);
1066     QUrl d = QUrl::fromLocalFile(dest);
1067 
1068     // move the symlink with move, NOT with file_move
1069     KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo);
1070     job->setUiDelegate(nullptr);
1071     job->setUiDelegateExtension(nullptr);
1072     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1073     QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0);
1074     QVERIFY(!QFile::exists(src)); // not there anymore
1075 
1076     // move it back with KIO::move()
1077     job = KIO::move(d, u, KIO::HideProgressInfo);
1078     job->setUiDelegate(nullptr);
1079     job->setUiDelegateExtension(nullptr);
1080     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1081     QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) != 0); // doesn't exist anymore
1082     QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); // it's back
1083 }
1084 
1085 void JobTest::moveLocalDirectory(const QString &src, const QString &dest)
1086 {
1087     qDebug() << src << " " << dest;
1088     QVERIFY(QFile::exists(src));
1089     QVERIFY(QFileInfo(src).isDir());
1090     QVERIFY(QFileInfo(src + "/testfile").isFile());
1091 #ifndef Q_OS_WIN
1092     QVERIFY(QFileInfo(src + "/testlink").isSymLink());
1093 #endif
1094     QUrl u = QUrl::fromLocalFile(src);
1095     QUrl d = QUrl::fromLocalFile(dest);
1096 
1097     KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo);
1098     job->setUiDelegate(nullptr);
1099     job->setUiDelegateExtension(nullptr);
1100     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1101     QVERIFY(QFile::exists(dest));
1102     QVERIFY(QFileInfo(dest).isDir());
1103     QVERIFY(QFileInfo(dest + "/testfile").isFile());
1104     QVERIFY(!QFile::exists(src)); // not there anymore
1105 #ifndef Q_OS_WIN
1106     QVERIFY(QFileInfo(dest + "/testlink").isSymLink());
1107 #endif
1108 }
1109 
1110 void JobTest::moveFileToSamePartition()
1111 {
1112     qDebug();
1113     const QString filePath = homeTmpDir() + "fileFromHome";
1114     const QString dest = homeTmpDir() + "fileFromHome_moved";
1115     createTestFile(filePath);
1116     moveLocalFile(filePath, dest);
1117 }
1118 
1119 void JobTest::moveDirectoryToSamePartition()
1120 {
1121     qDebug();
1122     const QString src = homeTmpDir() + "dirFromHome";
1123     const QString dest = homeTmpDir() + "dirFromHome_moved";
1124     createTestDirectory(src);
1125     moveLocalDirectory(src, dest);
1126 }
1127 
1128 void JobTest::moveDirectoryIntoItself()
1129 {
1130     qDebug();
1131     const QString src = homeTmpDir() + "dirFromHome";
1132     const QString dest = src + "/foo";
1133     createTestDirectory(src);
1134     QVERIFY(QFile::exists(src));
1135     QUrl u = QUrl::fromLocalFile(src);
1136     QUrl d = QUrl::fromLocalFile(dest);
1137     KIO::CopyJob *job = KIO::move(u, d);
1138     QVERIFY(!job->exec());
1139     QCOMPARE(job->error(), (int)KIO::ERR_CANNOT_MOVE_INTO_ITSELF);
1140     QCOMPARE(job->errorString(), i18n("A folder cannot be moved into itself"));
1141     QDir(dest).removeRecursively();
1142 }
1143 
1144 void JobTest::moveFileToOtherPartition()
1145 {
1146     qDebug();
1147     const QString filePath = homeTmpDir() + "fileFromHome";
1148     const QString dest = otherTmpDir() + "fileFromHome_moved";
1149     createTestFile(filePath);
1150     moveLocalFile(filePath, dest);
1151 }
1152 
1153 void JobTest::moveSymlinkToOtherPartition()
1154 {
1155 #ifndef Q_OS_WIN
1156     qDebug();
1157     const QString filePath = homeTmpDir() + "testlink";
1158     const QString dest = otherTmpDir() + "testlink_moved";
1159     createTestSymlink(filePath);
1160     moveLocalSymlink(filePath, dest);
1161 #endif
1162 }
1163 
1164 void JobTest::moveDirectoryToOtherPartition()
1165 {
1166     qDebug();
1167 #ifndef Q_OS_WIN
1168     const QString src = homeTmpDir() + "dirFromHome";
1169     const QString dest = otherTmpDir() + "dirFromHome_moved";
1170     createTestDirectory(src);
1171     moveLocalDirectory(src, dest);
1172 #endif
1173 }
1174 
1175 struct CleanupInaccessibleSubdir {
1176     explicit CleanupInaccessibleSubdir(const QString &subdir)
1177         : subdir(subdir)
1178     {
1179     }
1180     ~CleanupInaccessibleSubdir()
1181     {
1182         QVERIFY(QFile(subdir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)));
1183         QVERIFY(QDir(subdir).removeRecursively());
1184     }
1185 
1186 private:
1187     const QString subdir;
1188 };
1189 
1190 void JobTest::moveFileNoPermissions()
1191 {
1192 #ifdef Q_OS_WIN
1193     QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder");
1194 #endif
1195     // Given a file that cannot be moved (subdir has no permissions)
1196     const QString subdir = homeTmpDir() + "subdir";
1197     QVERIFY(QDir().mkpath(subdir));
1198     const QString src = subdir + "/thefile";
1199     createTestFile(src);
1200     QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible
1201     CleanupInaccessibleSubdir c(subdir);
1202 
1203     // When trying to move it
1204     const QString dest = homeTmpDir() + "dest";
1205     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1206     job->setUiDelegate(nullptr);
1207     job->setUiDelegateExtension(nullptr); // no skip dialog, thanks
1208 
1209     // The job should fail with "access denied"
1210     QVERIFY(!job->exec());
1211     QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED);
1212     // Note that, just like mv(1), KIO's behavior depends on whether
1213     // a direct rename(2) was used, or a full copy+del. In the first case
1214     // there is no destination file created, but in the second case the
1215     // destination file remains.
1216     // In this test it's the same partition, so no dest created.
1217     QVERIFY(!QFile::exists(dest));
1218 
1219     QCOMPARE(job->totalAmount(KJob::Files), 1);
1220     QCOMPARE(job->totalAmount(KJob::Directories), 0);
1221     QCOMPARE(job->processedAmount(KJob::Files), 0);
1222     QCOMPARE(job->processedAmount(KJob::Directories), 0);
1223     QCOMPARE(job->percent(), 0);
1224 }
1225 
1226 void JobTest::moveDirectoryNoPermissions()
1227 {
1228 #ifdef Q_OS_WIN
1229     QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder");
1230 #endif
1231     // Given a dir that cannot be moved (parent dir has no permissions)
1232     const QString subdir = homeTmpDir() + "subdir";
1233     const QString src = subdir + "/thedir";
1234     QVERIFY(QDir().mkpath(src));
1235     QVERIFY(QFileInfo(src).isDir());
1236     QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible
1237     CleanupInaccessibleSubdir c(subdir);
1238 
1239     // When trying to move it
1240     const QString dest = homeTmpDir() + "mdnp";
1241     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1242     job->setUiDelegate(nullptr);
1243     job->setUiDelegateExtension(nullptr); // no skip dialog, thanks
1244 
1245     // The job should fail with "access denied"
1246     QVERIFY(!job->exec());
1247     QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED);
1248 
1249     QVERIFY(!QFile::exists(dest));
1250 
1251     QCOMPARE(job->totalAmount(KJob::Files), 1);
1252     QCOMPARE(job->totalAmount(KJob::Directories), 0);
1253     QCOMPARE(job->processedAmount(KJob::Files), 0);
1254     QCOMPARE(job->processedAmount(KJob::Directories), 0);
1255     QCOMPARE(job->percent(), 0);
1256 }
1257 
1258 void JobTest::moveDirectoryToReadonlyFilesystem_data()
1259 {
1260     QTest::addColumn<QList<QUrl>>("sources");
1261     QTest::addColumn<int>("expectedErrorCode");
1262 
1263     const QString srcFileHomePath = homeTmpDir() + "srcFileHome";
1264     const QUrl srcFileHome = QUrl::fromLocalFile(srcFileHomePath);
1265     createTestFile(srcFileHomePath);
1266 
1267     const QString srcFileOtherPath = otherTmpDir() + "srcFileOther";
1268     const QUrl srcFileOther = QUrl::fromLocalFile(srcFileOtherPath);
1269     createTestFile(srcFileOtherPath);
1270 
1271     const QString srcDirHomePath = homeTmpDir() + "srcDirHome";
1272     const QUrl srcDirHome = QUrl::fromLocalFile(srcDirHomePath);
1273     createTestDirectory(srcDirHomePath);
1274 
1275     const QString srcDirHome2Path = homeTmpDir() + "srcDirHome2";
1276     const QUrl srcDirHome2 = QUrl::fromLocalFile(srcDirHome2Path);
1277     createTestDirectory(srcDirHome2Path);
1278 
1279     const QString srcDirOtherPath = otherTmpDir() + "srcDirOther";
1280     const QUrl srcDirOther = QUrl::fromLocalFile(srcDirOtherPath);
1281     createTestDirectory(srcDirOtherPath);
1282 
1283     QTest::newRow("file_same_partition") << QList<QUrl>{srcFileHome} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1284     QTest::newRow("file_other_partition") << QList<QUrl>{srcFileOther} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1285     QTest::newRow("one_dir_same_partition") << QList<QUrl>{srcDirHome} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1286     QTest::newRow("one_dir_other_partition") << QList<QUrl>{srcDirOther} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1287     QTest::newRow("dirs_same_partition") << QList<QUrl>{srcDirHome, srcDirHome2} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1288     QTest::newRow("dirs_both_partitions") << QList<QUrl>{srcDirOther, srcDirHome} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1289 }
1290 
1291 void JobTest::moveDirectoryToReadonlyFilesystem()
1292 {
1293     QFETCH(QList<QUrl>, sources);
1294     QFETCH(int, expectedErrorCode);
1295 
1296     const QString dst_dir = homeTmpDir() + "readonlyDest";
1297     const QUrl dst = QUrl::fromLocalFile(dst_dir);
1298     QVERIFY2(QDir().mkdir(dst_dir), qPrintable(dst_dir));
1299     QFile(dst_dir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::ExeOwner)); // Make it readonly, moving should throw some errors
1300 
1301     ScopedCleaner cleaner([&] {
1302         QVERIFY(QFile(dst_dir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)));
1303         QVERIFY(QDir(dst_dir).removeRecursively());
1304     });
1305 
1306     KIO::CopyJob *job = KIO::move(sources, dst, KIO::HideProgressInfo | KIO::NoPrivilegeExecution);
1307     job->setUiDelegate(nullptr);
1308     QVERIFY(!job->exec());
1309     QCOMPARE(job->error(), expectedErrorCode);
1310     for (const QUrl &srcUrl : std::as_const(sources)) {
1311         QVERIFY(QFileInfo::exists(srcUrl.toLocalFile())); // no moving happened
1312     }
1313 
1314     KIO::CopyJob *job2 = KIO::move(sources, dst, KIO::HideProgressInfo);
1315     job2->setUiDelegate(nullptr);
1316     QVERIFY(!job2->exec());
1317     if (job2->error() != KIO::ERR_CANNOT_MKDIR) { // This can happen when moving between partitions, but on CI it's the same partition so allow both
1318         QCOMPARE(job2->error(), expectedErrorCode);
1319     }
1320     for (const QUrl &srcUrl : std::as_const(sources)) {
1321         QVERIFY(QFileInfo::exists(srcUrl.toLocalFile())); // no moving happened
1322     }
1323 }
1324 
1325 static QByteArray expectedListRecursiveOutput()
1326 {
1327     return QByteArray(
1328         ".,..,"
1329         "dirFromHome,dirFromHome/testfile,"
1330         "dirFromHome/testlink," // exists on Windows too, see createTestDirectory
1331         "dirFromHome_copied,"
1332         "dirFromHome_copied/dirFromHome,dirFromHome_copied/dirFromHome/testfile,"
1333         "dirFromHome_copied/dirFromHome/testlink,"
1334         "dirFromHome_copied/testfile,"
1335         "dirFromHome_copied/testlink,"
1336 #ifndef Q_OS_WIN
1337         "dirFromHome_link,"
1338 #endif
1339         "fileFromHome");
1340 }
1341 
1342 void JobTest::listRecursive()
1343 {
1344     // Note: many other tests must have been run before since we rely on the files they created
1345 
1346     const QString src = homeTmpDir();
1347 #ifndef Q_OS_WIN
1348     // Add a symlink to a dir, to make sure we don't recurse into those
1349     bool symlinkOk = symlink("dirFromHome", QFile::encodeName(src + "/dirFromHome_link").constData()) == 0;
1350     QVERIFY(symlinkOk);
1351 #endif
1352     m_names.clear();
1353     KIO::ListJob *job = KIO::listRecursive(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1354     job->setUiDelegate(nullptr);
1355     connect(job, &KIO::ListJob::entries, this, &JobTest::slotEntries);
1356     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1357     m_names.sort();
1358     const QByteArray ref_names = expectedListRecursiveOutput();
1359     const QString joinedNames = m_names.join(QLatin1Char(','));
1360     if (joinedNames.toLatin1() != ref_names) {
1361         qDebug("%s", qPrintable(joinedNames));
1362         qDebug("%s", ref_names.data());
1363     }
1364     QCOMPARE(joinedNames.toLatin1(), ref_names);
1365 }
1366 
1367 void JobTest::multipleListRecursive()
1368 {
1369     // Note: listRecursive() must have been run first
1370     const QString src = homeTmpDir();
1371     m_names.clear();
1372     QVector<KIO::ListJob *> jobs;
1373     for (int i = 0; i < 100; ++i) {
1374         KIO::ListJob *job = KIO::listRecursive(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1375         job->setUiDelegate(nullptr);
1376         if (i == 6) {
1377             connect(job, &KIO::ListJob::entries, this, &JobTest::slotEntries);
1378         }
1379         connect(job, &KJob::result, this, [&jobs, job]() {
1380             jobs.removeOne(job);
1381         });
1382         jobs.push_back(job);
1383     }
1384     QTRY_VERIFY(jobs.isEmpty());
1385 
1386     m_names.sort();
1387     const QByteArray ref_names = expectedListRecursiveOutput();
1388     const QString joinedNames = m_names.join(QLatin1Char(','));
1389     if (joinedNames.toLatin1() != ref_names) {
1390         qDebug("%s", qPrintable(joinedNames));
1391         qDebug("%s", ref_names.data());
1392     }
1393     QCOMPARE(joinedNames.toLatin1(), ref_names);
1394 }
1395 
1396 void JobTest::listFile()
1397 {
1398     const QString filePath = homeTmpDir() + "fileFromHome";
1399     createTestFile(filePath);
1400     KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
1401     job->setUiDelegate(nullptr);
1402     QVERIFY(!job->exec());
1403     QCOMPARE(job->error(), static_cast<int>(KIO::ERR_IS_FILE));
1404 
1405     // And list something that doesn't exist
1406     const QString path = homeTmpDir() + "fileFromHomeDoesNotExist";
1407     job = KIO::listDir(QUrl::fromLocalFile(path), KIO::HideProgressInfo);
1408     job->setUiDelegate(nullptr);
1409     QVERIFY(!job->exec());
1410     QCOMPARE(job->error(), static_cast<int>(KIO::ERR_DOES_NOT_EXIST));
1411 }
1412 
1413 void JobTest::killJob()
1414 {
1415     const QString src = homeTmpDir();
1416     KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1417     QVERIFY(job->isAutoDelete());
1418     QPointer<KIO::ListJob> ptr(job);
1419     job->setUiDelegate(nullptr);
1420     qApp->processEvents(); // let the job start, it's no fun otherwise
1421     job->kill();
1422     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job
1423     QVERIFY(ptr.isNull());
1424 }
1425 
1426 void JobTest::killJobBeforeStart()
1427 {
1428     const QString src = homeTmpDir();
1429     KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1430     QVERIFY(job->isAutoDelete());
1431     QPointer<KIO::Job> ptr(job);
1432     job->setUiDelegate(nullptr);
1433     job->kill();
1434     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job
1435     QVERIFY(ptr.isNull());
1436     qApp->processEvents(); // does KIO scheduler crash here? nope.
1437 }
1438 
1439 void JobTest::deleteJobBeforeStart() // #163171
1440 {
1441     const QString src = homeTmpDir();
1442     KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1443     QVERIFY(job->isAutoDelete());
1444     job->setUiDelegate(nullptr);
1445     delete job;
1446     qApp->processEvents(); // does KIO scheduler crash here?
1447 }
1448 
1449 void JobTest::directorySize()
1450 {
1451     // Note: many other tests must have been run before since we rely on the files they created
1452 
1453     const QString src = homeTmpDir();
1454 
1455     KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(src));
1456     job->setUiDelegate(nullptr);
1457     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1458     qDebug() << "totalSize: " << job->totalSize();
1459     qDebug() << "totalFiles: " << job->totalFiles();
1460     qDebug() << "totalSubdirs: " << job->totalSubdirs();
1461 #ifdef Q_OS_WIN
1462     QCOMPARE(job->totalFiles(), 5ULL); // see expected result in listRecursive() above
1463     QCOMPARE(job->totalSubdirs(), 3ULL); // see expected result in listRecursive() above
1464     QVERIFY(job->totalSize() > 54);
1465 #else
1466     QCOMPARE(job->totalFiles(), 7ULL); // see expected result in listRecursive() above
1467     QCOMPARE(job->totalSubdirs(), 4ULL); // see expected result in listRecursive() above
1468     QVERIFY2(job->totalSize() >= 60,
1469              qPrintable(QString("totalSize was %1").arg(job->totalSize()))); // size of subdir entries is filesystem dependent. E.g. this is 16428 with ext4 but
1470                                                                              // only 272 with xfs, and 63 on FreeBSD
1471 #endif
1472 
1473     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete);
1474 }
1475 
1476 void JobTest::directorySizeError()
1477 {
1478     KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(QStringLiteral("/I/Dont/Exist")));
1479     job->setUiDelegate(nullptr);
1480     QVERIFY(!job->exec());
1481     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete);
1482 }
1483 
1484 void JobTest::slotEntries(KIO::Job *, const KIO::UDSEntryList &lst)
1485 {
1486     for (KIO::UDSEntryList::ConstIterator it = lst.begin(); it != lst.end(); ++it) {
1487         QString displayName = (*it).stringValue(KIO::UDSEntry::UDS_NAME);
1488         // QUrl url = (*it).stringValue( KIO::UDSEntry::UDS_URL );
1489         m_names.append(displayName);
1490     }
1491 }
1492 
1493 void JobTest::calculateRemainingSeconds()
1494 {
1495     unsigned int seconds = KIO::calculateRemainingSeconds(2 * 86400 - 60, 0, 1);
1496     QCOMPARE(seconds, static_cast<unsigned int>(2 * 86400 - 60));
1497     QString text = KIO::convertSeconds(seconds);
1498     QCOMPARE(text, i18n("1 day 23:59:00"));
1499 
1500     seconds = KIO::calculateRemainingSeconds(520, 20, 10);
1501     QCOMPARE(seconds, static_cast<unsigned int>(50));
1502     text = KIO::convertSeconds(seconds);
1503     QCOMPARE(text, i18n("00:00:50"));
1504 }
1505 
1506 void JobTest::getInvalidUrl()
1507 {
1508     QUrl url(QStringLiteral("http://strange<hostname>/"));
1509     QVERIFY(!url.isValid());
1510 
1511     KIO::SimpleJob *job = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo);
1512     QVERIFY(job != nullptr);
1513     job->setUiDelegate(nullptr);
1514 
1515     QVERIFY(!job->exec()); // it should fail :)
1516 }
1517 
1518 void JobTest::slotMimetype(KIO::Job *job, const QString &type)
1519 {
1520     QVERIFY(job != nullptr);
1521     m_mimetype = type;
1522 }
1523 
1524 void JobTest::deleteFile()
1525 {
1526     const QString dest = otherTmpDir() + "fileFromHome_copied";
1527     createTestFile(dest);
1528     KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1529     job->setUiDelegate(nullptr);
1530     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1531     QVERIFY(!QFile::exists(dest));
1532 }
1533 
1534 void JobTest::deleteDirectory()
1535 {
1536     const QString dest = otherTmpDir() + "dirFromHome_copied";
1537     if (!QFile::exists(dest)) {
1538         createTestDirectory(dest);
1539     }
1540     // Let's put a few things in there to see if the recursive deletion works correctly
1541     // A hidden file:
1542     createTestFile(dest + "/.hidden");
1543 #ifndef Q_OS_WIN
1544     // A broken symlink:
1545     createTestSymlink(dest + "/broken_symlink");
1546     // A symlink to a dir:
1547     const auto srcFileName = QFile::encodeName(QFileInfo(QFINDTESTDATA("jobtest.cpp")).absolutePath()).constData();
1548     const auto symLinkFileName = QFile::encodeName(dest + "/symlink_to_dir").constData();
1549     if (symlink(srcFileName, symLinkFileName) != 0) {
1550         qFatal("couldn't create symlink: %s", strerror(errno));
1551     }
1552 #endif
1553 
1554     KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1555     job->setUiDelegate(nullptr);
1556     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1557     QVERIFY(!QFile::exists(dest));
1558 }
1559 
1560 void JobTest::deleteSymlink(bool using_fast_path)
1561 {
1562     extern KIOCORE_EXPORT bool kio_resolve_local_urls;
1563     kio_resolve_local_urls = !using_fast_path;
1564 
1565 #ifndef Q_OS_WIN
1566     const QString src = homeTmpDir() + "dirFromHome";
1567     createTestDirectory(src);
1568     QVERIFY(QFile::exists(src));
1569     const QString dest = homeTmpDir() + "/dirFromHome_link";
1570     if (!QFile::exists(dest)) {
1571         // Add a symlink to a dir, to make sure we don't recurse into those
1572         bool symlinkOk = symlink(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) == 0;
1573         QVERIFY(symlinkOk);
1574         QVERIFY(QFile::exists(dest));
1575     }
1576     KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1577     job->setUiDelegate(nullptr);
1578     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1579     QVERIFY(!QFile::exists(dest));
1580     QVERIFY(QFile::exists(src));
1581 #endif
1582 
1583     kio_resolve_local_urls = true;
1584 }
1585 
1586 void JobTest::deleteSymlink()
1587 {
1588 #ifndef Q_OS_WIN
1589     deleteSymlink(true);
1590     deleteSymlink(false);
1591 #endif
1592 }
1593 
1594 void JobTest::deleteManyDirs(bool using_fast_path)
1595 {
1596     extern KIOCORE_EXPORT bool kio_resolve_local_urls;
1597     kio_resolve_local_urls = !using_fast_path;
1598 
1599     const int numDirs = 50;
1600     QList<QUrl> dirs;
1601     for (int i = 0; i < numDirs; ++i) {
1602         const QString dir = homeTmpDir() + "dir" + QString::number(i);
1603         createTestDirectory(dir);
1604         dirs << QUrl::fromLocalFile(dir);
1605     }
1606     QElapsedTimer dt;
1607     dt.start();
1608     KIO::Job *job = KIO::del(dirs, KIO::HideProgressInfo);
1609     job->setUiDelegate(nullptr);
1610     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1611     for (const QUrl &dir : std::as_const(dirs)) {
1612         QVERIFY(!QFile::exists(dir.toLocalFile()));
1613     }
1614 
1615     qDebug() << "Deleted" << numDirs << "dirs in" << dt.elapsed() << "milliseconds";
1616     kio_resolve_local_urls = true;
1617 }
1618 
1619 void JobTest::deleteManyDirs()
1620 {
1621     deleteManyDirs(true);
1622     deleteManyDirs(false);
1623 }
1624 
1625 static QList<QUrl> createManyFiles(const QString &baseDir, int numFiles)
1626 {
1627     QList<QUrl> ret;
1628     ret.reserve(numFiles);
1629     for (int i = 0; i < numFiles; ++i) {
1630         // create empty file
1631         const QString file = baseDir + QString::number(i);
1632         QFile f(file);
1633         bool ok = f.open(QIODevice::WriteOnly);
1634         if (ok) {
1635             f.write("Hello");
1636             ret.append(QUrl::fromLocalFile(file));
1637         }
1638     }
1639     return ret;
1640 }
1641 
1642 void JobTest::deleteManyFilesIndependently()
1643 {
1644     QElapsedTimer dt;
1645     dt.start();
1646     const int numFiles = 100; // Use 1000 for performance testing
1647     const QString baseDir = homeTmpDir();
1648     const QList<QUrl> urls = createManyFiles(baseDir, numFiles);
1649     QCOMPARE(urls.count(), numFiles);
1650     for (int i = 0; i < numFiles; ++i) {
1651         // delete each file independently. lots of jobs. this stress-tests kio scheduling.
1652         const QUrl url = urls.at(i);
1653         const QString file = url.toLocalFile();
1654         QVERIFY(QFile::exists(file));
1655         // qDebug() << file;
1656         KIO::Job *job = KIO::del(url, KIO::HideProgressInfo);
1657         job->setUiDelegate(nullptr);
1658         QVERIFY2(job->exec(), qPrintable(job->errorString()));
1659         QVERIFY(!QFile::exists(file));
1660     }
1661     qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds";
1662 }
1663 
1664 void JobTest::deleteManyFilesTogether(bool using_fast_path)
1665 {
1666     extern KIOCORE_EXPORT bool kio_resolve_local_urls;
1667     kio_resolve_local_urls = !using_fast_path;
1668 
1669     QElapsedTimer dt;
1670     dt.start();
1671     const int numFiles = 100; // Use 1000 for performance testing
1672     const QString baseDir = homeTmpDir();
1673     const QList<QUrl> urls = createManyFiles(baseDir, numFiles);
1674     QCOMPARE(urls.count(), numFiles);
1675 
1676     // qDebug() << file;
1677     KIO::Job *job = KIO::del(urls, KIO::HideProgressInfo);
1678     job->setUiDelegate(nullptr);
1679     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1680     qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds";
1681 
1682     kio_resolve_local_urls = true;
1683 }
1684 
1685 void JobTest::deleteManyFilesTogether()
1686 {
1687     deleteManyFilesTogether(true);
1688     deleteManyFilesTogether(false);
1689 }
1690 
1691 void JobTest::rmdirEmpty()
1692 {
1693     const QString dir = homeTmpDir() + "dir";
1694     QDir().mkdir(dir);
1695     QVERIFY(QFile::exists(dir));
1696     KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir));
1697     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1698     QVERIFY(!QFile::exists(dir));
1699 }
1700 
1701 void JobTest::rmdirNotEmpty()
1702 {
1703     const QString dir = homeTmpDir() + "dir";
1704     createTestDirectory(dir);
1705     createTestDirectory(dir + "/subdir");
1706     KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir));
1707     QVERIFY(!job->exec());
1708     QVERIFY(QFile::exists(dir));
1709 }
1710 
1711 void JobTest::stat()
1712 {
1713 #if 1
1714     const QString filePath = homeTmpDir() + "fileFromHome";
1715     createTestFile(filePath);
1716     const QUrl url(QUrl::fromLocalFile(filePath));
1717     KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo);
1718     QVERIFY(job);
1719     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1720     // TODO set setSide
1721     const KIO::UDSEntry &entry = job->statResult();
1722 
1723     // we only get filename, access, type, size, uid, gid, btime, mtime, atime
1724     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1725     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1726     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1727     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1728     QVERIFY(entry.contains(KIO::UDSEntry::UDS_USER));
1729     QVERIFY(entry.contains(KIO::UDSEntry::UDS_GROUP));
1730     // QVERIFY(entry.contains(KIO::UDSEntry::UDS_CREATION_TIME)); // only true if st_birthtime or statx is used
1731     QVERIFY(entry.contains(KIO::UDSEntry::UDS_MODIFICATION_TIME));
1732     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS_TIME));
1733     QCOMPARE(entry.count(), 8 + (entry.contains(KIO::UDSEntry::UDS_CREATION_TIME) ? 1 : 0));
1734 
1735     QVERIFY(!entry.isDir());
1736     QVERIFY(!entry.isLink());
1737     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome"));
1738 
1739     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1740     const KFileItem kioItem(entry, url);
1741     const KFileItem fileItem(url);
1742     QCOMPARE(kioItem.name(), fileItem.name());
1743     QCOMPARE(kioItem.url(), fileItem.url());
1744     QCOMPARE(kioItem.size(), fileItem.size());
1745     QCOMPARE(kioItem.user(), fileItem.user());
1746     QCOMPARE(kioItem.group(), fileItem.group());
1747     QCOMPARE(kioItem.mimetype(), fileItem.mimetype());
1748     QCOMPARE(kioItem.permissions(), fileItem.permissions());
1749     QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime));
1750     QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime));
1751 
1752 #else
1753     // Testing stat over HTTP
1754     KIO::StatJob *job = KIO::stat(QUrl("http://www.kde.org"), KIO::HideProgressInfo);
1755     QVERIFY(job);
1756     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1757     // TODO set setSide, setDetails
1758     const KIO::UDSEntry &entry = job->statResult();
1759     QVERIFY(!entry.isDir());
1760     QVERIFY(!entry.isLink());
1761     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QString());
1762 #endif
1763 }
1764 
1765 void JobTest::statDetailsBasic()
1766 {
1767     const QString filePath = homeTmpDir() + "fileFromHome";
1768     createTestFile(filePath);
1769     const QUrl url(QUrl::fromLocalFile(filePath));
1770     KIO::StatJob *job = KIO::statDetails(url, KIO::StatJob::StatSide::SourceSide, KIO::StatBasic, KIO::HideProgressInfo);
1771     QVERIFY(job);
1772     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1773     // TODO set setSide
1774     const KIO::UDSEntry &entry = job->statResult();
1775 
1776     // we only get filename, access, type, size, (no linkdest)
1777     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1778     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1779     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1780     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1781     QCOMPARE(entry.count(), 4);
1782 
1783     QVERIFY(!entry.isDir());
1784     QVERIFY(!entry.isLink());
1785     QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0);
1786     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome"));
1787 
1788     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1789     // for the requested fields
1790     const KFileItem kioItem(entry, url);
1791     const KFileItem fileItem(url);
1792     QCOMPARE(kioItem.name(), fileItem.name());
1793     QCOMPARE(kioItem.url(), fileItem.url());
1794     QCOMPARE(kioItem.size(), fileItem.size());
1795     QCOMPARE(kioItem.user(), "");
1796     QCOMPARE(kioItem.group(), "");
1797     QCOMPARE(kioItem.mimetype(), "application/octet-stream");
1798     QCOMPARE(kioItem.permissions(), 438);
1799     QCOMPARE(kioItem.time(KFileItem::ModificationTime), QDateTime());
1800     QCOMPARE(kioItem.time(KFileItem::AccessTime), QDateTime());
1801 }
1802 
1803 void JobTest::statDetailsBasicSetDetails()
1804 {
1805     const QString filePath = homeTmpDir() + "fileFromHome";
1806     createTestFile(filePath);
1807     const QUrl url(QUrl::fromLocalFile(filePath));
1808     KIO::StatJob *job = KIO::stat(url);
1809     job->setDetails(KIO::StatBasic);
1810     QVERIFY(job);
1811     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1812     // TODO set setSide
1813     const KIO::UDSEntry &entry = job->statResult();
1814 
1815     // we only get filename, access, type, size, (no linkdest)
1816     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1817     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1818     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1819     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1820     QCOMPARE(entry.count(), 4);
1821 
1822     QVERIFY(!entry.isDir());
1823     QVERIFY(!entry.isLink());
1824     QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0);
1825     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome"));
1826 
1827     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1828     // for the requested fields
1829     const KFileItem kioItem(entry, url);
1830     const KFileItem fileItem(url);
1831     QCOMPARE(kioItem.name(), fileItem.name());
1832     QCOMPARE(kioItem.url(), fileItem.url());
1833     QCOMPARE(kioItem.size(), fileItem.size());
1834     QCOMPARE(kioItem.user(), "");
1835     QCOMPARE(kioItem.group(), "");
1836     QCOMPARE(kioItem.mimetype(), "application/octet-stream");
1837     QCOMPARE(kioItem.permissions(), 438);
1838     QCOMPARE(kioItem.time(KFileItem::ModificationTime), QDateTime());
1839     QCOMPARE(kioItem.time(KFileItem::AccessTime), QDateTime());
1840 }
1841 
1842 void JobTest::statWithInode()
1843 {
1844     const QString filePath = homeTmpDir() + "fileFromHome";
1845     createTestFile(filePath);
1846     const QUrl url(QUrl::fromLocalFile(filePath));
1847     KIO::StatJob *job = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatInode);
1848     QVERIFY(job);
1849     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1850 
1851     const KIO::UDSEntry entry = job->statResult();
1852     QVERIFY(entry.contains(KIO::UDSEntry::UDS_DEVICE_ID));
1853     QVERIFY(entry.contains(KIO::UDSEntry::UDS_INODE));
1854     QCOMPARE(entry.count(), 2);
1855 
1856     const QString path = otherTmpDir() + "otherFile";
1857     createTestFile(path);
1858     const QUrl otherUrl(QUrl::fromLocalFile(path));
1859     KIO::StatJob *otherJob = KIO::statDetails(otherUrl, KIO::StatJob::SourceSide, KIO::StatInode);
1860     QVERIFY(otherJob);
1861     QVERIFY2(otherJob->exec(), qPrintable(otherJob->errorString()));
1862 
1863     const KIO::UDSEntry otherEntry = otherJob->statResult();
1864     QVERIFY(otherEntry.contains(KIO::UDSEntry::UDS_DEVICE_ID));
1865     QVERIFY(otherEntry.contains(KIO::UDSEntry::UDS_INODE));
1866     QCOMPARE(otherEntry.count(), 2);
1867 
1868     const int device = entry.numberValue(KIO::UDSEntry::UDS_DEVICE_ID);
1869     const int otherDevice = otherEntry.numberValue(KIO::UDSEntry::UDS_DEVICE_ID);
1870 
1871     // this test doesn't make sense on the CI as it's an LXC container with one partition
1872     if (otherTmpDirIsOnSamePartition()) {
1873         // On the CI where the two tmp dirs are on the only partition available
1874         // in the LXC container, the device ID's would be identical
1875         QCOMPARE(device, otherDevice);
1876     } else {
1877         QVERIFY(device != otherDevice);
1878     }
1879 }
1880 
1881 #ifndef Q_OS_WIN
1882 void JobTest::statSymlink()
1883 {
1884     const QString filePath = homeTmpDir() + "fileFromHome";
1885     createTestFile(filePath);
1886     const QString symlink = otherTmpDir() + "link";
1887     QVERIFY(QFile(filePath).link(symlink));
1888     QVERIFY(QFile::exists(symlink));
1889     setTimeStamp(symlink, QDateTime::currentDateTime().addSecs(-20)); // differentiate link time and source file time
1890 
1891     const QUrl url(QUrl::fromLocalFile(symlink));
1892     KIO::StatJob *job = KIO::statDetails(url,
1893                                          KIO::StatJob::StatSide::SourceSide,
1894                                          KIO::StatBasic | KIO::StatResolveSymlink | KIO::StatUser | KIO::StatTime,
1895                                          KIO::HideProgressInfo);
1896     QVERIFY(job);
1897     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1898     // TODO set setSide, setDetails
1899     const KIO::UDSEntry &entry = job->statResult();
1900 
1901     // we only get filename, access, type, size, linkdest, uid, gid, btime, mtime, atime
1902     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1903     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1904     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1905     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1906     QVERIFY(entry.contains(KIO::UDSEntry::UDS_LINK_DEST));
1907     QVERIFY(entry.contains(KIO::UDSEntry::UDS_USER));
1908     QVERIFY(entry.contains(KIO::UDSEntry::UDS_GROUP));
1909     // QVERIFY(entry.contains(KIO::UDSEntry::UDS_CREATION_TIME)); // only true if st_birthtime or statx is used
1910     QVERIFY(entry.contains(KIO::UDSEntry::UDS_MODIFICATION_TIME));
1911     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS_TIME));
1912     QCOMPARE(entry.count(), 9 + (entry.contains(KIO::UDSEntry::UDS_CREATION_TIME) ? 1 : 0));
1913 
1914     QVERIFY(!entry.isDir());
1915     QVERIFY(entry.isLink());
1916     QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0);
1917     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("link"));
1918 
1919     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1920     const KFileItem kioItem(entry, url);
1921     const KFileItem fileItem(url);
1922     QCOMPARE(kioItem.name(), fileItem.name());
1923     QCOMPARE(kioItem.url(), fileItem.url());
1924     QVERIFY(kioItem.isLink());
1925     QVERIFY(fileItem.isLink());
1926     QCOMPARE(kioItem.linkDest(), fileItem.linkDest());
1927     QCOMPARE(kioItem.size(), fileItem.size());
1928     QCOMPARE(kioItem.user(), fileItem.user());
1929     QCOMPARE(kioItem.group(), fileItem.group());
1930     QCOMPARE(kioItem.mimetype(), fileItem.mimetype());
1931     QCOMPARE(kioItem.permissions(), fileItem.permissions());
1932     QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime));
1933     QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime));
1934 }
1935 
1936 /* Check that the underlying system, and Qt, support
1937  * millisecond timestamp resolution.
1938  */
1939 void JobTest::statTimeResolution()
1940 {
1941     const QString filePath = homeTmpDir() + "statFile";
1942     const QDateTime early70sDate = QDateTime::fromMSecsSinceEpoch(107780520123L);
1943     const time_t early70sTime = 107780520; // Seconds for January 6 1973, 12:02
1944 
1945     createTestFile(filePath);
1946 
1947     QFile dest_file(filePath);
1948     QVERIFY(dest_file.open(QIODevice::ReadOnly));
1949 #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
1950     // with nano secs precision
1951     struct timespec ut[2];
1952     ut[0].tv_sec = early70sTime;
1953     ut[0].tv_nsec = 123000000L; // 123 ms
1954     ut[1] = ut[0];
1955     // need to do this with the dest file still opened, or this fails
1956     QCOMPARE(::futimens(dest_file.handle(), ut), 0);
1957 #else
1958     struct timeval ut[2];
1959     ut[0].tv_sec = early70sTime;
1960     ut[0].tv_usec = 123000;
1961     ut[1] = ut[0];
1962     QCOMPARE(::futimes(dest_file.handle(), ut), 0);
1963 #endif
1964     dest_file.close();
1965 
1966     // Check that the modification time is set with millisecond precision
1967     dest_file.setFileName(filePath);
1968     QDateTime d = dest_file.fileTime(QFileDevice::FileModificationTime);
1969     QCOMPARE(d, early70sDate);
1970     QCOMPARE(d.time().msec(), 123);
1971 
1972 #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
1973     QT_STATBUF buff_dest;
1974     QCOMPARE(QT_STAT(filePath.toLocal8Bit().data(), &buff_dest), 0);
1975     QCOMPARE(buff_dest.st_mtim.tv_sec, early70sTime);
1976     QCOMPARE(buff_dest.st_mtim.tv_nsec, 123000000L);
1977 #endif
1978 
1979     QCOMPARE(QFileInfo(filePath).lastModified(), early70sDate);
1980 }
1981 #endif
1982 
1983 void JobTest::mostLocalUrl()
1984 {
1985     const QString filePath = homeTmpDir() + "fileFromHome";
1986     createTestFile(filePath);
1987     KIO::StatJob *job = KIO::mostLocalUrl(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
1988     QVERIFY(job);
1989     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1990     QCOMPARE(job->mostLocalUrl().toLocalFile(), filePath);
1991 }
1992 
1993 void JobTest::mostLocalUrlHttp()
1994 {
1995     // the url is returned as-is, as an http url can't have a mostLocalUrl
1996     const QUrl url("http://www.google.com");
1997     KIO::StatJob *httpStat = KIO::mostLocalUrl(url, KIO::HideProgressInfo);
1998     QVERIFY(httpStat);
1999     QVERIFY2(httpStat->exec(), qPrintable(httpStat->errorString()));
2000     QCOMPARE(httpStat->mostLocalUrl(), url);
2001 }
2002 
2003 void JobTest::chmodFile()
2004 {
2005     const QString filePath = homeTmpDir() + "fileForChmod";
2006     createTestFile(filePath);
2007     KFileItem item(QUrl::fromLocalFile(filePath));
2008     const mode_t origPerm = item.permissions();
2009     mode_t newPerm = origPerm ^ S_IWGRP;
2010     QVERIFY(newPerm != origPerm);
2011     KFileItemList items;
2012     items << item;
2013     KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo);
2014     job->setUiDelegate(nullptr);
2015     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2016 
2017     KFileItem newItem(QUrl::fromLocalFile(filePath));
2018     QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8));
2019     QFile::remove(filePath);
2020 }
2021 
2022 #ifdef Q_OS_UNIX
2023 void JobTest::chmodSticky()
2024 {
2025     const QString dirPath = homeTmpDir() + "dirForChmodSticky";
2026     QDir().mkpath(dirPath);
2027     KFileItem item(QUrl::fromLocalFile(dirPath));
2028     const mode_t origPerm = item.permissions();
2029     mode_t newPerm = origPerm ^ S_ISVTX;
2030     QVERIFY(newPerm != origPerm);
2031     KFileItemList items({item});
2032     KIO::Job *job = KIO::chmod(items, newPerm, S_ISVTX, QString(), QString(), false, KIO::HideProgressInfo);
2033     job->setUiDelegate(nullptr);
2034     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2035 
2036     KFileItem newItem(QUrl::fromLocalFile(dirPath));
2037     QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8));
2038     QVERIFY(QDir().rmdir(dirPath));
2039 }
2040 #endif
2041 
2042 void JobTest::chmodFileError()
2043 {
2044     // chown(root) should fail
2045     const QString filePath = homeTmpDir() + "fileForChmod";
2046     createTestFile(filePath);
2047     KFileItem item(QUrl::fromLocalFile(filePath));
2048     const mode_t origPerm = item.permissions();
2049     mode_t newPerm = origPerm ^ S_IWGRP;
2050     QVERIFY(newPerm != origPerm);
2051     KFileItemList items;
2052     items << item;
2053     KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QStringLiteral("root"), QString(), false, KIO::HideProgressInfo);
2054     // Simulate the user pressing "Skip" in the dialog.
2055     job->setUiDelegate(new KJobUiDelegate);
2056     auto *askUser = new MockAskUserInterface(job->uiDelegate());
2057 
2058     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2059 
2060     QCOMPARE(askUser->m_askUserSkipCalled, 1);
2061     KFileItem newItem(QUrl::fromLocalFile(filePath));
2062     // We skipped, so the chmod didn't happen.
2063     QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(origPerm, 8));
2064     QFile::remove(filePath);
2065 }
2066 
2067 void JobTest::mimeType()
2068 {
2069 #if 1
2070     const QString filePath = homeTmpDir() + "fileFromHome";
2071     createTestFile(filePath);
2072     KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
2073     QVERIFY(job);
2074 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2075     QSignalSpy spyMimeType(job, qOverload<KIO::Job *, const QString &>(&KIO::MimetypeJob::mimetype));
2076 #endif
2077     QSignalSpy spyMimeTypeFound(job, &KIO::TransferJob::mimeTypeFound);
2078     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2079 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2080     QCOMPARE(spyMimeType.count(), 1);
2081     QCOMPARE(spyMimeType[0][0], QVariant::fromValue(static_cast<KIO::Job *>(job)));
2082     QCOMPARE(spyMimeType[0][1].toString(), QStringLiteral("application/octet-stream"));
2083 #endif
2084     QCOMPARE(spyMimeTypeFound.count(), 1);
2085     QCOMPARE(spyMimeTypeFound[0][0], QVariant::fromValue(static_cast<KIO::Job *>(job)));
2086     QCOMPARE(spyMimeTypeFound[0][1].toString(), QStringLiteral("application/octet-stream"));
2087 #else
2088     // Testing mimetype over HTTP
2089     KIO::MimetypeJob *job = KIO::mimetype(QUrl("http://www.kde.org"), KIO::HideProgressInfo);
2090     QVERIFY(job);
2091 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2092     QSignalSpy spyMimeType(job, qOverload<KIO::Job *, const QString &>(&KIO::MimetypeJob::mimetype));
2093 #endif
2094     QSignalSpy spyMimeTypeFound(job, &KIO::TransferJob::mimeTypeFound);
2095     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2096 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2097     QCOMPARE(spyMimeType.count(), 1);
2098     QCOMPARE(spyMimeType[0][0], QVariant::fromValue(static_cast<KIO::Job *>(job)));
2099     QCOMPARE(spyMimeType[0][1].toString(), QString("text/html"));
2100 #endif
2101     QCOMPARE(spyMimeTypeFound.count(), 1);
2102     QCOMPARE(spyMimeTypeFound[0][0], QVariant::fromValue(static_cast<KIO::Job *>(job)));
2103     QCOMPARE(spyMimeTypeFound[0][1].toString(), QString("text/html"));
2104 #endif
2105 }
2106 
2107 void JobTest::mimeTypeError()
2108 {
2109     // KIO::mimetype() on a file that doesn't exist
2110     const QString filePath = homeTmpDir() + "doesNotExist";
2111     KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
2112     QVERIFY(job);
2113 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2114     QSignalSpy spyMimeType(job, qOverload<KIO::Job *, const QString &>(&KIO::MimetypeJob::mimetype));
2115 #endif
2116     QSignalSpy spyMimeTypeFound(job, &KIO::TransferJob::mimeTypeFound);
2117     QSignalSpy spyResult(job, &KJob::result);
2118     QVERIFY(!job->exec());
2119 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2120     QCOMPARE(spyMimeType.count(), 0);
2121 #endif
2122     QCOMPARE(spyMimeTypeFound.count(), 0);
2123     QCOMPARE(spyResult.count(), 1);
2124 }
2125 
2126 void JobTest::moveFileDestAlreadyExists_data()
2127 {
2128     QTest::addColumn<bool>("autoSkip");
2129 
2130     QTest::newRow("autoSkip") << true;
2131     QTest::newRow("manualSkip") << false;
2132 }
2133 
2134 void JobTest::moveFileDestAlreadyExists() // #157601
2135 {
2136     QFETCH(bool, autoSkip);
2137 
2138     const QString file1 = homeTmpDir() + "fileFromHome";
2139     createTestFile(file1);
2140     const QString file2 = homeTmpDir() + "fileFromHome2";
2141     createTestFile(file2);
2142     const QString file3 = homeTmpDir() + "anotherFile";
2143     createTestFile(file3);
2144     const QString existingDest = otherTmpDir() + "fileFromHome";
2145     createTestFile(existingDest);
2146     const QString existingDest2 = otherTmpDir() + "fileFromHome2";
2147     createTestFile(existingDest2);
2148 
2149     ScopedCleaner cleaner([] {
2150         QFile::remove(otherTmpDir() + "anotherFile");
2151     });
2152 
2153     const QList<QUrl> urls{QUrl::fromLocalFile(file1), QUrl::fromLocalFile(file2), QUrl::fromLocalFile(file3)};
2154     KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(otherTmpDir()), KIO::HideProgressInfo);
2155     MockAskUserInterface *askUserHandler = nullptr;
2156     if (autoSkip) {
2157         job->setUiDelegate(nullptr);
2158         job->setAutoSkip(true);
2159     } else {
2160         // Simulate the user pressing "Skip" in the dialog.
2161         job->setUiDelegate(new KJobUiDelegate);
2162         askUserHandler = new MockAskUserInterface(job->uiDelegate());
2163         askUserHandler->m_renameResult = KIO::Result_Skip;
2164     }
2165     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2166 
2167     if (askUserHandler) {
2168         QCOMPARE(askUserHandler->m_askUserRenameCalled, 2);
2169         QCOMPARE(askUserHandler->m_askUserSkipCalled, 0);
2170     }
2171     QVERIFY(QFile::exists(file1)); // it was skipped
2172     QVERIFY(QFile::exists(file2)); // it was skipped
2173     QVERIFY(!QFile::exists(file3)); // it was moved
2174 
2175     QCOMPARE(job->totalAmount(KJob::Files), 3);
2176     QCOMPARE(job->totalAmount(KJob::Directories), 0);
2177     QCOMPARE(job->processedAmount(KJob::Files), 1);
2178     QCOMPARE(job->processedAmount(KJob::Directories), 0);
2179     QCOMPARE(job->percent(), 100);
2180 }
2181 
2182 void JobTest::copyFileDestAlreadyExists_data()
2183 {
2184     QTest::addColumn<bool>("autoSkip");
2185 
2186     QTest::newRow("autoSkip") << true;
2187     QTest::newRow("manualSkip") << false;
2188 }
2189 
2190 static void simulatePressingSkip(KJob *job)
2191 {
2192     // Simulate the user pressing "Skip" in the dialog.
2193     job->setUiDelegate(new KJobUiDelegate);
2194     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
2195     askUserHandler->m_skipResult = KIO::Result_Skip;
2196 }
2197 
2198 void JobTest::copyFileDestAlreadyExists() // to test skipping when copying
2199 {
2200     QFETCH(bool, autoSkip);
2201     const QString file1 = homeTmpDir() + "fileFromHome";
2202     createTestFile(file1);
2203     const QString file2 = homeTmpDir() + "anotherFile";
2204     createTestFile(file2);
2205     const QString existingDest = otherTmpDir() + "fileFromHome";
2206     createTestFile(existingDest);
2207 
2208     ScopedCleaner cleaner([] {
2209         QFile::remove(otherTmpDir() + "anotherFile");
2210     });
2211 
2212     const QList<QUrl> urls{QUrl::fromLocalFile(file1), QUrl::fromLocalFile(file2)};
2213     KIO::CopyJob *job = KIO::copy(urls, QUrl::fromLocalFile(otherTmpDir()), KIO::HideProgressInfo);
2214     if (autoSkip) {
2215         job->setUiDelegate(nullptr);
2216         job->setAutoSkip(true);
2217     } else {
2218         simulatePressingSkip(job);
2219     }
2220     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2221     QVERIFY(QFile::exists(otherTmpDir() + "anotherFile"));
2222 
2223     QCOMPARE(job->totalAmount(KJob::Files), 2); // file1, file2
2224     QCOMPARE(job->totalAmount(KJob::Directories), 0);
2225     QCOMPARE(job->processedAmount(KJob::Files), 1);
2226     QCOMPARE(job->processedAmount(KJob::Directories), 0);
2227     QCOMPARE(job->percent(), 100);
2228 }
2229 
2230 void JobTest::moveDestAlreadyExistsAutoRename_data()
2231 {
2232     QTest::addColumn<bool>("samePartition");
2233     QTest::addColumn<bool>("moveDirs");
2234 
2235     QTest::newRow("files_same_partition") << true << false;
2236     QTest::newRow("files_other_partition") << false << false;
2237     QTest::newRow("dirs_same_partition") << true << true;
2238     QTest::newRow("dirs_other_partition") << false << true;
2239 }
2240 
2241 void JobTest::moveDestAlreadyExistsAutoRename()
2242 {
2243     QFETCH(bool, samePartition);
2244     QFETCH(bool, moveDirs);
2245 
2246     QString dir;
2247     if (samePartition) {
2248         dir = homeTmpDir() + "dir/";
2249         QVERIFY(QDir(dir).exists() || QDir().mkdir(dir));
2250     } else {
2251         dir = otherTmpDir();
2252     }
2253     moveDestAlreadyExistsAutoRename(dir, moveDirs);
2254 
2255     if (samePartition) {
2256         // cleanup
2257         KIO::Job *job = KIO::del(QUrl::fromLocalFile(dir), KIO::HideProgressInfo);
2258         QVERIFY2(job->exec(), qPrintable(job->errorString()));
2259         QVERIFY(!QFile::exists(dir));
2260     }
2261 }
2262 
2263 void JobTest::moveDestAlreadyExistsAutoRename(const QString &destDir, bool moveDirs) // #256650
2264 {
2265     const QString prefix = moveDirs ? QStringLiteral("dir ") : QStringLiteral("file ");
2266 
2267     const QString file1 = homeTmpDir() + prefix + "(1)";
2268     const QString file2 = homeTmpDir() + prefix + "(2)";
2269     const QString existingDest1 = destDir + prefix + "(1)";
2270     const QString existingDest2 = destDir + prefix + "(2)";
2271     const QStringList sources = QStringList{file1, file2, existingDest1, existingDest2};
2272     for (const QString &source : sources) {
2273         if (moveDirs) {
2274             QVERIFY(QDir().mkdir(source));
2275             createTestFile(source + "/innerfile");
2276             createTestFile(source + "/innerfile2");
2277         } else {
2278             createTestFile(source);
2279         }
2280     }
2281     const QString file3 = destDir + prefix + "(3)";
2282     const QString file4 = destDir + prefix + "(4)";
2283 
2284     ScopedCleaner cleaner([&]() {
2285         if (moveDirs) {
2286             QDir().rmdir(file1);
2287             QDir().rmdir(file2);
2288             QDir().rmdir(file3);
2289             QDir().rmdir(file4);
2290         } else {
2291             QFile::remove(file1);
2292             QFile::remove(file2);
2293             QFile::remove(file3);
2294             QFile::remove(file4);
2295         }
2296     });
2297 
2298     const QList<QUrl> urls = {QUrl::fromLocalFile(file1), QUrl::fromLocalFile(file2)};
2299     KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(destDir), KIO::HideProgressInfo);
2300     job->setUiDelegate(nullptr);
2301     job->setUiDelegateExtension(nullptr);
2302     job->setAutoRename(true);
2303 
2304     QSignalSpy spyRenamed(job, &KIO::CopyJob::renamed);
2305 
2306     // qDebug() << QDir(destDir).entryList();
2307 
2308     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2309 
2310     // qDebug() << QDir(destDir).entryList();
2311     QVERIFY(!QFile::exists(file1)); // it was moved
2312     QVERIFY(!QFile::exists(file2)); // it was moved
2313 
2314     QVERIFY(QFile::exists(existingDest1));
2315     QVERIFY(QFile::exists(existingDest2));
2316     QVERIFY(QFile::exists(file3));
2317     QVERIFY(QFile::exists(file4));
2318 
2319     QVERIFY(!spyRenamed.isEmpty());
2320 
2321     auto list = spyRenamed.takeFirst();
2322     QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(1)"));
2323     QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file3));
2324 
2325     bool samePartition = false;
2326     // Normally we'd see renamed(1, 3) and renamed(2, 4)
2327     // But across partitions, direct rename fails, and we end up with a task list of
2328     // 1->3, 2->3 since renaming 1 to 3 didn't happen yet.
2329     // so renamed(2, 3) is emitted, as if the user had chosen that.
2330     // And when that fails, we then get (3, 4)
2331     if (spyRenamed.count() == 1) {
2332         // It was indeed on the same partition
2333         samePartition = true;
2334         list = spyRenamed.takeFirst();
2335         QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(2)"));
2336         QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file4));
2337     } else {
2338         // Remove all renamed signals about innerfiles
2339         spyRenamed.erase(std::remove_if(spyRenamed.begin(),
2340                                         spyRenamed.end(),
2341                                         [](const QList<QVariant> &spy) {
2342                                             return spy.at(1).toUrl().path().contains("innerfile");
2343                                         }),
2344                          spyRenamed.end());
2345 
2346         list = spyRenamed.takeFirst();
2347         QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(2)"));
2348         QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file3));
2349 
2350         list = spyRenamed.takeFirst();
2351         QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(file3));
2352         QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file4));
2353     }
2354 
2355     if (samePartition) {
2356         QCOMPARE(job->totalAmount(KJob::Files), 2); // direct-renamed, so counted as files
2357         QCOMPARE(job->totalAmount(KJob::Directories), 0);
2358         QCOMPARE(job->processedAmount(KJob::Files), 2);
2359         QCOMPARE(job->processedAmount(KJob::Directories), 0);
2360     } else {
2361         if (moveDirs) {
2362             QCOMPARE(job->totalAmount(KJob::Directories), 2);
2363             QCOMPARE(job->totalAmount(KJob::Files), 4); // innerfiles
2364             QCOMPARE(job->processedAmount(KJob::Directories), 2);
2365             QCOMPARE(job->processedAmount(KJob::Files), 4);
2366         } else {
2367             QCOMPARE(job->totalAmount(KJob::Files), 2);
2368             QCOMPARE(job->totalAmount(KJob::Directories), 0);
2369             QCOMPARE(job->processedAmount(KJob::Files), 2);
2370             QCOMPARE(job->processedAmount(KJob::Directories), 0);
2371         }
2372     }
2373 
2374     QCOMPARE(job->percent(), 100);
2375 }
2376 
2377 void JobTest::copyDirectoryAlreadyExistsSkip()
2378 {
2379     // when copying a directory (which contains at least one file) to some location, and then
2380     // copying the same dir to the same location again, and clicking "Skip" there should be no
2381     // segmentation fault, bug 408350
2382 
2383     const QString src = homeTmpDir() + "a";
2384     createTestDirectory(src);
2385     const QString dest = homeTmpDir() + "dest";
2386     createTestDirectory(dest);
2387 
2388     QUrl u = QUrl::fromLocalFile(src);
2389     QUrl d = QUrl::fromLocalFile(dest);
2390 
2391     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
2392     job->setUiDelegate(nullptr);
2393     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2394     QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile")));
2395 
2396     job = KIO::copy(u, d, KIO::HideProgressInfo);
2397 
2398     simulatePressingSkip(job);
2399 
2400     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2401     QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile")));
2402 
2403     QDir(src).removeRecursively();
2404     QDir(dest).removeRecursively();
2405 
2406     QCOMPARE(job->totalAmount(KJob::Files), 2); // testfile, testlink
2407     QCOMPARE(job->totalAmount(KJob::Directories), 1);
2408     QCOMPARE(job->processedAmount(KJob::Files), 0);
2409     QCOMPARE(job->processedAmount(KJob::Directories), 1);
2410     QCOMPARE(job->percent(), 0);
2411 }
2412 
2413 void JobTest::copyFileAlreadyExistsRename()
2414 {
2415     const QString sourceFile = homeTmpDir() + "file";
2416     const QString dest = homeTmpDir() + "dest/";
2417     const QString alreadyExisting = dest + "file";
2418     const QString renamedFile = dest + "file-renamed";
2419 
2420     createTestFile(sourceFile);
2421     createTestFile(alreadyExisting);
2422     QVERIFY(QFile::exists(sourceFile));
2423     QVERIFY(QFile::exists(alreadyExisting));
2424 
2425     createTestDirectory(dest);
2426 
2427     ScopedCleaner cleaner([&] {
2428         QVERIFY(QFile(sourceFile).remove());
2429         QVERIFY(QDir(dest).removeRecursively());
2430     });
2431 
2432     QUrl s = QUrl::fromLocalFile(sourceFile);
2433     QUrl d = QUrl::fromLocalFile(dest);
2434 
2435     KIO::CopyJob *job = KIO::copy(s, d, KIO::HideProgressInfo);
2436     // Simulate the user pressing "Rename" in the dialog and choosing another destination.
2437     job->setUiDelegate(new KJobUiDelegate);
2438     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
2439     askUserHandler->m_renameResult = KIO::Result_Rename;
2440     askUserHandler->m_newDestUrl = QUrl::fromLocalFile(renamedFile);
2441 
2442     QSignalSpy spyRenamed(job, &KIO::CopyJob::renamed);
2443 
2444     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2445     QVERIFY(QFile::exists(renamedFile));
2446 
2447     QCOMPARE(spyRenamed.count(), 1);
2448     auto list = spyRenamed.takeFirst();
2449     QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(alreadyExisting));
2450     QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(renamedFile));
2451 }
2452 
2453 void JobTest::safeOverwrite_data()
2454 {
2455     QTest::addColumn<bool>("destFileExists");
2456 
2457     QTest::newRow("dest_file_exists") << true;
2458     QTest::newRow("dest_file_does_not_exist") << false;
2459 }
2460 
2461 void JobTest::safeOverwrite()
2462 {
2463 #ifdef Q_OS_WIN
2464     QSKIP("Test skipped on Windows");
2465 #endif
2466 
2467     QFETCH(bool, destFileExists);
2468     const QString srcDir = homeTmpDir() + "overwrite";
2469     const QString srcFile = srcDir + "/testfile";
2470     const QString destDir = otherTmpDir() + "overwrite_other";
2471     const QString destFile = destDir + "/testfile";
2472     const QString destPartFile = destFile + ".part";
2473 
2474     createTestDirectory(srcDir);
2475     createTestDirectory(destDir);
2476 
2477     ScopedCleaner cleaner([&] {
2478         QDir(srcDir).removeRecursively();
2479         QDir(destDir).removeRecursively();
2480     });
2481 
2482     const int srcSize = 1000000; // ~1MB
2483     QVERIFY(QFile::resize(srcFile, srcSize));
2484     if (!destFileExists) {
2485         QVERIFY(QFile::remove(destFile));
2486     } else {
2487         QVERIFY(QFile::exists(destFile));
2488     }
2489     QVERIFY(!QFile::exists(destPartFile));
2490 
2491     if (otherTmpDirIsOnSamePartition()) {
2492         QSKIP(qPrintable(QStringLiteral("This test requires %1 and %2 to be on different partitions").arg(srcDir, destDir)));
2493     }
2494 
2495     KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | KIO::Overwrite);
2496     job->setUiDelegate(nullptr);
2497     QSignalSpy spyTotalSize(job, &KIO::FileCopyJob::totalSize);
2498     connect(job, &KIO::FileCopyJob::processedSize, this, [&](KJob *job, qulonglong size) {
2499         Q_UNUSED(job);
2500         if (size > 0 && size < srcSize) {
2501             // To avoid overwriting dest, we want the KIO worker to use dest.part
2502             QCOMPARE(QFileInfo::exists(destPartFile), destFileExists);
2503         }
2504     });
2505     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2506     QVERIFY(QFile::exists(destFile));
2507     QVERIFY(!QFile::exists(srcFile));
2508     QVERIFY(!QFile::exists(destPartFile));
2509     QCOMPARE(spyTotalSize.count(), 1);
2510 }
2511 
2512 void JobTest::overwriteOlderFiles_data()
2513 {
2514     QTest::addColumn<bool>("destFileOlder");
2515     QTest::addColumn<bool>("moving");
2516 
2517     QTest::newRow("dest_file_older_copying") << true << false;
2518     QTest::newRow("dest_file_older_moving") << true << true;
2519     QTest::newRow("dest_file_younger_copying") << false << false;
2520     QTest::newRow("dest_file_younger_moving") << false << true;
2521 }
2522 
2523 void JobTest::overwriteOlderFiles()
2524 {
2525     QFETCH(bool, destFileOlder);
2526     QFETCH(bool, moving);
2527     const QString srcDir = homeTmpDir() + "overwrite";
2528     const QString srcFile = srcDir + "/testfile";
2529     const QString srcFile2 = srcDir + "/testfile2";
2530     const QString srcFile3 = srcDir + "/testfile3";
2531     const QString destDir = otherTmpDir() + "overwrite_other";
2532     const QString destFile = destDir + "/testfile";
2533     const QString destFile2 = destDir + "/testfile2";
2534     const QString destFile3 = destDir + "/testfile3";
2535     const QString destPartFile = destFile + ".part";
2536 
2537     createTestDirectory(srcDir);
2538     createTestDirectory(destDir);
2539     createTestFile(srcFile2);
2540     createTestFile(srcFile3);
2541     createTestFile(destFile2);
2542     createTestFile(destFile3);
2543     QVERIFY(!QFile::exists(destPartFile));
2544 
2545     const int srcSize = 1000; // ~1KB
2546     QVERIFY(QFile::resize(srcFile, srcSize));
2547     QVERIFY(QFile::resize(srcFile2, srcSize));
2548     QVERIFY(QFile::resize(srcFile3, srcSize));
2549     if (destFileOlder) {
2550         setTimeStamp(destFile, QFile(srcFile).fileTime(QFileDevice::FileModificationTime).addSecs(-2));
2551         setTimeStamp(destFile2, QFile(srcFile2).fileTime(QFileDevice::FileModificationTime).addSecs(-2));
2552 
2553         QVERIFY(QFile(destFile).fileTime(QFileDevice::FileModificationTime) <= QFile(srcFile).fileTime(QFileDevice::FileModificationTime));
2554         QVERIFY(QFile(destFile2).fileTime(QFileDevice::FileModificationTime) <= QFile(srcFile2).fileTime(QFileDevice::FileModificationTime));
2555     } else {
2556         setTimeStamp(destFile, QFile(srcFile).fileTime(QFileDevice::FileModificationTime).addSecs(2));
2557         setTimeStamp(destFile2, QFile(srcFile2).fileTime(QFileDevice::FileModificationTime).addSecs(2));
2558 
2559         QVERIFY(QFile(destFile).fileTime(QFileDevice::FileModificationTime) >= QFile(srcFile).fileTime(QFileDevice::FileModificationTime));
2560         QVERIFY(QFile(destFile2).fileTime(QFileDevice::FileModificationTime) >= QFile(srcFile2).fileTime(QFileDevice::FileModificationTime));
2561     }
2562     // to have an always skipped file
2563     setTimeStamp(destFile3, QFile(srcFile3).fileTime(QFileDevice::FileModificationTime).addSecs(2));
2564 
2565     KIO::CopyJob *job;
2566     if (moving) {
2567         job = KIO::move({QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(srcFile2), QUrl::fromLocalFile(srcFile3)},
2568                         QUrl::fromLocalFile(destDir),
2569                         KIO::HideProgressInfo);
2570     } else {
2571         job = KIO::copy({QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(srcFile2), QUrl::fromLocalFile(srcFile3)},
2572                         QUrl::fromLocalFile(destDir),
2573                         KIO::HideProgressInfo);
2574     }
2575 
2576     job->setUiDelegate(new KJobUiDelegate);
2577     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
2578     askUserHandler->m_renameResult = KIO::Result_OverwriteWhenOlder;
2579 
2580     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2581     QCOMPARE(askUserHandler->m_askUserRenameCalled, 1);
2582     QVERIFY(!QFile::exists(destPartFile));
2583     // QCOMPARE(spyTotalSize.count(), 1);
2584 
2585     // skipped file whose dest is always newer
2586     QVERIFY(QFile::exists(srcFile3)); // it was skipped
2587     QCOMPARE(QFile(destFile3).size(), 11);
2588 
2589     if (destFileOlder) {
2590         // files were overwritten
2591         QCOMPARE(QFile(destFile).size(), 1000);
2592         QCOMPARE(QFile(destFile2).size(), 1000);
2593 
2594         // files were overwritten
2595         QCOMPARE(job->processedAmount(KJob::Files), 2);
2596         QCOMPARE(job->processedAmount(KJob::Directories), 0);
2597 
2598         if (moving) {
2599             QVERIFY(!QFile::exists(srcFile)); // it was moved
2600             QVERIFY(!QFile::exists(srcFile2)); // it was moved
2601         } else {
2602             QVERIFY(QFile::exists(srcFile)); // it was copied
2603             QVERIFY(QFile::exists(srcFile2)); // it was copied
2604 
2605             QCOMPARE(QFile(destFile).fileTime(QFileDevice::FileModificationTime), QFile(srcFile).fileTime(QFileDevice::FileModificationTime));
2606             QCOMPARE(QFile(destFile2).fileTime(QFileDevice::FileModificationTime), QFile(srcFile2).fileTime(QFileDevice::FileModificationTime));
2607         }
2608     } else {
2609         // files were skipped
2610         QCOMPARE(job->processedAmount(KJob::Files), 0);
2611         QCOMPARE(job->processedAmount(KJob::Directories), 0);
2612 
2613         QCOMPARE(QFile(destFile).size(), 11);
2614         QCOMPARE(QFile(destFile2).size(), 11);
2615 
2616         QVERIFY(QFile::exists(srcFile));
2617         QVERIFY(QFile::exists(srcFile2));
2618     }
2619 
2620     QDir(srcDir).removeRecursively();
2621     QDir(destDir).removeRecursively();
2622 }
2623 
2624 void JobTest::moveAndOverwrite()
2625 {
2626     const QString sourceFile = homeTmpDir() + "fileFromHome";
2627     createTestFile(sourceFile);
2628     QString existingDest = otherTmpDir() + "fileFromHome";
2629     createTestFile(existingDest);
2630 
2631     KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2632     job->setUiDelegate(nullptr);
2633     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2634     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2635 
2636 #ifndef Q_OS_WIN
2637     // Now same thing when the target is a symlink to the source
2638     createTestFile(sourceFile);
2639     createTestSymlink(existingDest, QFile::encodeName(sourceFile));
2640     QVERIFY(QFile::exists(existingDest));
2641     job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2642     job->setUiDelegate(nullptr);
2643     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2644     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2645 
2646     // Now same thing when the target is a symlink to another file
2647     createTestFile(sourceFile);
2648     createTestFile(sourceFile + QLatin1Char('2'));
2649     createTestSymlink(existingDest, QFile::encodeName(sourceFile + QLatin1Char('2')));
2650     QVERIFY(QFile::exists(existingDest));
2651     job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2652     job->setUiDelegate(nullptr);
2653     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2654     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2655 
2656     // Now same thing when the target is a _broken_ symlink
2657     createTestFile(sourceFile);
2658     createTestSymlink(existingDest);
2659     QVERIFY(!QFile::exists(existingDest)); // it exists, but it's broken...
2660     job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2661     job->setUiDelegate(nullptr);
2662     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2663     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2664 #endif
2665 }
2666 
2667 void JobTest::moveOverSymlinkToSelf() // #169547
2668 {
2669 #ifndef Q_OS_WIN
2670     const QString sourceFile = homeTmpDir() + "fileFromHome";
2671     createTestFile(sourceFile);
2672     const QString existingDest = homeTmpDir() + "testlink";
2673     createTestSymlink(existingDest, QFile::encodeName(sourceFile));
2674     QVERIFY(QFile::exists(existingDest));
2675 
2676     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), KIO::HideProgressInfo);
2677     job->setUiDelegate(nullptr);
2678     QVERIFY(!job->exec());
2679     QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // and not ERR_IDENTICAL_FILES!
2680     QVERIFY(QFile::exists(sourceFile)); // it not moved
2681 #endif
2682 }
2683 
2684 void JobTest::createSymlink()
2685 {
2686 #ifdef Q_OS_WIN
2687     QSKIP("Test skipped on Windows");
2688 #endif
2689     const QString sourceFile = homeTmpDir() + "fileFromHome";
2690     createTestFile(sourceFile);
2691     const QString destDir = homeTmpDir() + "dest";
2692     QVERIFY(QDir().mkpath(destDir));
2693 
2694     ScopedCleaner cleaner([&] {
2695         QDir(destDir).removeRecursively();
2696     });
2697 
2698     // With KIO::link (high-level)
2699     KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo);
2700     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2701     QVERIFY(QFileInfo::exists(sourceFile));
2702     const QString dest = destDir + "/fileFromHome";
2703     QVERIFY(QFileInfo(dest).isSymLink());
2704     QCOMPARE(QFileInfo(dest).symLinkTarget(), sourceFile);
2705     QFile::remove(dest);
2706 
2707     // With KIO::symlink (low-level)
2708     const QString linkPath = destDir + "/link";
2709     KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(linkPath), KIO::HideProgressInfo);
2710     QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString()));
2711     QVERIFY(QFileInfo::exists(sourceFile));
2712     QVERIFY(QFileInfo(linkPath).isSymLink());
2713     QCOMPARE(QFileInfo(linkPath).symLinkTarget(), sourceFile);
2714 }
2715 
2716 void JobTest::createSymlinkTargetDirDoesntExist()
2717 {
2718 #ifdef Q_OS_WIN
2719     QSKIP("Test skipped on Windows");
2720 #endif
2721     const QString sourceFile = homeTmpDir() + "fileFromHome";
2722     createTestFile(sourceFile);
2723     const QString destDir = homeTmpDir() + "dest/does/not/exist";
2724 
2725     KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo);
2726     QVERIFY(!job->exec());
2727     QCOMPARE(job->error(), static_cast<int>(KIO::ERR_CANNOT_SYMLINK));
2728 }
2729 
2730 void JobTest::createSymlinkAsShouldSucceed()
2731 {
2732 #ifdef Q_OS_WIN
2733     QSKIP("Test skipped on Windows");
2734 #endif
2735     const QString sourceFile = homeTmpDir() + "fileFromHome";
2736     createTestFile(sourceFile);
2737     const QString dest = homeTmpDir() + "testlink";
2738     QFile::remove(dest); // just in case
2739 
2740     ScopedCleaner cleaner([&] {
2741         QVERIFY(QFile::remove(dest));
2742     });
2743 
2744     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2745     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2746     QVERIFY(QFileInfo::exists(sourceFile));
2747     QVERIFY(QFileInfo(dest).isSymLink());
2748 }
2749 
2750 void JobTest::createSymlinkAsShouldFailDirectoryExists()
2751 {
2752 #ifdef Q_OS_WIN
2753     QSKIP("Test skipped on Windows");
2754 #endif
2755     const QString sourceFile = homeTmpDir() + "fileFromHome";
2756     createTestFile(sourceFile);
2757     const QString dest = homeTmpDir() + "dest";
2758     QVERIFY(QDir().mkpath(dest)); // dest exists as a directory
2759 
2760     ScopedCleaner cleaner([&] {
2761         QVERIFY(QDir().rmdir(dest));
2762     });
2763 
2764     // With KIO::link (high-level)
2765     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2766     QVERIFY(!job->exec());
2767     QCOMPARE(job->error(), (int)KIO::ERR_DIR_ALREADY_EXIST);
2768     QVERIFY(QFileInfo::exists(sourceFile));
2769     QVERIFY(!QFileInfo::exists(dest + "/fileFromHome"));
2770 
2771     // With KIO::symlink (low-level)
2772     KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2773     QVERIFY(!symlinkJob->exec());
2774     QCOMPARE(symlinkJob->error(), (int)KIO::ERR_DIR_ALREADY_EXIST);
2775     QVERIFY(QFileInfo::exists(sourceFile));
2776 }
2777 
2778 void JobTest::createSymlinkAsShouldFailFileExists()
2779 {
2780 #ifdef Q_OS_WIN
2781     QSKIP("Test skipped on Windows");
2782 #endif
2783     const QString sourceFile = homeTmpDir() + "fileFromHome";
2784     createTestFile(sourceFile);
2785     const QString dest = homeTmpDir() + "testlink";
2786     QFile::remove(dest); // just in case
2787 
2788     ScopedCleaner cleaner([&] {
2789         QVERIFY(QFile::remove(sourceFile));
2790         QVERIFY(QFile::remove(dest));
2791     });
2792 
2793     // First time works
2794     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2795     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2796     QVERIFY(QFileInfo(dest).isSymLink());
2797 
2798     // Second time fails (already exists)
2799     job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2800     QVERIFY(!job->exec());
2801     QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST);
2802 
2803     // KIO::symlink fails too
2804     KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2805     QVERIFY(!symlinkJob->exec());
2806     QCOMPARE(symlinkJob->error(), (int)KIO::ERR_FILE_ALREADY_EXIST);
2807 }
2808 
2809 void JobTest::createSymlinkWithOverwriteShouldWork()
2810 {
2811 #ifdef Q_OS_WIN
2812     QSKIP("Test skipped on Windows");
2813 #endif
2814     const QString sourceFile = homeTmpDir() + "fileFromHome";
2815     createTestFile(sourceFile);
2816     const QString dest = homeTmpDir() + "testlink";
2817     QFile::remove(dest); // just in case
2818 
2819     ScopedCleaner cleaner([&] {
2820         QVERIFY(QFile::remove(sourceFile));
2821         QVERIFY(QFile::remove(dest));
2822     });
2823 
2824     // First time works
2825     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2826     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2827     QVERIFY(QFileInfo(dest).isSymLink());
2828 
2829     // Changing the link target, with overwrite, works
2830     job = KIO::linkAs(QUrl::fromLocalFile(sourceFile + QLatin1Char('2')), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo);
2831     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2832     QVERIFY(QFileInfo(dest).isSymLink());
2833     QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('2')));
2834 
2835     // Changing the link target using KIO::symlink, with overwrite, works
2836     KIO::Job *symlinkJob = KIO::symlink(sourceFile + QLatin1Char('3'), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo);
2837     QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString()));
2838     QVERIFY(QFileInfo(dest).isSymLink());
2839     QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('3')));
2840 }
2841 
2842 void JobTest::createBrokenSymlink()
2843 {
2844 #ifdef Q_OS_WIN
2845     QSKIP("Test skipped on Windows");
2846 #endif
2847     const QString sourceFile = "/does/not/exist";
2848     const QString dest = homeTmpDir() + "testlink";
2849     QFile::remove(dest); // just in case
2850     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2851     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2852     QVERIFY(QFileInfo(dest).isSymLink());
2853 
2854     // Second time fails (already exists)
2855     job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2856     QVERIFY(!job->exec());
2857     QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST);
2858     QVERIFY(QFile::remove(dest));
2859 }
2860 
2861 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 84)
2862 void JobTest::multiGet()
2863 {
2864     const int numFiles = 10;
2865     const QString baseDir = homeTmpDir();
2866     const QList<QUrl> urls = createManyFiles(baseDir, numFiles);
2867     QCOMPARE(urls.count(), numFiles);
2868 
2869     // qDebug() << file;
2870     KIO::MultiGetJob *job = KIO::multi_get(0, urls.at(0), KIO::MetaData()); // TODO: missing KIO::HideProgressInfo
2871 
2872 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 79)
2873     QSignalSpy spyData(job, &KIO::MultiGetJob::data);
2874 #else
2875     QSignalSpy spyData(job, &KIO::MultiGetJob::dataReceived);
2876 #endif
2877 
2878 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2879     QSignalSpy spyMimeType(job, &KIO::MultiGetJob::mimetype);
2880 #endif
2881     QSignalSpy spyMimeTypeFound(job, &KIO::MultiGetJob::mimeTypeFound);
2882 
2883 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 79)
2884     QSignalSpy spyResultId(job, qOverload<long>(&KIO::MultiGetJob::result));
2885 #else
2886     QSignalSpy spyResultId(job, &KIO::MultiGetJob::fileTransferred);
2887 #endif
2888 
2889     QSignalSpy spyResult(job, &KJob::result);
2890     job->setUiDelegate(nullptr);
2891 
2892     for (int i = 1; i < numFiles; ++i) {
2893         const QUrl url = urls.at(i);
2894         job->get(i, url, KIO::MetaData());
2895     }
2896     // connect(job, &KIO::MultiGetJob::result, [=] (long id) { qDebug() << "ID I got" << id;});
2897     // connect(job, &KJob::result, [this](KJob* ) {qDebug() << "END";});
2898 
2899     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2900 
2901     QCOMPARE(spyResult.count(), 1);
2902     QCOMPARE(spyResultId.count(), numFiles);
2903 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2904     QCOMPARE(spyMimeType.count(), numFiles);
2905 #endif
2906     QCOMPARE(spyMimeTypeFound.count(), numFiles);
2907     QCOMPARE(spyData.count(), numFiles * 2);
2908     for (int i = 0; i < numFiles; ++i) {
2909         QCOMPARE(spyResultId.at(i).at(0).toInt(), i);
2910 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
2911         QCOMPARE(spyMimeType.at(i).at(0).toInt(), i);
2912         QCOMPARE(spyMimeType.at(i).at(1).toString(), QStringLiteral("text/plain"));
2913 #endif
2914         QCOMPARE(spyMimeTypeFound.at(i).at(0).toInt(), i);
2915         QCOMPARE(spyMimeTypeFound.at(i).at(1).toString(), QStringLiteral("text/plain"));
2916         QCOMPARE(spyData.at(i * 2).at(0).toInt(), i);
2917         QCOMPARE(QString(spyData.at(i * 2).at(1).toByteArray()), QStringLiteral("Hello"));
2918         QCOMPARE(spyData.at(i * 2 + 1).at(0).toInt(), i);
2919         QCOMPARE(QString(spyData.at(i * 2 + 1).at(1).toByteArray()), QLatin1String(""));
2920     }
2921 }
2922 #endif // Deprecated since 5.84
2923 
2924 void JobTest::cancelCopyAndCleanDest_data()
2925 {
2926     QTest::addColumn<bool>("suspend");
2927     QTest::addColumn<bool>("overwrite");
2928 
2929     QTest::newRow("suspend_no_overwrite") << true << false;
2930     QTest::newRow("no_suspend_no_overwrite") << false << false;
2931 
2932 #ifndef Q_OS_WIN
2933     QTest::newRow("suspend_with_overwrite") << true << true;
2934     QTest::newRow("no_suspend_with_overwrite") << false << true;
2935 #endif
2936 }
2937 
2938 void JobTest::cancelCopyAndCleanDest()
2939 {
2940     QFETCH(bool, suspend);
2941     QFETCH(bool, overwrite);
2942 
2943     const QString baseDir = homeTmpDir();
2944     const QString srcTemplate = baseDir + QStringLiteral("testfile_XXXXXX");
2945     const QString destFile = baseDir + QStringLiteral("testfile_copy_slow_") + QString::fromLatin1(QTest::currentDataTag());
2946 
2947     QTemporaryFile f(srcTemplate);
2948     if (!f.open()) {
2949         qFatal("Couldn't open %s", qPrintable(f.fileName()));
2950     }
2951 
2952     const int sz = 4000000; //~4MB
2953     f.seek(sz - 1);
2954     f.write("0");
2955     f.close();
2956     QCOMPARE(f.size(), sz);
2957 
2958     if (overwrite) {
2959         createTestFile(destFile);
2960     }
2961     const QString destToCheck = (overwrite) ? destFile + QStringLiteral(".part") : destFile;
2962 
2963     KIO::JobFlag m_overwriteFlag = overwrite ? KIO::Overwrite : KIO::DefaultFlags;
2964     KIO::FileCopyJob *copyJob = KIO::file_copy(QUrl::fromLocalFile(f.fileName()), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | m_overwriteFlag);
2965     copyJob->setUiDelegate(nullptr);
2966     QSignalSpy spyProcessedSize(copyJob, &KIO::Job::processedSize);
2967     QSignalSpy spyFinished(copyJob, &KIO::Job::finished);
2968     connect(copyJob, &KIO::Job::processedSize, this, [destFile, suspend, destToCheck](KJob *job, qulonglong processedSize) {
2969         if (processedSize > 0) {
2970             QVERIFY2(QFile::exists(destToCheck), qPrintable(destToCheck));
2971             qDebug() << "processedSize=" << processedSize << "file size" << QFileInfo(destToCheck).size();
2972             if (suspend) {
2973                 job->suspend();
2974             }
2975             QVERIFY(job->kill());
2976         }
2977     });
2978 
2979     QVERIFY(!copyJob->exec());
2980     QCOMPARE(spyProcessedSize.count(), 1);
2981     QCOMPARE(spyFinished.count(), 1);
2982     QCOMPARE(copyJob->error(), KIO::ERR_USER_CANCELED);
2983 
2984     // the destination file actual deletion happens after finished() is emitted
2985     // we need to give some time to the KIO worker to finish the file cleaning
2986     QTRY_VERIFY2(!QFile::exists(destToCheck), qPrintable(destToCheck));
2987 }
2988 
2989 #include "moc_jobtest.cpp"