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"