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"