File indexing completed on 2024-11-10 04:40:10

0001 /*
0002     SPDX-FileCopyrightText: 2009 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "agentinstance.h"
0008 #include "agentmanager.h"
0009 #include "collection.h"
0010 #include "collectiondeletejob.h"
0011 #include "collectionfetchjob.h"
0012 #include "collectionfetchscope.h"
0013 #include "collectionmodifyjob.h"
0014 #include "collectionsync_p.h"
0015 #include "control.h"
0016 #include "entitydisplayattribute.h"
0017 #include "qtest_akonadi.h"
0018 #include "resourceselectjob_p.h"
0019 
0020 #include <KRandom>
0021 
0022 #include <QObject>
0023 #include <QSignalSpy>
0024 
0025 using namespace Akonadi;
0026 
0027 class CollectionSyncTest : public QObject
0028 {
0029     Q_OBJECT
0030 private:
0031     Collection::List fetchCollections(const QString &res)
0032     {
0033         auto fetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this);
0034         fetch->fetchScope().setResource(res);
0035         fetch->fetchScope().setAncestorRetrieval(CollectionFetchScope::All);
0036         if (!fetch->exec()) {
0037             qWarning() << "CollectionFetchJob failed!";
0038             return Collection::List();
0039         }
0040         return fetch->collections();
0041     }
0042 
0043     void makeTestData()
0044     {
0045         QTest::addColumn<bool>("hierarchicalRIDs");
0046         QTest::addColumn<QString>("resource");
0047 
0048         QTest::newRow("akonadi_knut_resource_0 global RID") << false << "akonadi_knut_resource_0";
0049         QTest::newRow("akonadi_knut_resource_1 global RID") << false << "akonadi_knut_resource_1";
0050         QTest::newRow("akonadi_knut_resource_2 global RID") << false << "akonadi_knut_resource_2";
0051 
0052         QTest::newRow("akonadi_knut_resource_0 hierarchical RID") << true << "akonadi_knut_resource_0";
0053         QTest::newRow("akonadi_knut_resource_1 hierarchical RID") << true << "akonadi_knut_resource_1";
0054         QTest::newRow("akonadi_knut_resource_2 hierarchical RID") << true << "akonadi_knut_resource_2";
0055     }
0056 
0057     Collection createCollection(const QString &name, const QString &remoteId, const Collection &parent)
0058     {
0059         Collection c;
0060         c.setName(name);
0061         c.setRemoteId(remoteId);
0062         c.setParentCollection(parent);
0063         c.setResource(QStringLiteral("akonadi_knut_resource_0"));
0064         c.setContentMimeTypes(QStringList() << Collection::mimeType());
0065         return c;
0066     }
0067 
0068     Collection::List prepareBenchmark()
0069     {
0070         Collection::List collections = fetchCollections(QStringLiteral("akonadi_knut_resource_0"));
0071 
0072         auto resJob = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"));
0073         Q_ASSERT(resJob->exec());
0074 
0075         Collection root;
0076         for (const Collection &col : std::as_const(collections)) {
0077             if (col.parentCollection() == Collection::root()) {
0078                 root = col;
0079                 break;
0080             }
0081         }
0082         Q_ASSERT(root.isValid());
0083 
0084         // we must build on top of existing collections, because only resource is
0085         // allowed to create top-level collection
0086         Collection::List baseCollections;
0087         for (int i = 0; i < 20; ++i) {
0088             baseCollections << createCollection(QStringLiteral("Base Col %1").arg(i), QStringLiteral("/baseCol%1").arg(i), root);
0089         }
0090         collections += baseCollections;
0091 
0092         const Collection shared = createCollection(QStringLiteral("Shared collections"), QStringLiteral("/shared"), root);
0093         baseCollections << shared;
0094         collections << shared;
0095         for (int i = 0; i < 10000; ++i) {
0096             const Collection col = createCollection(QStringLiteral("Shared Col %1").arg(i), QStringLiteral("/shared%1").arg(i), shared);
0097             collections << col;
0098             for (int j = 0; j < 6; ++j) {
0099                 collections << createCollection(QStringLiteral("Shared Subcol %1-%2").arg(i).arg(j), QStringLiteral("/shared%1-%2").arg(i).arg(j), col);
0100             }
0101         }
0102         return collections;
0103     }
0104 
0105     CollectionSync *prepareBenchmarkSyncer(const Collection::List &collections)
0106     {
0107         auto syncer = new CollectionSync(QStringLiteral("akonadi_knut_resource_0"));
0108         connect(syncer, SIGNAL(percent(KJob *, ulong)), this, SLOT(syncBenchmarkProgress(KJob *, ulong)));
0109         syncer->setHierarchicalRemoteIds(false);
0110         syncer->setRemoteCollections(collections);
0111         return syncer;
0112     }
0113 
0114     void cleanupBenchmark(const Collection::List &collections)
0115     {
0116         Collection::List baseCols;
0117         for (const Collection &col : collections) {
0118             if (col.remoteId().startsWith(QLatin1StringView("/baseCol")) || col.remoteId() == QLatin1StringView("/shared")) {
0119                 baseCols << col;
0120             }
0121         }
0122         for (const Collection &col : std::as_const(baseCols)) {
0123             auto del = new CollectionDeleteJob(col);
0124             AKVERIFYEXEC(del);
0125         }
0126     }
0127 
0128 public Q_SLOTS:
0129     void syncBenchmarkProgress(KJob *job, ulong percent)
0130     {
0131         Q_UNUSED(job)
0132         qDebug() << "CollectionSync progress:" << percent << "%";
0133     }
0134 
0135 private Q_SLOTS:
0136     void initTestCase()
0137     {
0138         AkonadiTest::checkTestIsIsolated();
0139         Control::start();
0140         AkonadiTest::setAllResourcesOffline();
0141         qRegisterMetaType<KJob *>();
0142     }
0143 
0144     void testFullSync_data()
0145     {
0146         makeTestData();
0147     }
0148 
0149     void testFullSync()
0150     {
0151         QFETCH(bool, hierarchicalRIDs);
0152         QFETCH(QString, resource);
0153 
0154         Collection::List origCols = fetchCollections(resource);
0155         QVERIFY(!origCols.isEmpty());
0156 
0157         auto syncer = new CollectionSync(resource, this);
0158         syncer->setHierarchicalRemoteIds(hierarchicalRIDs);
0159         syncer->setRemoteCollections(origCols);
0160         AKVERIFYEXEC(syncer);
0161 
0162         Collection::List resultCols = fetchCollections(resource);
0163         QCOMPARE(resultCols.count(), origCols.count());
0164     }
0165 
0166     void testFullStreamingSync_data()
0167     {
0168         makeTestData();
0169     }
0170 
0171     void testFullStreamingSync()
0172     {
0173         QFETCH(bool, hierarchicalRIDs);
0174         QFETCH(QString, resource);
0175 
0176         Collection::List origCols = fetchCollections(resource);
0177         QVERIFY(!origCols.isEmpty());
0178 
0179         auto syncer = new CollectionSync(resource, this);
0180         syncer->setHierarchicalRemoteIds(hierarchicalRIDs);
0181         syncer->setAutoDelete(false);
0182         QSignalSpy spy(syncer, &KJob::result);
0183         QVERIFY(spy.isValid());
0184         syncer->setStreamingEnabled(true);
0185         QTest::qWait(10);
0186         QCOMPARE(spy.count(), 0);
0187 
0188         for (int i = 0; i < origCols.count(); ++i) {
0189             Collection::List l;
0190             l << origCols[i];
0191             syncer->setRemoteCollections(l);
0192             if (i < origCols.count() - 1) {
0193                 QTest::qWait(10); // enter the event loop so itemsync actually can do something
0194             }
0195             QCOMPARE(spy.count(), 0);
0196         }
0197         syncer->retrievalDone();
0198         QTRY_COMPARE(spy.count(), 1);
0199         QCOMPARE(spy.count(), 1);
0200         KJob *job = spy.at(0).at(0).value<KJob *>();
0201         QCOMPARE(job, syncer);
0202         QCOMPARE(job->errorText(), QString());
0203         QCOMPARE(job->error(), 0);
0204 
0205         Collection::List resultCols = fetchCollections(resource);
0206         QCOMPARE(resultCols.count(), origCols.count());
0207 
0208         delete syncer;
0209     }
0210 
0211     void testIncrementalSync_data()
0212     {
0213         makeTestData();
0214     }
0215 
0216     void testIncrementalSync()
0217     {
0218         QFETCH(bool, hierarchicalRIDs);
0219         QFETCH(QString, resource);
0220         if (resource == QLatin1StringView("akonadi_knut_resource_2")) {
0221             QSKIP("test requires more than one collection", SkipSingle);
0222         }
0223 
0224         Collection::List origCols = fetchCollections(resource);
0225         QVERIFY(!origCols.isEmpty());
0226 
0227         auto syncer = new CollectionSync(resource, this);
0228         syncer->setHierarchicalRemoteIds(hierarchicalRIDs);
0229         syncer->setRemoteCollections(origCols, Collection::List());
0230         AKVERIFYEXEC(syncer);
0231 
0232         Collection::List resultCols = fetchCollections(resource);
0233         QCOMPARE(resultCols.count(), origCols.count());
0234 
0235         // Find leaf collections that we can delete
0236         Collection::List leafCols = resultCols;
0237         for (auto iter = leafCols.begin(); iter != leafCols.end();) {
0238             bool found = false;
0239             for (const Collection &c : std::as_const(resultCols)) {
0240                 if (c.parentCollection().id() == iter->id()) {
0241                     iter = leafCols.erase(iter);
0242                     found = true;
0243                     break;
0244                 }
0245             }
0246             if (!found) {
0247                 ++iter;
0248             }
0249         }
0250         QVERIFY(!leafCols.isEmpty());
0251         Collection::List delCols;
0252         delCols << leafCols.first();
0253         resultCols.removeOne(leafCols.first());
0254 
0255         // ### not implemented yet I guess
0256 #if 0
0257         Collection colWithOnlyRemoteId;
0258         colWithOnlyRemoteId.setRemoteId(resultCols.front().remoteId());
0259         delCols << colWithOnlyRemoteId;
0260         resultCols.pop_front();
0261 #endif
0262 
0263 #if 0
0264         // ### should this work?
0265         Collection colWithRandomRemoteId;
0266         colWithRandomRemoteId.setRemoteId(KRandom::randomString(100));
0267         delCols << colWithRandomRemoteId;
0268 #endif
0269 
0270         syncer = new CollectionSync(resource, this);
0271         syncer->setRemoteCollections(resultCols, delCols);
0272         AKVERIFYEXEC(syncer);
0273 
0274         Collection::List resultCols2 = fetchCollections(resource);
0275         QCOMPARE(resultCols2.count(), resultCols.count());
0276     }
0277 
0278     void testIncrementalStreamingSync_data()
0279     {
0280         makeTestData();
0281     }
0282 
0283     void testIncrementalStreamingSync()
0284     {
0285         QFETCH(bool, hierarchicalRIDs);
0286         QFETCH(QString, resource);
0287 
0288         Collection::List origCols = fetchCollections(resource);
0289         QVERIFY(!origCols.isEmpty());
0290 
0291         auto syncer = new CollectionSync(resource, this);
0292         syncer->setHierarchicalRemoteIds(hierarchicalRIDs);
0293         syncer->setAutoDelete(false);
0294         QSignalSpy spy(syncer, &KJob::result);
0295         QVERIFY(spy.isValid());
0296         syncer->setStreamingEnabled(true);
0297         QTest::qWait(10);
0298         QCOMPARE(spy.count(), 0);
0299 
0300         for (int i = 0; i < origCols.count(); ++i) {
0301             Collection::List l;
0302             l << origCols[i];
0303             syncer->setRemoteCollections(l, Collection::List());
0304             if (i < origCols.count() - 1) {
0305                 QTest::qWait(10); // enter the event loop so itemsync actually can do something
0306             }
0307             QCOMPARE(spy.count(), 0);
0308         }
0309         syncer->retrievalDone();
0310         QTRY_COMPARE(spy.count(), 1);
0311         KJob *job = spy.at(0).at(0).value<KJob *>();
0312         QCOMPARE(job, syncer);
0313         QCOMPARE(job->errorText(), QString());
0314         QCOMPARE(job->error(), 0);
0315 
0316         Collection::List resultCols = fetchCollections(resource);
0317         QCOMPARE(resultCols.count(), origCols.count());
0318 
0319         delete syncer;
0320     }
0321 
0322     void testEmptyIncrementalSync_data()
0323     {
0324         makeTestData();
0325     }
0326 
0327     void testEmptyIncrementalSync()
0328     {
0329         QFETCH(bool, hierarchicalRIDs);
0330         QFETCH(QString, resource);
0331 
0332         Collection::List origCols = fetchCollections(resource);
0333         QVERIFY(!origCols.isEmpty());
0334 
0335         auto syncer = new CollectionSync(resource, this);
0336         syncer->setHierarchicalRemoteIds(hierarchicalRIDs);
0337         syncer->setRemoteCollections(Collection::List(), Collection::List());
0338         AKVERIFYEXEC(syncer);
0339 
0340         Collection::List resultCols = fetchCollections(resource);
0341         QCOMPARE(resultCols.count(), origCols.count());
0342     }
0343 
0344     void testAttributeChanges_data()
0345     {
0346         QTest::addColumn<bool>("keepLocalChanges");
0347         QTest::newRow("keep local changes") << true;
0348         QTest::newRow("overwrite local changes") << false;
0349     }
0350 
0351     void testAttributeChanges()
0352     {
0353         QFETCH(bool, keepLocalChanges);
0354         const QString resource(QStringLiteral("akonadi_knut_resource_0"));
0355         Collection col = fetchCollections(resource).first();
0356         col.attribute<EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing)->setDisplayName(QStringLiteral("foo"));
0357         col.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("foo"));
0358         {
0359             auto job = new CollectionModifyJob(col);
0360             AKVERIFYEXEC(job);
0361         }
0362 
0363         col.attribute<EntityDisplayAttribute>()->setDisplayName(QStringLiteral("default"));
0364         col.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("default"));
0365 
0366         auto syncer = new CollectionSync(resource, this);
0367         if (keepLocalChanges) {
0368             syncer->setKeepLocalChanges(QSet<QByteArray>() << "ENTITYDISPLAY"
0369                                                            << "CONTENTMIMETYPES");
0370         } else {
0371             syncer->setKeepLocalChanges(QSet<QByteArray>());
0372         }
0373 
0374         syncer->setRemoteCollections(Collection::List() << col, Collection::List());
0375         AKVERIFYEXEC(syncer);
0376 
0377         {
0378             auto job = new CollectionFetchJob(col, Akonadi::CollectionFetchJob::Base);
0379             AKVERIFYEXEC(job);
0380             Collection resultCol = job->collections().first();
0381             if (keepLocalChanges) {
0382                 QCOMPARE(resultCol.displayName(), QString::fromLatin1("foo"));
0383                 QVERIFY(resultCol.contentMimeTypes().contains(QLatin1StringView("foo")));
0384             } else {
0385                 QCOMPARE(resultCol.displayName(), QString::fromLatin1("default"));
0386                 QVERIFY(resultCol.contentMimeTypes().contains(QLatin1StringView("default")));
0387             }
0388         }
0389     }
0390 
0391     void testCancelation()
0392     {
0393         const QString resource(QStringLiteral("akonadi_knut_resource_0"));
0394         Collection col = fetchCollections(resource).first();
0395 
0396         auto syncer = new CollectionSync(resource, this);
0397         syncer->setStreamingEnabled(true);
0398         syncer->setRemoteCollections({col}, {});
0399 
0400         QSignalSpy spy(syncer, &CollectionSync::result);
0401         QVERIFY(spy.isValid());
0402 
0403         syncer->rollback();
0404 
0405         QTRY_VERIFY(!spy.empty());
0406         QVERIFY(syncer->error());
0407     }
0408 
0409 // Disabled by default, because they take ~15 minutes to complete
0410 #if 0
0411     void benchmarkInitialSync()
0412     {
0413         const Collection::List collections = prepareBenchmark();
0414 
0415         CollectionSync *syncer = prepareBenchmarkSyncer(collections);
0416 
0417         QBENCHMARK_ONCE {
0418             AKVERIFYEXEC(syncer);
0419         }
0420 
0421         cleanupBenchmark(collections);
0422     }
0423 
0424     void benchmarkIncrementalSync()
0425     {
0426         const Collection::List collections = prepareBenchmark();
0427 
0428         // First populate Akonadi with Collections
0429         CollectionSync *syncer = prepareBenchmarkSyncer(collections);
0430         AKVERIFYEXEC(syncer);
0431 
0432         // Now create a new syncer to benchmark the incremental sync
0433         syncer = prepareBenchmarkSyncer(collections);
0434 
0435         QBENCHMARK_ONCE {
0436             AKVERIFYEXEC(syncer);
0437         }
0438 
0439         cleanupBenchmark(collections);
0440     }
0441 #endif
0442 };
0443 
0444 QTEST_AKONADIMAIN(CollectionSyncTest)
0445 
0446 #include "collectionsynctest.moc"