File indexing completed on 2024-12-22 04:57:38

0001 /*  This file is part of the KDE project
0002     SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0003     SPDX-FileContributor: Kevin Krammer <krake@kdab.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "mixedmaildirstore.h"
0009 
0010 #include "testdatautil.h"
0011 
0012 #include "filestore/entitycompactchangeattribute.h"
0013 #include "filestore/itemdeletejob.h"
0014 #include "filestore/storecompactjob.h"
0015 
0016 #include <KMbox/MBox>
0017 
0018 #include <QTemporaryDir>
0019 
0020 #include <QSignalSpy>
0021 
0022 #include <QDir>
0023 #include <QFileInfo>
0024 #include <QTest>
0025 using namespace Akonadi;
0026 using namespace KMBox;
0027 
0028 static Collection::List collectionsFromSpy(QSignalSpy *spy)
0029 {
0030     Collection::List collections;
0031 
0032     QListIterator<QList<QVariant>> it(*spy);
0033     while (it.hasNext()) {
0034         const QList<QVariant> invocation = it.next();
0035         Q_ASSERT(invocation.count() == 1);
0036 
0037         collections << invocation.first().value<Collection::List>();
0038     }
0039 
0040     return collections;
0041 }
0042 
0043 static Item::List itemsFromSpy(QSignalSpy *spy)
0044 {
0045     Item::List items;
0046 
0047     QListIterator<QList<QVariant>> it(*spy);
0048     while (it.hasNext()) {
0049         const QList<QVariant> invocation = it.next();
0050         Q_ASSERT(invocation.count() == 1);
0051 
0052         items << invocation.first().value<Item::List>();
0053     }
0054 
0055     return items;
0056 }
0057 
0058 static bool fullEntryCompare(const MBoxEntry &a, const MBoxEntry &b)
0059 {
0060     return a.messageOffset() == b.messageOffset() && a.separatorSize() == b.separatorSize() && a.messageSize() == b.messageSize();
0061 }
0062 
0063 static quint64 changedOffset(const Item &item)
0064 {
0065     Q_ASSERT(item.hasAttribute<FileStore::EntityCompactChangeAttribute>());
0066 
0067     const QString remoteId = item.attribute<FileStore::EntityCompactChangeAttribute>()->remoteId();
0068     Q_ASSERT(!remoteId.isEmpty());
0069 
0070     bool ok = false;
0071     const quint64 result = remoteId.toULongLong(&ok);
0072     Q_ASSERT(ok);
0073 
0074     return result;
0075 }
0076 
0077 class StoreCompactTest : public QObject
0078 {
0079     Q_OBJECT
0080 
0081 public:
0082     StoreCompactTest()
0083         : QObject()
0084         , mStore(nullptr)
0085         , mDir(nullptr)
0086     {
0087         // for monitoring signals
0088         qRegisterMetaType<Akonadi::Collection::List>();
0089         qRegisterMetaType<Akonadi::Item::List>();
0090     }
0091 
0092     ~StoreCompactTest() override
0093     {
0094         delete mStore;
0095         delete mDir;
0096     }
0097 
0098 private:
0099     MixedMaildirStore *mStore = nullptr;
0100     QTemporaryDir *mDir = nullptr;
0101 
0102 private Q_SLOTS:
0103     void init();
0104     void cleanup();
0105     void testCompact();
0106 };
0107 
0108 void StoreCompactTest::init()
0109 {
0110     mStore = new MixedMaildirStore;
0111 
0112     mDir = new QTemporaryDir;
0113     QVERIFY(mDir->isValid());
0114     QVERIFY(QDir(mDir->path()).exists());
0115 }
0116 
0117 void StoreCompactTest::cleanup()
0118 {
0119     delete mStore;
0120     mStore = nullptr;
0121     delete mDir;
0122     mDir = nullptr;
0123 }
0124 
0125 void StoreCompactTest::testCompact()
0126 {
0127     QDir topDir(mDir->path());
0128 
0129     QVERIFY(TestDataUtil::installFolder(QLatin1StringView("mbox"), topDir.path(), QStringLiteral("collection1")));
0130     QVERIFY(TestDataUtil::installFolder(QLatin1StringView("mbox"), topDir.path(), QStringLiteral("collection2")));
0131     QVERIFY(TestDataUtil::installFolder(QLatin1StringView("mbox"), topDir.path(), QStringLiteral("collection3")));
0132     QVERIFY(TestDataUtil::installFolder(QLatin1StringView("mbox"), topDir.path(), QStringLiteral("collection4")));
0133 
0134     QFileInfo fileInfo1(topDir.path(), QStringLiteral("collection1"));
0135     MBox mbox1;
0136     QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
0137     MBoxEntry::List entryList1 = mbox1.entries();
0138     QCOMPARE((int)entryList1.count(), 4);
0139 
0140     QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
0141     MBox mbox2;
0142     QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
0143     MBoxEntry::List entryList2 = mbox2.entries();
0144     QCOMPARE((int)entryList2.count(), 4);
0145 
0146     QFileInfo fileInfo3(topDir.path(), QStringLiteral("collection3"));
0147     MBox mbox3;
0148     QVERIFY(mbox3.load(fileInfo3.absoluteFilePath()));
0149     MBoxEntry::List entryList3 = mbox3.entries();
0150     QCOMPARE((int)entryList3.count(), 4);
0151 
0152     QFileInfo fileInfo4(topDir.path(), QStringLiteral("collection4"));
0153     MBox mbox4;
0154     QVERIFY(mbox4.load(fileInfo4.absoluteFilePath()));
0155     MBoxEntry::List entryList4 = mbox4.entries();
0156     QCOMPARE((int)entryList4.count(), 4);
0157 
0158     mStore->setPath(topDir.path());
0159 
0160     // common variables
0161     FileStore::CollectionFetchJob *collectionFetch = nullptr;
0162     FileStore::ItemDeleteJob *itemDelete = nullptr;
0163     FileStore::StoreCompactJob *job = nullptr;
0164 
0165     Collection::List collections;
0166     Item::List items;
0167 
0168     QSignalSpy *collectionSpy = nullptr;
0169     QSignalSpy *itemSpy = nullptr;
0170 
0171     MBoxEntry::List entryList;
0172     Collection collection;
0173     FileStore::EntityCompactChangeAttribute *attribute = nullptr;
0174 
0175     const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
0176     QVariant var;
0177 
0178     // test compact after delete from the end of an mbox
0179     Collection collection1;
0180     collection1.setName(QStringLiteral("collection1"));
0181     collection1.setRemoteId(QStringLiteral("collection1"));
0182     collection1.setParentCollection(mStore->topLevelCollection());
0183 
0184     Item item1;
0185     item1.setRemoteId(QString::number(entryList1.last().messageOffset()));
0186     item1.setParentCollection(collection1);
0187 
0188     itemDelete = mStore->deleteItem(item1);
0189 
0190     QVERIFY(itemDelete->exec());
0191 
0192     job = mStore->compactStore();
0193 
0194     collectionSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::collectionsChanged);
0195     itemSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::itemsChanged);
0196 
0197     QVERIFY(job->exec());
0198     QCOMPARE(job->error(), 0);
0199 
0200     collections = job->changedCollections();
0201     items = job->changedItems();
0202 
0203     QCOMPARE(collections.count(), 0);
0204     QCOMPARE(items.count(), 0);
0205 
0206     QCOMPARE(collectionSpy->count(), 0);
0207     QCOMPARE(itemSpy->count(), 0);
0208 
0209     QVERIFY(mbox1.load(mbox1.fileName()));
0210     entryList = mbox1.entries();
0211     entryList1.pop_back();
0212     QVERIFY(std::equal(entryList.begin(), entryList.end(), entryList1.begin(), fullEntryCompare));
0213 
0214     var = job->property("onDiskIndexInvalidated");
0215     QVERIFY(var.isValid());
0216     QCOMPARE(var.userType(), colListVar.userType());
0217 
0218     collections = var.value<Collection::List>();
0219     QCOMPARE((int)collections.count(), 1);
0220     QCOMPARE(collections, Collection::List() << collection1);
0221 
0222     // test compact after delete from before the end of an mbox
0223     Collection collection2;
0224     collection2.setName(QStringLiteral("collection2"));
0225     collection2.setRemoteId(QStringLiteral("collection2"));
0226     collection2.setParentCollection(mStore->topLevelCollection());
0227 
0228     Item item2;
0229     item2.setRemoteId(QString::number(entryList2.first().messageOffset()));
0230     item2.setParentCollection(collection2);
0231 
0232     itemDelete = mStore->deleteItem(item2);
0233 
0234     QVERIFY(itemDelete->exec());
0235 
0236     job = mStore->compactStore();
0237 
0238     collectionSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::collectionsChanged);
0239     itemSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::itemsChanged);
0240 
0241     QVERIFY(job->exec());
0242     QCOMPARE(job->error(), 0);
0243 
0244     collections = job->changedCollections();
0245     items = job->changedItems();
0246 
0247     QCOMPARE(collections.count(), 1);
0248     QCOMPARE(items.count(), 3);
0249 
0250     QCOMPARE(collectionSpy->count(), 1);
0251     QCOMPARE(itemSpy->count(), 1);
0252 
0253     QCOMPARE(collectionsFromSpy(collectionSpy), collections);
0254     QCOMPARE(itemsFromSpy(itemSpy), items);
0255 
0256     collection = collections.first();
0257     QCOMPARE(collection, collection2);
0258     QVERIFY(collection.hasAttribute<FileStore::EntityCompactChangeAttribute>());
0259     attribute = collection.attribute<FileStore::EntityCompactChangeAttribute>();
0260     const QString remoteRevision = attribute->remoteRevision();
0261     QCOMPARE(remoteRevision.toInt(), collection2.remoteRevision().toInt() + 1);
0262 
0263     QVERIFY(mbox2.load(mbox2.fileName()));
0264     entryList = mbox2.entries();
0265 
0266     entryList2.pop_front();
0267     for (int i = 0; i < items.count(); ++i) {
0268         entryList2[i] = MBoxEntry(changedOffset(items[i]));
0269     }
0270     QCOMPARE(entryList, entryList2);
0271 
0272     var = job->property("onDiskIndexInvalidated");
0273     QVERIFY(var.isValid());
0274     QCOMPARE(var.userType(), colListVar.userType());
0275 
0276     collections = var.value<Collection::List>();
0277     QCOMPARE((int)collections.count(), 1);
0278     QCOMPARE(collections, Collection::List() << collection2);
0279 
0280     collectionFetch = mStore->fetchCollections(collection2, FileStore::CollectionFetchJob::Base);
0281 
0282     QVERIFY(collectionFetch->exec());
0283 
0284     collections = collectionFetch->collections();
0285     QCOMPARE((int)collections.count(), 1);
0286 
0287     collection = collections.first();
0288     QCOMPARE(collection, collection2);
0289     QCOMPARE(collection.remoteRevision(), remoteRevision);
0290 
0291     // test compact after delete from before the end of more than one mbox
0292     Collection collection3;
0293     collection3.setName(QStringLiteral("collection3"));
0294     collection3.setRemoteId(QStringLiteral("collection3"));
0295     collection3.setParentCollection(mStore->topLevelCollection());
0296 
0297     Item item3;
0298     item3.setRemoteId(QString::number(entryList3.first().messageOffset()));
0299     item3.setParentCollection(collection3);
0300 
0301     itemDelete = mStore->deleteItem(item3);
0302 
0303     QVERIFY(itemDelete->exec());
0304 
0305     Collection collection4;
0306     collection4.setName(QStringLiteral("collection4"));
0307     collection4.setRemoteId(QStringLiteral("collection4"));
0308     collection4.setParentCollection(mStore->topLevelCollection());
0309 
0310     Item item4;
0311     item4.setRemoteId(QString::number(entryList3.value(1).messageOffset()));
0312     item4.setParentCollection(collection4);
0313 
0314     itemDelete = mStore->deleteItem(item4);
0315 
0316     QVERIFY(itemDelete->exec());
0317 
0318     job = mStore->compactStore();
0319 
0320     collectionSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::collectionsChanged);
0321     itemSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::itemsChanged);
0322 
0323     QVERIFY(job->exec());
0324     QCOMPARE(job->error(), 0);
0325 
0326     collections = job->changedCollections();
0327     items = job->changedItems();
0328 
0329     QCOMPARE(collections.count(), 2);
0330     QCOMPARE(items.count(), 5);
0331 
0332     QCOMPARE(collectionSpy->count(), 2);
0333     QCOMPARE(itemSpy->count(), 2);
0334 
0335     QCOMPARE(collectionsFromSpy(collectionSpy), collections);
0336     QCOMPARE(itemsFromSpy(itemSpy), items);
0337 
0338     QHash<QString, Collection> compactedCollections;
0339     for (const Collection &col : std::as_const(collections)) {
0340         compactedCollections.insert(col.remoteId(), col);
0341     }
0342     QCOMPARE(compactedCollections.count(), 2);
0343 
0344     QVERIFY(compactedCollections.contains(collection3.remoteId()));
0345     collection = compactedCollections[collection3.remoteId()];
0346     QCOMPARE(collection, collection3);
0347     QVERIFY(collection.hasAttribute<FileStore::EntityCompactChangeAttribute>());
0348     attribute = collection.attribute<FileStore::EntityCompactChangeAttribute>();
0349     QCOMPARE(attribute->remoteRevision().toInt(), collection3.remoteRevision().toInt() + 1);
0350 
0351     QVERIFY(mbox3.load(mbox3.fileName()));
0352     entryList = mbox3.entries();
0353 
0354     // The order of items depends on the order of iteration of a QHash in MixedMaildirStore.
0355     // This makes sure that the items are always sorted by collection and offset
0356     std::sort(items.begin(), items.end(), [](const Akonadi::Item &left, const Akonadi::Item &right) {
0357         return left.parentCollection().remoteId().compare(right.parentCollection().remoteId()) < 0
0358             || (left.parentCollection().remoteId() == right.parentCollection().remoteId() && changedOffset(left) < changedOffset(right));
0359     });
0360 
0361     entryList3.pop_front();
0362     for (int i = 0; i < entryList3.count(); ++i) {
0363         entryList3[i] = MBoxEntry(changedOffset(items.first()));
0364         items.pop_front();
0365     }
0366     QCOMPARE(entryList, entryList3);
0367 
0368     QVERIFY(compactedCollections.contains(collection4.remoteId()));
0369     collection = compactedCollections[collection4.remoteId()];
0370     QCOMPARE(collection, collection4);
0371     QVERIFY(collection.hasAttribute<FileStore::EntityCompactChangeAttribute>());
0372     attribute = collection.attribute<FileStore::EntityCompactChangeAttribute>();
0373     QCOMPARE(attribute->remoteRevision().toInt(), collection4.remoteRevision().toInt() + 1);
0374 
0375     QVERIFY(mbox4.load(mbox4.fileName()));
0376     entryList = mbox4.entries();
0377 
0378     entryList4.removeAt(1);
0379     for (int i = 0; i < items.count(); ++i) {
0380         entryList4[i + 1] = MBoxEntry(changedOffset(items[i]));
0381     }
0382     QCOMPARE(entryList, entryList4);
0383 
0384     var = job->property("onDiskIndexInvalidated");
0385     QVERIFY(var.isValid());
0386     QCOMPARE(var.userType(), colListVar.userType());
0387 
0388     collections = var.value<Collection::List>();
0389     QCOMPARE((int)collections.count(), 2);
0390     QCOMPARE(collections, Collection::List() << collection3 << collection4);
0391 }
0392 
0393 QTEST_MAIN(StoreCompactTest)
0394 
0395 #include "storecompacttest.moc"