File indexing completed on 2024-11-10 04:40:21
0001 /* 0002 SPDX-FileCopyrightText: 2013 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include <QMutex> 0008 #include <QObject> 0009 #include <QTest> 0010 #include <QTimer> 0011 0012 #include "commandcontext.h" 0013 #include "storage/datastore.h" 0014 #include "storage/itemretrievaljob.h" 0015 #include "storage/itemretrievalmanager.h" 0016 #include "storage/itemretrievalrequest.h" 0017 #include "storage/itemretriever.h" 0018 0019 #include "dbinitializer.h" 0020 #include "fakeakonadiserver.h" 0021 0022 #include "aktest.h" 0023 0024 using namespace Akonadi::Server; 0025 using namespace std::chrono_literals; 0026 struct JobResult { 0027 qint64 pimItemId; 0028 QByteArray partname; 0029 QByteArray partdata; 0030 QString error; 0031 }; 0032 0033 class FakeItemRetrievalJob : public AbstractItemRetrievalJob 0034 { 0035 Q_OBJECT 0036 public: 0037 FakeItemRetrievalJob(ItemRetrievalRequest req, DbInitializer &dbInitializer, const QList<JobResult> &results, QObject *parent) 0038 : AbstractItemRetrievalJob(std::move(req), parent) 0039 , mDbInitializer(dbInitializer) 0040 , mResults(results) 0041 { 0042 } 0043 0044 void start() override 0045 { 0046 for (const JobResult &res : std::as_const(mResults)) { 0047 if (res.error.isEmpty()) { 0048 // This is analogous to what STORE/MERGE does 0049 const PimItem item = PimItem::retrieveById(res.pimItemId); 0050 const auto parts = item.parts(); 0051 // Try to find the part by name 0052 auto it = std::find_if(parts.begin(), parts.end(), [res](const Part &part) { 0053 return part.partType().name().toLatin1() == res.partname; 0054 }); 0055 if (it == parts.end()) { 0056 // Does not exist, create it 0057 mDbInitializer.createPart(res.pimItemId, "PLD:" + res.partname, res.partdata); 0058 } else { 0059 // Exist, update it 0060 Part part(*it); 0061 part.setData(res.partdata); 0062 part.setDatasize(res.partdata.size()); 0063 part.update(); 0064 } 0065 } else { 0066 m_result.errorMsg = res.error; 0067 break; 0068 } 0069 } 0070 0071 QTimer::singleShot(0s, this, [this]() { 0072 Q_EMIT requestCompleted(this); 0073 }); 0074 } 0075 0076 void kill() override 0077 { 0078 // TODO 0079 Q_ASSERT(false); 0080 } 0081 0082 private: 0083 DbInitializer &mDbInitializer; 0084 QList<JobResult> mResults; 0085 }; 0086 0087 class FakeItemRetrievalJobFactory : public AbstractItemRetrievalJobFactory 0088 { 0089 public: 0090 explicit FakeItemRetrievalJobFactory(DbInitializer &initializer) 0091 : mJobsCount(0) 0092 , mDbInitializer(initializer) 0093 { 0094 } 0095 0096 void addJobResult(qint64 itemId, const QByteArray &partname, const QByteArray &partdata) 0097 { 0098 mJobResults.insert(itemId, JobResult{itemId, partname, partdata, QString()}); 0099 } 0100 0101 void addJobResult(qint64 itemId, const QString &error) 0102 { 0103 mJobResults.insert(itemId, JobResult{itemId, QByteArray(), QByteArray(), error}); 0104 } 0105 0106 AbstractItemRetrievalJob *retrievalJob(ItemRetrievalRequest request, QObject *parent) override 0107 { 0108 QList<JobResult> results; 0109 for (auto id : std::as_const(request.ids)) { 0110 auto it = mJobResults.constFind(id); 0111 while (it != mJobResults.constEnd() && it.key() == id) { 0112 if (request.parts.contains(it->partname)) { 0113 results << *it; 0114 } 0115 ++it; 0116 } 0117 } 0118 0119 ++mJobsCount; 0120 return new FakeItemRetrievalJob(std::move(request), mDbInitializer, results, parent); 0121 } 0122 0123 int jobsCount() const 0124 { 0125 return mJobsCount; 0126 } 0127 0128 private: 0129 int mJobsCount; 0130 DbInitializer &mDbInitializer; 0131 QMultiHash<qint64, JobResult> mJobResults; 0132 }; 0133 0134 using RequestedParts = QList<QByteArray /* FQ name */>; 0135 0136 class ClientThread : public QThread 0137 { 0138 public: 0139 ClientThread(Entity::Id itemId, const RequestedParts &requestedParts, ItemRetrievalManager &manager) 0140 : m_itemId(itemId) 0141 , m_requestedParts(requestedParts) 0142 , m_manager(manager) 0143 { 0144 } 0145 0146 void run() override 0147 { 0148 // ItemRetriever should... 0149 CommandContext context; 0150 ItemRetriever retriever(m_manager, nullptr, context); 0151 retriever.setItem(m_itemId); 0152 retriever.setRetrieveParts(m_requestedParts); 0153 QSignalSpy spy(&retriever, &ItemRetriever::itemsRetrieved); 0154 0155 const bool success = retriever.exec(); 0156 0157 QMutexLocker lock(&m_mutex); 0158 m_results.success = success; 0159 m_results.signalsCount = spy.count(); 0160 if (m_results.signalsCount > 0) { 0161 m_results.emittedItems = spy.at(0).at(0).value<QList<qint64>>(); 0162 } 0163 0164 DataStore::self()->close(); 0165 } 0166 0167 struct Results { 0168 bool success; 0169 int signalsCount; 0170 QList<qint64> emittedItems; 0171 }; 0172 Results results() const 0173 { 0174 QMutexLocker lock(&m_mutex); 0175 return m_results; 0176 } 0177 0178 private: 0179 const Entity::Id m_itemId; 0180 const RequestedParts m_requestedParts; 0181 ItemRetrievalManager &m_manager; 0182 0183 mutable QMutex m_mutex; // protects results below 0184 Results m_results; 0185 }; 0186 0187 class ItemRetrieverTest : public QObject 0188 { 0189 Q_OBJECT 0190 0191 using ExistingParts = QList<QPair<QByteArray /* name */, QByteArray /* data */>>; 0192 using AvailableParts = QList<QPair<QByteArray /* name */, QByteArray /* data */>>; 0193 0194 FakeAkonadiServer mAkonadi; 0195 0196 public: 0197 ItemRetrieverTest() 0198 { 0199 mAkonadi.setPopulateDb(false); 0200 mAkonadi.disableItemRetrievalManager(); 0201 mAkonadi.init(); 0202 } 0203 0204 private Q_SLOTS: 0205 void testFullPayload() 0206 { 0207 CommandContext context; 0208 ItemRetriever r1(mAkonadi.itemRetrievalManager(), nullptr, context); 0209 r1.setRetrieveFullPayload(true); 0210 QCOMPARE(r1.retrieveParts().size(), 1); 0211 QCOMPARE(r1.retrieveParts().at(0), QByteArray{"PLD:RFC822"}); 0212 r1.setRetrieveParts({"PLD:FOO"}); 0213 QCOMPARE(r1.retrieveParts().size(), 2); 0214 } 0215 0216 void testRetrieval_data() 0217 { 0218 QTest::addColumn<ExistingParts>("existingParts"); 0219 QTest::addColumn<AvailableParts>("availableParts"); 0220 QTest::addColumn<RequestedParts>("requestedParts"); 0221 QTest::addColumn<int>("expectedRetrievalJobs"); 0222 QTest::addColumn<int>("expectedSignals"); 0223 QTest::addColumn<int>("expectedParts"); 0224 0225 QTest::newRow("should retrieve missing payload part") 0226 << ExistingParts() << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:RFC822"} << 1 << 1 << 1; 0227 0228 QTest::newRow("should retrieve multiple missing payload parts") 0229 << ExistingParts() << AvailableParts{{"RFC822", "somedata"}, {"HEAD", "head"}} << RequestedParts{"PLD:HEAD", "PLD:RFC822"} << 1 << 1 << 2; 0230 0231 QTest::newRow("should not retrieve existing payload part") 0232 << ExistingParts{{"PLD:RFC822", "somedata"}} << AvailableParts() << RequestedParts{"PLD:RFC822"} << 0 << 1 << 1; 0233 0234 QTest::newRow("should not retrieve multiple existing payload parts") 0235 << ExistingParts{{"PLD:RFC822", "somedata"}, {"PLD:HEAD", "head"}} << AvailableParts() << RequestedParts{"PLD:RFC822", "PLD:HEAD"} << 0 << 1 << 2; 0236 0237 QTest::newRow("should retrieve missing but not existing payload part") 0238 << ExistingParts{{"PLD:HEAD", "head"}} << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:HEAD", "PLD:RFC822"} << 1 << 1 << 2; 0239 0240 QTest::newRow("should retrieve expired payload part") 0241 << ExistingParts{{"PLD:RFC822", QByteArray()}} << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:RFc822"} << 1 << 1 << 1; 0242 0243 QTest::newRow("should not retrieve one out of multiple existing payload parts") 0244 << ExistingParts{{"PLD:RFC822", "somedata"}, {"PLD:HEAD", "head"}, {"PLD:ENVELOPE", "envelope"}} << AvailableParts() 0245 << RequestedParts{"PLD:RFC822", "PLD:HEAD"} << 0 << 1 << 3; 0246 0247 QTest::newRow("should retrieve missing payload part and ignore attributes") 0248 << ExistingParts{{"ATR:MYATTR", "myattrdata"}} << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:RFC822"} << 1 << 1 << 2; 0249 } 0250 0251 void testRetrieval() 0252 { 0253 QFETCH(ExistingParts, existingParts); 0254 QFETCH(AvailableParts, availableParts); 0255 QFETCH(RequestedParts, requestedParts); 0256 QFETCH(int, expectedRetrievalJobs); 0257 QFETCH(int, expectedSignals); 0258 QFETCH(int, expectedParts); 0259 0260 // Setup 0261 for (int step = 0; step < 2; ++step) { 0262 DbInitializer dbInitializer; 0263 auto factory = new FakeItemRetrievalJobFactory(dbInitializer); 0264 auto mgr = AkThread::create<ItemRetrievalManager>(std::unique_ptr<AbstractItemRetrievalJobFactory>(factory)); 0265 QTest::qWait(100); 0266 0267 // Given a PimItem with existing parts 0268 Resource res = dbInitializer.createResource("testresource"); 0269 Collection col = dbInitializer.createCollection("col1"); 0270 0271 // step 0: do it in the main thread, for easier debugging 0272 PimItem item = dbInitializer.createItem("1", col); 0273 for (const auto &existingPart : std::as_const(existingParts)) { 0274 dbInitializer.createPart(item.id(), existingPart.first, existingPart.second); 0275 } 0276 0277 for (const auto &availablePart : std::as_const(availableParts)) { 0278 factory->addJobResult(item.id(), availablePart.first, availablePart.second); 0279 } 0280 0281 if (step == 0) { 0282 ClientThread thread(item.id(), requestedParts, *mgr); 0283 thread.run(); 0284 0285 const ClientThread::Results results = thread.results(); 0286 // ItemRetriever should ... succeed 0287 QVERIFY(results.success); 0288 // Emit exactly one signal ... 0289 QCOMPARE(results.signalsCount, expectedSignals); 0290 // ... with that one item 0291 if (expectedSignals > 0) { 0292 QCOMPARE(results.emittedItems, QList<qint64>{item.id()}); 0293 } 0294 0295 // Check that the factory had exactly one retrieval job 0296 QCOMPARE(factory->jobsCount(), expectedRetrievalJobs); 0297 0298 } else { 0299 QList<ClientThread *> threads; 0300 for (int i = 0; i < 20; ++i) { 0301 threads.append(new ClientThread(item.id(), requestedParts, *mgr)); 0302 } 0303 for (int i = 0; i < threads.size(); ++i) { 0304 threads.at(i)->start(); 0305 } 0306 for (int i = 0; i < threads.size(); ++i) { 0307 threads.at(i)->wait(); 0308 } 0309 for (int i = 0; i < threads.size(); ++i) { 0310 const ClientThread::Results results = threads.at(i)->results(); 0311 QVERIFY(results.success); 0312 QCOMPARE(results.signalsCount, expectedSignals); 0313 if (expectedSignals > 0) { 0314 QCOMPARE(results.emittedItems, QList<qint64>{item.id()}); 0315 } 0316 } 0317 qDeleteAll(threads); 0318 } 0319 0320 // Check that the parts now exist in the DB 0321 const auto parts = item.parts(); 0322 QCOMPARE(parts.count(), expectedParts); 0323 for (const Part &dbPart : parts) { 0324 const QString fqname = dbPart.partType().ns() + QLatin1Char(':') + dbPart.partType().name(); 0325 if (!requestedParts.contains(fqname.toLatin1())) { 0326 continue; 0327 } 0328 0329 auto it = std::find_if(availableParts.constBegin(), availableParts.constEnd(), [dbPart](const QPair<QByteArray, QByteArray> &p) { 0330 return dbPart.partType().name().toLatin1() == p.first; 0331 }); 0332 if (it == availableParts.constEnd()) { 0333 it = std::find_if(existingParts.constBegin(), existingParts.constEnd(), [fqname](const QPair<QByteArray, QByteArray> &p) { 0334 return fqname.toLatin1() == p.first; 0335 }); 0336 QVERIFY(it != existingParts.constEnd()); 0337 } 0338 0339 QCOMPARE(dbPart.data(), it->second); 0340 QCOMPARE(dbPart.datasize(), it->second.size()); 0341 } 0342 } 0343 } 0344 }; 0345 0346 AKTEST_FAKESERVER_MAIN(ItemRetrieverTest) 0347 0348 #include "itemretrievertest.moc"