File indexing completed on 2024-05-19 04:39:57

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2013 Kevin Funk <kevin@kfunk.org>
0004     SPDX-FileCopyrightText: 2023 Igor Kushnir <igorkuo@gmail.com>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-only
0007 */
0008 
0009 #include "kcompoundjobtest.h"
0010 
0011 #include "ksequentialcompoundjob.h"
0012 
0013 #include <QEventLoop>
0014 #include <QMetaEnum>
0015 #include <QSignalSpy>
0016 #include <QStandardPaths>
0017 #include <QTest>
0018 #include <QTimer>
0019 
0020 namespace
0021 {
0022 class TestSequentialCompoundJob : public KSequentialCompoundJob
0023 {
0024 public:
0025     using KSequentialCompoundJob::addSubjob;
0026     using KSequentialCompoundJob::clearSubjobs;
0027 };
0028 
0029 struct JobSpies {
0030     QSignalSpy finished;
0031     QSignalSpy result;
0032     QSignalSpy destroyed;
0033     explicit JobSpies(KJob *job)
0034         : finished(job, &KJob::finished)
0035         , result(job, &KJob::result)
0036         , destroyed(job, &QObject::destroyed)
0037     {
0038     }
0039 };
0040 } // namespace
0041 
0042 TestJob::TestJob(QObject *parent)
0043     : KJob(parent)
0044 {
0045 }
0046 
0047 void TestJob::start()
0048 {
0049     QTimer::singleShot(1000, this, &TestJob::emitResult);
0050 }
0051 
0052 KillableTestJob::KillableTestJob(QObject *parent)
0053     : TestJob(parent)
0054 {
0055     setCapabilities(Killable);
0056 }
0057 
0058 bool KillableTestJob::doKill()
0059 {
0060     return true;
0061 }
0062 
0063 void TestCompoundJob::start()
0064 {
0065     if (hasSubjobs()) {
0066         subjobs().first()->start();
0067     } else {
0068         emitResult();
0069     }
0070 }
0071 
0072 void TestCompoundJob::subjobFinished(KJob *job)
0073 {
0074     KCompoundJob::subjobFinished(job);
0075 
0076     if (error()) {
0077         return; // KCompoundJob::subjobFinished() must have called emitResult().
0078     }
0079     if (hasSubjobs()) {
0080         // start next
0081         subjobs().first()->start();
0082     } else {
0083         emitResult();
0084     }
0085 }
0086 
0087 KCompoundJobTest::KCompoundJobTest()
0088 {
0089 }
0090 
0091 void KCompoundJobTest::initTestCase()
0092 {
0093     QStandardPaths::setTestModeEnabled(true);
0094 }
0095 
0096 /**
0097  * In case a compound job is deleted during execution
0098  * we still want to assure that we don't crash
0099  *
0100  * see bug: https://bugs.kde.org/show_bug.cgi?id=230692
0101  */
0102 template<class CompoundJob>
0103 static void testDeletionDuringExecution()
0104 {
0105     QObject *someParent = new QObject;
0106     KJob *job = new TestJob(someParent);
0107 
0108     auto *compoundJob = new CompoundJob;
0109     compoundJob->setAutoDelete(false);
0110     QVERIFY(compoundJob->addSubjob(job));
0111 
0112     QCOMPARE(job->parent(), compoundJob);
0113 
0114     QSignalSpy destroyed_spy(job, &QObject::destroyed);
0115     // check if job got reparented properly
0116     delete someParent;
0117     someParent = nullptr;
0118     // the job should still exist, because it is a child of KCompoundJob now
0119     QCOMPARE(destroyed_spy.size(), 0);
0120 
0121     // start async, the subjob takes 1 second to finish
0122     compoundJob->start();
0123 
0124     // delete the job during the execution
0125     delete compoundJob;
0126     compoundJob = nullptr;
0127     // at this point, the subjob should be deleted, too
0128     QCOMPARE(destroyed_spy.size(), 1);
0129 }
0130 
0131 void KCompoundJobTest::testDeletionDuringExecution_data()
0132 {
0133     QTest::addColumn<bool>("useSequentialCompoundJob");
0134     QTest::newRow("CompoundJob") << false;
0135     QTest::newRow("SequentialCompoundJob") << true;
0136 }
0137 
0138 void KCompoundJobTest::testDeletionDuringExecution()
0139 {
0140     QFETCH(const bool, useSequentialCompoundJob);
0141     if (useSequentialCompoundJob) {
0142         ::testDeletionDuringExecution<TestSequentialCompoundJob>();
0143     } else {
0144         ::testDeletionDuringExecution<TestCompoundJob>();
0145     }
0146 }
0147 
0148 template<class CompoundJob>
0149 static void testFinishingSubjob()
0150 {
0151     auto *const job = new KillableTestJob;
0152     auto *const compoundJob = new CompoundJob;
0153     QVERIFY(compoundJob->addSubjob(job));
0154 
0155     JobSpies jobSpies(job);
0156     JobSpies compoundJobSpies(compoundJob);
0157 
0158     compoundJob->start();
0159 
0160     using Action = KCompoundJobTest::Action;
0161     QFETCH(const Action, action);
0162     switch (action) {
0163     case Action::Finish:
0164         job->emitResult();
0165         break;
0166     case Action::KillVerbosely:
0167         QVERIFY(job->kill(KJob::EmitResult));
0168         break;
0169     case Action::KillQuietly:
0170         QVERIFY(job->kill(KJob::Quietly));
0171         break;
0172     case Action::Destroy:
0173         job->deleteLater();
0174         break;
0175     }
0176 
0177     QEventLoop loop;
0178     QTimer::singleShot(100, &loop, &QEventLoop::quit);
0179     QObject::connect(compoundJob, &QObject::destroyed, &loop, &QEventLoop::quit);
0180     QCOMPARE(loop.exec(), 0);
0181 
0182     // The following 3 comparisons verify that KJob works as expected.
0183     QCOMPARE(jobSpies.finished.size(), 1); // KJob::finished() is always emitted.
0184     // KJob::result() is not emitted when a job is killed quietly or destroyed.
0185     QCOMPARE(jobSpies.result.size(), action == Action::Finish || action == Action::KillVerbosely);
0186     // An auto-delete job is destroyed via deleteLater() when finished.
0187     QCOMPARE(jobSpies.destroyed.size(), 1);
0188 
0189     // KCompoundJob must listen to &KJob::finished signal to invoke subjobFinished()
0190     // no matter how a subjob is finished - normally, killed or destroyed.
0191     // CompoundJob calls emitResult() and is destroyed when its last subjob finishes.
0192     QFETCH(const bool, crashOnFailure);
0193     if (crashOnFailure) {
0194         if (compoundJobSpies.destroyed.empty()) {
0195             // compoundJob is still alive. This must be a bug.
0196             // The clearSubjobs() call will segfault if the already destroyed job
0197             // has not been removed from the subjob list.
0198             compoundJob->clearSubjobs();
0199             delete compoundJob;
0200         }
0201     } else {
0202         QCOMPARE(compoundJobSpies.finished.size(), 1);
0203         QCOMPARE(compoundJobSpies.result.size(), 1);
0204         QCOMPARE(compoundJobSpies.destroyed.size(), 1);
0205     }
0206 }
0207 
0208 void KCompoundJobTest::testFinishingSubjob_data()
0209 {
0210     QTest::addColumn<bool>("useSequentialCompoundJob");
0211     QTest::addColumn<Action>("action");
0212     QTest::addColumn<bool>("crashOnFailure");
0213 
0214     const auto actionName = [](Action action) {
0215         return QMetaEnum::fromType<Action>().valueToKey(static_cast<int>(action));
0216     };
0217 
0218     for (bool useSequentialCompoundJob : {false, true}) {
0219         const char *const sequentialStr = useSequentialCompoundJob ? "sequential-" : "";
0220         for (bool crashOnFailure : {false, true}) {
0221             const char *const failureStr = crashOnFailure ? "segfault-on-failure" : "compound-job-destroyed";
0222             for (Action action : {Action::Finish, Action::KillVerbosely, Action::KillQuietly, Action::Destroy}) {
0223                 const QByteArray dataTag = QByteArray{actionName(action)} + "-a-subjob-" + sequentialStr + failureStr;
0224                 QTest::newRow(dataTag.constData()) << useSequentialCompoundJob << action << crashOnFailure;
0225             }
0226         }
0227     }
0228 }
0229 
0230 void KCompoundJobTest::testFinishingSubjob()
0231 {
0232     QFETCH(const bool, useSequentialCompoundJob);
0233     if (useSequentialCompoundJob) {
0234         ::testFinishingSubjob<TestSequentialCompoundJob>();
0235     } else {
0236         ::testFinishingSubjob<TestCompoundJob>();
0237     }
0238 }
0239 
0240 QTEST_GUILESS_MAIN(KCompoundJobTest)