File indexing completed on 2024-04-21 03:53:39

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2006 Kevin Ottens <ervin@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "kjobtest.h"
0009 
0010 #include <QList>
0011 #include <QMetaEnum>
0012 #include <QSignalSpy>
0013 #include <QTest>
0014 #include <QTimer>
0015 
0016 #include <string>
0017 
0018 QTEST_MAIN(KJobTest)
0019 
0020 KJobTest::KJobTest()
0021     : loop(this)
0022 {
0023 }
0024 
0025 void KJobTest::testEmitResult_data()
0026 {
0027     QTest::addColumn<int>("errorCode");
0028     QTest::addColumn<QString>("errorText");
0029 
0030     QTest::newRow("no error") << int(KJob::NoError) << QString();
0031     QTest::newRow("error no text") << 2 << QString();
0032     QTest::newRow("error with text") << 6 << "oops! an error? naaah, really?";
0033 }
0034 
0035 void KJobTest::testEmitResult()
0036 {
0037     TestJob *job = new TestJob;
0038 
0039     connect(job, &KJob::result, this, [this](KJob *job) {
0040         slotResult(job);
0041         loop.quit();
0042     });
0043 
0044     QFETCH(int, errorCode);
0045     QFETCH(QString, errorText);
0046 
0047     job->setError(errorCode);
0048     job->setErrorText(errorText);
0049 
0050     QSignalSpy destroyed_spy(job, &QObject::destroyed);
0051     job->start();
0052     QVERIFY(!job->isFinished());
0053     loop.exec();
0054     QVERIFY(job->isFinished());
0055 
0056     QCOMPARE(m_lastError, errorCode);
0057     QCOMPARE(m_lastErrorText, errorText);
0058 
0059     // Verify that the job is not deleted immediately...
0060     QCOMPARE(destroyed_spy.size(), 0);
0061     QTimer::singleShot(0, &loop, &QEventLoop::quit);
0062     // ... but when we enter the event loop again.
0063     loop.exec();
0064     QCOMPARE(destroyed_spy.size(), 1);
0065 }
0066 
0067 void KJobTest::testProgressTracking()
0068 {
0069     TestJob *testJob = new TestJob;
0070     KJob *job = testJob;
0071 
0072     qRegisterMetaType<KJob *>("KJob*");
0073     qRegisterMetaType<qulonglong>("qulonglong");
0074 
0075     QSignalSpy processedChanged_spy(job, &KJob::processedAmountChanged);
0076     QSignalSpy totalChanged_spy(job, &KJob::totalAmountChanged);
0077     QSignalSpy percentChanged_spy(job, &KJob::percentChanged);
0078 
0079     /* Process a first item. Corresponding signal should be emitted.
0080      * Total size didn't change.
0081      * Since the total size is unknown, no percent signal is emitted.
0082      */
0083     testJob->setProcessedSize(1);
0084 
0085     QCOMPARE(processedChanged_spy.size(), 1);
0086     QCOMPARE(processedChanged_spy.at(0).at(0).value<KJob *>(), static_cast<KJob *>(job));
0087     QCOMPARE(processedChanged_spy.at(0).at(2).value<qulonglong>(), qulonglong(1));
0088     QCOMPARE(totalChanged_spy.size(), 0);
0089     QCOMPARE(percentChanged_spy.size(), 0);
0090 
0091     /* Now, we know the total size. It's signaled.
0092      * The new percentage is signaled too.
0093      */
0094     testJob->setTotalSize(10);
0095 
0096     QCOMPARE(processedChanged_spy.size(), 1);
0097     QCOMPARE(totalChanged_spy.size(), 1);
0098     QCOMPARE(totalChanged_spy.at(0).at(0).value<KJob *>(), job);
0099     QCOMPARE(totalChanged_spy.at(0).at(2).value<qulonglong>(), qulonglong(10));
0100     QCOMPARE(percentChanged_spy.size(), 1);
0101     QCOMPARE(percentChanged_spy.at(0).at(0).value<KJob *>(), job);
0102     QCOMPARE(percentChanged_spy.at(0).at(1).value<unsigned long>(), static_cast<unsigned long>(10));
0103 
0104     /* We announce a new percentage by hand.
0105      * Total, and processed didn't change, so no signal is emitted for them.
0106      */
0107     testJob->setPercent(15);
0108 
0109     QCOMPARE(processedChanged_spy.size(), 1);
0110     QCOMPARE(totalChanged_spy.size(), 1);
0111     QCOMPARE(percentChanged_spy.size(), 2);
0112     QCOMPARE(percentChanged_spy.at(1).at(0).value<KJob *>(), job);
0113     QCOMPARE(percentChanged_spy.at(1).at(1).value<unsigned long>(), static_cast<unsigned long>(15));
0114 
0115     /* We make some progress.
0116      * Processed size and percent are signaled.
0117      */
0118     testJob->setProcessedSize(3);
0119 
0120     QCOMPARE(processedChanged_spy.size(), 2);
0121     QCOMPARE(processedChanged_spy.at(1).at(0).value<KJob *>(), job);
0122     QCOMPARE(processedChanged_spy.at(1).at(2).value<qulonglong>(), qulonglong(3));
0123     QCOMPARE(totalChanged_spy.size(), 1);
0124     QCOMPARE(percentChanged_spy.size(), 3);
0125     QCOMPARE(percentChanged_spy.at(2).at(0).value<KJob *>(), job);
0126     QCOMPARE(percentChanged_spy.at(2).at(1).value<unsigned long>(), static_cast<unsigned long>(30));
0127 
0128     /* We set a new total size, but equals to the previous one.
0129      * No signal is emitted.
0130      */
0131     testJob->setTotalSize(10);
0132     QCOMPARE(processedChanged_spy.size(), 2);
0133     QCOMPARE(totalChanged_spy.size(), 1);
0134     QCOMPARE(percentChanged_spy.size(), 3);
0135 
0136     /* We 'lost' the previous work done.
0137      * Signals both percentage and new processed size.
0138      */
0139     testJob->setProcessedSize(0);
0140 
0141     QCOMPARE(processedChanged_spy.size(), 3);
0142     QCOMPARE(processedChanged_spy.at(2).at(0).value<KJob *>(), job);
0143     QCOMPARE(processedChanged_spy.at(2).at(2).value<qulonglong>(), qulonglong(0));
0144     QCOMPARE(totalChanged_spy.size(), 1);
0145     QCOMPARE(percentChanged_spy.size(), 4);
0146     QCOMPARE(percentChanged_spy.at(3).at(0).value<KJob *>(), job);
0147     QCOMPARE(percentChanged_spy.at(3).at(1).value<unsigned long>(), static_cast<unsigned long>(0));
0148 
0149     /* We process more than the total size!?
0150      * Signals both percentage and new processed size.
0151      * Percentage is 150%
0152      *
0153      * Might sounds weird, but verify that this case is handled gracefully.
0154      */
0155     testJob->setProcessedSize(15);
0156 
0157     QCOMPARE(processedChanged_spy.size(), 4);
0158     QCOMPARE(processedChanged_spy.at(3).at(0).value<KJob *>(), job);
0159     QCOMPARE(processedChanged_spy.at(3).at(2).value<qulonglong>(), qulonglong(15));
0160     QCOMPARE(totalChanged_spy.size(), 1);
0161     QCOMPARE(percentChanged_spy.size(), 5);
0162     QCOMPARE(percentChanged_spy.at(4).at(0).value<KJob *>(), job);
0163     QCOMPARE(percentChanged_spy.at(4).at(1).value<unsigned long>(), static_cast<unsigned long>(150));
0164 
0165     processedChanged_spy.clear();
0166     totalChanged_spy.clear();
0167     percentChanged_spy.clear();
0168 
0169     /**
0170      * Try again with Files as the progress unit
0171      */
0172     testJob->setProgressUnit(KJob::Files);
0173     testJob->setProcessedSize(16);
0174     QCOMPARE(percentChanged_spy.size(), 0);
0175 
0176     testJob->setTotalFiles(5);
0177     QCOMPARE(percentChanged_spy.size(), 1);
0178     QCOMPARE(percentChanged_spy.at(0).at(1).value<unsigned long>(), static_cast<unsigned long>(0));
0179 
0180     testJob->setProcessedFiles(2);
0181     QCOMPARE(percentChanged_spy.size(), 2);
0182     QCOMPARE(percentChanged_spy.at(1).at(1).value<unsigned long>(), static_cast<unsigned long>(40));
0183 
0184     delete job;
0185 }
0186 
0187 void KJobTest::testExec_data()
0188 {
0189     QTest::addColumn<int>("errorCode");
0190     QTest::addColumn<QString>("errorText");
0191 
0192     QTest::newRow("no error") << int(KJob::NoError) << QString();
0193     QTest::newRow("error no text") << 2 << QString();
0194     QTest::newRow("error with text") << 6 << "oops! an error? naaah, really?";
0195 }
0196 
0197 void KJobTest::testExec()
0198 {
0199     TestJob *job = new TestJob;
0200 
0201     QFETCH(int, errorCode);
0202     QFETCH(QString, errorText);
0203 
0204     job->setError(errorCode);
0205     job->setErrorText(errorText);
0206 
0207     int resultEmitted = 0;
0208     // Prove to Kai Uwe that one can connect a job to a lambdas, despite the "private" signal
0209     connect(job, &KJob::result, this, [&resultEmitted](KJob *) {
0210         ++resultEmitted;
0211     });
0212 
0213     QSignalSpy destroyed_spy(job, &QObject::destroyed);
0214 
0215     QVERIFY(!job->isFinished());
0216     bool status = job->exec();
0217     QVERIFY(job->isFinished());
0218 
0219     QCOMPARE(resultEmitted, 1);
0220     QCOMPARE(status, (errorCode == KJob::NoError));
0221     QCOMPARE(job->error(), errorCode);
0222     QCOMPARE(job->errorText(), errorText);
0223 
0224     // Verify that the job is not deleted immediately...
0225     QCOMPARE(destroyed_spy.size(), 0);
0226     QTimer::singleShot(0, &loop, &QEventLoop::quit);
0227     // ... but when we enter the event loop again.
0228     loop.exec();
0229     QCOMPARE(destroyed_spy.size(), 1);
0230 }
0231 
0232 void KJobTest::testKill_data()
0233 {
0234     QTest::addColumn<int>("killVerbosity");
0235     QTest::addColumn<int>("errorCode");
0236     QTest::addColumn<QString>("errorText");
0237     QTest::addColumn<int>("resultEmitCount");
0238     QTest::addColumn<int>("finishedEmitCount");
0239 
0240     QTest::newRow("killed with result") << int(KJob::EmitResult) << int(KJob::KilledJobError) << QString() << 1 << 1;
0241     QTest::newRow("killed quietly") << int(KJob::Quietly) << int(KJob::KilledJobError) << QString() << 0 << 1;
0242 }
0243 
0244 void KJobTest::testKill()
0245 {
0246     auto *const job = setupErrorResultFinished();
0247     QSignalSpy destroyed_spy(job, &QObject::destroyed);
0248 
0249     QFETCH(int, killVerbosity);
0250     QFETCH(int, errorCode);
0251     QFETCH(QString, errorText);
0252     QFETCH(int, resultEmitCount);
0253     QFETCH(int, finishedEmitCount);
0254 
0255     QVERIFY(!job->isFinished());
0256     job->kill(static_cast<KJob::KillVerbosity>(killVerbosity));
0257     QVERIFY(job->isFinished());
0258     loop.processEvents(QEventLoop::AllEvents, 2000);
0259 
0260     QCOMPARE(m_lastError, errorCode);
0261     QCOMPARE(m_lastErrorText, errorText);
0262 
0263     QCOMPARE(job->error(), errorCode);
0264     QCOMPARE(job->errorText(), errorText);
0265 
0266     QCOMPARE(m_resultCount, resultEmitCount);
0267     QCOMPARE(m_finishedCount, finishedEmitCount);
0268 
0269     // Verify that the job is not deleted immediately...
0270     QCOMPARE(destroyed_spy.size(), 0);
0271     QTimer::singleShot(0, &loop, &QEventLoop::quit);
0272     // ... but when we enter the event loop again.
0273     loop.exec();
0274     QCOMPARE(destroyed_spy.size(), 1);
0275 
0276     QVERIFY(m_jobFinishCount.size() == (finishedEmitCount - resultEmitCount));
0277     m_jobFinishCount.clear();
0278 }
0279 
0280 void KJobTest::testDestroy()
0281 {
0282     auto *const job = setupErrorResultFinished();
0283     QVERIFY(!job->isFinished());
0284     delete job;
0285     QCOMPARE(m_lastError, static_cast<int>(KJob::NoError));
0286     QCOMPARE(m_lastErrorText, QString{});
0287     QCOMPARE(m_resultCount, 0);
0288     QCOMPARE(m_finishedCount, 1);
0289 
0290     QVERIFY(m_jobFinishCount.size() == 1);
0291     m_jobFinishCount.clear();
0292 }
0293 
0294 void KJobTest::testEmitAtMostOnce_data()
0295 {
0296     QTest::addColumn<bool>("autoDelete");
0297     QTest::addColumn<QList<Action>>("actions");
0298 
0299     const auto actionName = [](Action action) {
0300         return QMetaEnum::fromType<Action>().valueToKey(static_cast<int>(action));
0301     };
0302 
0303     for (bool autoDelete : {true, false}) {
0304         for (Action a : {Action::Start, Action::KillQuietly, Action::KillVerbosely}) {
0305             for (Action b : {Action::Start, Action::KillQuietly, Action::KillVerbosely}) {
0306                 const auto dataTag = std::string{actionName(a)} + '-' + actionName(b) + (autoDelete ? "-autoDelete" : "");
0307                 QTest::newRow(dataTag.c_str()) << autoDelete << QList<Action>{a, b};
0308             }
0309         }
0310     }
0311 }
0312 
0313 void KJobTest::testEmitAtMostOnce()
0314 {
0315     auto *const job = setupErrorResultFinished();
0316     QSignalSpy destroyed_spy(job, &QObject::destroyed);
0317 
0318     QFETCH(bool, autoDelete);
0319     job->setAutoDelete(autoDelete);
0320 
0321     QFETCH(QList<Action>, actions);
0322     for (auto action : actions) {
0323         switch (action) {
0324         case Action::Start:
0325             job->start(); // in effect calls QTimer::singleShot(0, ... emitResult)
0326             break;
0327         case Action::KillQuietly:
0328             QTimer::singleShot(0, job, [=] {
0329                 job->kill(KJob::Quietly);
0330             });
0331             break;
0332         case Action::KillVerbosely:
0333             QTimer::singleShot(0, job, [=] {
0334                 job->kill(KJob::EmitResult);
0335             });
0336             break;
0337         }
0338     }
0339 
0340     QVERIFY(!job->isFinished());
0341     loop.processEvents(QEventLoop::AllEvents, 2000);
0342     QCOMPARE(destroyed_spy.size(), autoDelete);
0343     if (!autoDelete) {
0344         QVERIFY(job->isFinished());
0345     }
0346 
0347     QVERIFY(!actions.empty());
0348     // The first action alone should determine the job's error and result.
0349     const auto firstAction = actions.front();
0350 
0351     const int errorCode = firstAction == Action::Start ? KJob::NoError : KJob::KilledJobError;
0352     QCOMPARE(m_lastError, errorCode);
0353     QCOMPARE(m_lastErrorText, QString{});
0354     if (!autoDelete) {
0355         QCOMPARE(job->error(), m_lastError);
0356         QCOMPARE(job->errorText(), m_lastErrorText);
0357     }
0358 
0359     QCOMPARE(m_resultCount, firstAction == Action::KillQuietly ? 0 : 1);
0360     QCOMPARE(m_finishedCount, 1);
0361 
0362     if (!autoDelete) {
0363         delete job;
0364     }
0365 }
0366 
0367 void KJobTest::testDelegateUsage()
0368 {
0369     TestJob *job1 = new TestJob;
0370     TestJob *job2 = new TestJob;
0371     TestJobUiDelegate *delegate = new TestJobUiDelegate;
0372     QPointer<TestJobUiDelegate> guard(delegate);
0373 
0374     QVERIFY(job1->uiDelegate() == nullptr);
0375     job1->setUiDelegate(delegate);
0376     QVERIFY(job1->uiDelegate() == delegate);
0377 
0378     QVERIFY(job2->uiDelegate() == nullptr);
0379     job2->setUiDelegate(delegate);
0380     QVERIFY(job2->uiDelegate() == nullptr);
0381 
0382     delete job1;
0383     delete job2;
0384     QVERIFY(guard.isNull()); // deleted by job1
0385 }
0386 
0387 void KJobTest::testNestedExec()
0388 {
0389     m_innerJob = nullptr;
0390     QTimer::singleShot(100, this, &KJobTest::slotStartInnerJob);
0391     m_outerJob = new WaitJob();
0392     m_outerJob->exec();
0393 }
0394 
0395 void KJobTest::slotStartInnerJob()
0396 {
0397     QTimer::singleShot(100, this, &KJobTest::slotFinishOuterJob);
0398     m_innerJob = new WaitJob();
0399     m_innerJob->exec();
0400 }
0401 
0402 void KJobTest::slotFinishOuterJob()
0403 {
0404     QTimer::singleShot(100, this, &KJobTest::slotFinishInnerJob);
0405     m_outerJob->makeItFinish();
0406 }
0407 
0408 void KJobTest::slotFinishInnerJob()
0409 {
0410     m_innerJob->makeItFinish();
0411 }
0412 
0413 void KJobTest::slotResult(KJob *job)
0414 {
0415     const auto testJob = qobject_cast<const TestJob *>(job);
0416     QVERIFY(testJob);
0417     QVERIFY(testJob->isFinished());
0418 
0419     // Ensure the job has already emitted finished() if we are tracking from
0420     // setupErrorResultFinished
0421     if (m_jobFinishCount.contains(job)) {
0422         QVERIFY(m_jobFinishCount.value(job) == 1);
0423         QVERIFY(m_jobFinishCount.remove(job) == 1 /* num items removed */);
0424     }
0425 
0426     if (job->error()) {
0427         m_lastError = job->error();
0428         m_lastErrorText = job->errorText();
0429     } else {
0430         m_lastError = KJob::NoError;
0431         m_lastErrorText.clear();
0432     }
0433 
0434     m_resultCount++;
0435 }
0436 
0437 void KJobTest::slotFinished(KJob *job)
0438 {
0439     QVERIFY2(m_jobFinishCount.value(job) == 0, "Ensure we have not double-emitted KJob::finished()");
0440     m_jobFinishCount[job]++;
0441 
0442     if (job->error()) {
0443         m_lastError = job->error();
0444         m_lastErrorText = job->errorText();
0445     } else {
0446         m_lastError = KJob::NoError;
0447         m_lastErrorText.clear();
0448     }
0449 
0450     m_finishedCount++;
0451 }
0452 
0453 TestJob *KJobTest::setupErrorResultFinished()
0454 {
0455     m_lastError = KJob::UserDefinedError;
0456     m_lastErrorText.clear();
0457     m_resultCount = 0;
0458     m_finishedCount = 0;
0459 
0460     auto *job = new TestJob;
0461     m_jobFinishCount[job] = 0;
0462     connect(job, &KJob::result, this, &KJobTest::slotResult);
0463     connect(job, &KJob::finished, this, &KJobTest::slotFinished);
0464     return job;
0465 }
0466 
0467 TestJob::TestJob()
0468     : KJob()
0469 {
0470 }
0471 
0472 TestJob::~TestJob()
0473 {
0474 }
0475 
0476 void TestJob::start()
0477 {
0478     QTimer::singleShot(0, this, [this] {
0479         emitResult();
0480     });
0481 }
0482 
0483 bool TestJob::doKill()
0484 {
0485     return true;
0486 }
0487 
0488 void TestJob::setError(int errorCode)
0489 {
0490     KJob::setError(errorCode);
0491 }
0492 
0493 void TestJob::setErrorText(const QString &errorText)
0494 {
0495     KJob::setErrorText(errorText);
0496 }
0497 
0498 void TestJob::setProcessedSize(qulonglong size)
0499 {
0500     KJob::setProcessedAmount(KJob::Bytes, size);
0501 }
0502 
0503 void TestJob::setTotalSize(qulonglong size)
0504 {
0505     KJob::setTotalAmount(KJob::Bytes, size);
0506 }
0507 
0508 void TestJob::setProcessedFiles(qulonglong files)
0509 {
0510     KJob::setProcessedAmount(KJob::Files, files);
0511 }
0512 
0513 void TestJob::setTotalFiles(qulonglong files)
0514 {
0515     KJob::setTotalAmount(KJob::Files, files);
0516 }
0517 
0518 void TestJob::setPercent(unsigned long percentage)
0519 {
0520     KJob::setPercent(percentage);
0521 }
0522 
0523 void WaitJob::start()
0524 {
0525 }
0526 
0527 void WaitJob::makeItFinish()
0528 {
0529     emitResult();
0530 }
0531 
0532 void TestJobUiDelegate::connectJob(KJob *job)
0533 {
0534     QVERIFY(job->uiDelegate() != nullptr);
0535 }
0536 
0537 #include "moc_kjobtest.cpp"