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)