File indexing completed on 2024-11-10 04:40:13
0001 /* 0002 SPDX-FileCopyrightText: 2006 Volker Krause <vkrause@kde.org> 0003 SPDX-FileCopyrightText: 2007 Robert Zwerus <arzie@dds.nl> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "itemstoretest.h" 0009 0010 #include "agentinstance.h" 0011 #include "agentmanager.h" 0012 #include "attributefactory.h" 0013 #include "collectionfetchjob.h" 0014 #include "config_p.h" 0015 #include "control.h" 0016 #include "itemcreatejob.h" 0017 #include "itemdeletejob.h" 0018 #include "itemfetchjob.h" 0019 #include "itemfetchscope.h" 0020 #include "itemmodifyjob.h" 0021 #include "itemmodifyjob_p.h" 0022 #include "qtest_akonadi.h" 0023 #include "resourceselectjob_p.h" 0024 #include "testattribute.h" 0025 0026 using namespace Akonadi; 0027 0028 QTEST_AKONADIMAIN(ItemStoreTest) 0029 0030 static Collection res1_foo; 0031 static Collection res2; 0032 static Collection res3; 0033 0034 void ItemStoreTest::initTestCase() 0035 { 0036 // The Item size tests expect the payload not to be compressed. 0037 QVERIFY(!Config::get().payloadCompression.enabled); 0038 0039 AkonadiTest::checkTestIsIsolated(); 0040 Control::start(); 0041 AttributeFactory::registerAttribute<TestAttribute>(); 0042 0043 // get the collections we run the tests on 0044 res1_foo = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); 0045 QVERIFY(res1_foo.isValid()); 0046 res2 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2"))); 0047 QVERIFY(res2.isValid()); 0048 res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); 0049 QVERIFY(res3.isValid()); 0050 0051 AkonadiTest::setAllResourcesOffline(); 0052 } 0053 0054 void ItemStoreTest::testFlagChange() 0055 { 0056 auto fjob = new ItemFetchJob(Item(1)); 0057 AKVERIFYEXEC(fjob); 0058 QCOMPARE(fjob->items().count(), 1); 0059 Item item = fjob->items()[0]; 0060 0061 // add a flag 0062 Item::Flags origFlags = item.flags(); 0063 Item::Flags expectedFlags = origFlags; 0064 expectedFlags.insert("added_test_flag_1"); 0065 item.setFlag("added_test_flag_1"); 0066 auto sjob = new ItemModifyJob(item, this); 0067 AKVERIFYEXEC(sjob); 0068 0069 fjob = new ItemFetchJob(Item(1)); 0070 AKVERIFYEXEC(fjob); 0071 QCOMPARE(fjob->items().count(), 1); 0072 item = fjob->items()[0]; 0073 QCOMPARE(item.flags().count(), expectedFlags.count()); 0074 Item::Flags diff = expectedFlags - item.flags(); 0075 QVERIFY(diff.isEmpty()); 0076 0077 // set flags 0078 expectedFlags.insert("added_test_flag_2"); 0079 item.setFlags(expectedFlags); 0080 sjob = new ItemModifyJob(item, this); 0081 AKVERIFYEXEC(sjob); 0082 0083 fjob = new ItemFetchJob(Item(1)); 0084 AKVERIFYEXEC(fjob); 0085 QCOMPARE(fjob->items().count(), 1); 0086 item = fjob->items()[0]; 0087 QCOMPARE(item.flags().count(), expectedFlags.count()); 0088 diff = expectedFlags - item.flags(); 0089 QVERIFY(diff.isEmpty()); 0090 0091 // remove a flag 0092 item.clearFlag("added_test_flag_1"); 0093 item.clearFlag("added_test_flag_2"); 0094 sjob = new ItemModifyJob(item, this); 0095 AKVERIFYEXEC(sjob); 0096 0097 fjob = new ItemFetchJob(Item(1)); 0098 AKVERIFYEXEC(fjob); 0099 QCOMPARE(fjob->items().count(), 1); 0100 item = fjob->items()[0]; 0101 QCOMPARE(item.flags().count(), origFlags.count()); 0102 diff = origFlags - item.flags(); 0103 QVERIFY(diff.isEmpty()); 0104 } 0105 0106 void ItemStoreTest::testDataChange_data() 0107 { 0108 QTest::addColumn<QByteArray>("data"); 0109 QTest::addColumn<qint64>("expectedSize"); 0110 0111 QTest::newRow("simple") << QByteArray("testbody") << 8LL; 0112 QTest::newRow("null") << QByteArray() << 0LL; 0113 QTest::newRow("empty") << QByteArray("") << 0LL; 0114 QTest::newRow("nullbyte") << QByteArray("\0", 1) << 1LL; 0115 QTest::newRow("nullbyte2") << QByteArray("\0X", 2) << 2LL; 0116 QTest::newRow("linebreaks") << QByteArray("line1\nline2\n\rline3\rline4\r\n") << 26LL; 0117 QTest::newRow("linebreaks2") << QByteArray("line1\r\nline2\r\n\r\n") << 16LL; 0118 QTest::newRow("linebreaks3") << QByteArray("line1\nline2") << 11LL; 0119 QByteArray b; 0120 QTest::newRow("big") << b.fill('a', 1 << 20) << (1LL << 20); 0121 QTest::newRow("bignull") << b.fill('\0', 1 << 20) << (1LL << 20); 0122 QTest::newRow("bigcr") << b.fill('\r', 1 << 20) << (1LL << 20); 0123 QTest::newRow("biglf") << b.fill('\n', 1 << 20) << (1LL << 20); 0124 } 0125 0126 void ItemStoreTest::testDataChange() 0127 { 0128 QFETCH(QByteArray, data); 0129 QFETCH(qint64, expectedSize); 0130 0131 Item item; 0132 auto prefetchjob = new ItemFetchJob(Item(1)); 0133 AKVERIFYEXEC(prefetchjob); 0134 item = prefetchjob->items()[0]; 0135 item.setMimeType(QStringLiteral("application/octet-stream")); 0136 item.setPayload(data); 0137 QCOMPARE(item.payload<QByteArray>(), data); 0138 0139 // modify data 0140 auto sjob = new ItemModifyJob(item); 0141 AKVERIFYEXEC(sjob); 0142 0143 auto fjob = new ItemFetchJob(Item(1)); 0144 fjob->fetchScope().fetchFullPayload(); 0145 fjob->fetchScope().setCacheOnly(true); 0146 AKVERIFYEXEC(fjob); 0147 QCOMPARE(fjob->items().count(), 1); 0148 item = fjob->items()[0]; 0149 QVERIFY(item.hasPayload<QByteArray>()); 0150 QCOMPARE(item.payload<QByteArray>(), data); 0151 QEXPECT_FAIL("null", "STORE will not update item size on 0 sizes", Continue); 0152 QEXPECT_FAIL("empty", "STORE will not update item size on 0 sizes", Continue); 0153 // Cannot compare with data.size() due to payload compression 0154 QCOMPARE(item.size(), expectedSize); 0155 } 0156 0157 void ItemStoreTest::testRemoteId_data() 0158 { 0159 QTest::addColumn<QString>("rid"); 0160 QTest::addColumn<QString>("exprid"); 0161 0162 QTest::newRow("set") << QStringLiteral("A") << QStringLiteral("A"); 0163 QTest::newRow("no-change") << QString() << QStringLiteral("A"); 0164 QTest::newRow("clear") << QStringLiteral("") << QStringLiteral(""); 0165 QTest::newRow("reset") << QStringLiteral("A") << QStringLiteral("A"); 0166 QTest::newRow("utf8") << QStringLiteral("ä ö ü @") << QStringLiteral("ä ö ü @"); 0167 } 0168 0169 void ItemStoreTest::testRemoteId() 0170 { 0171 QFETCH(QString, rid); 0172 QFETCH(QString, exprid); 0173 0174 // pretend to be a resource, we cannot change remote identifiers otherwise 0175 auto rsel = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this); 0176 AKVERIFYEXEC(rsel); 0177 0178 auto prefetchjob = new ItemFetchJob(Item(1)); 0179 AKVERIFYEXEC(prefetchjob); 0180 Item item = prefetchjob->items()[0]; 0181 0182 item.setRemoteId(rid); 0183 auto store = new ItemModifyJob(item, this); 0184 store->disableRevisionCheck(); 0185 store->setIgnorePayload(true); // we only want to update the remote id 0186 AKVERIFYEXEC(store); 0187 0188 auto fetch = new ItemFetchJob(item, this); 0189 AKVERIFYEXEC(fetch); 0190 QCOMPARE(fetch->items().count(), 1); 0191 item = fetch->items().at(0); 0192 QEXPECT_FAIL("clear", "Clearing RID by clients is currently forbidden to avoid conflicts.", Continue); 0193 QCOMPARE(item.remoteId().toUtf8(), exprid.toUtf8()); 0194 0195 // no longer pretend to be a resource 0196 rsel = new ResourceSelectJob(QString(), this); 0197 AKVERIFYEXEC(rsel); 0198 } 0199 0200 void ItemStoreTest::testMultiPart() 0201 { 0202 auto prefetchjob = new ItemFetchJob(Item(1)); 0203 AKVERIFYEXEC(prefetchjob); 0204 QCOMPARE(prefetchjob->items().count(), 1); 0205 Item item = prefetchjob->items()[0]; 0206 item.setMimeType(QStringLiteral("application/octet-stream")); 0207 item.setPayload<QByteArray>("testmailbody"); 0208 item.attribute<TestAttribute>(Item::AddIfMissing)->data = "extra"; 0209 0210 // store item 0211 auto sjob = new ItemModifyJob(item); 0212 AKVERIFYEXEC(sjob); 0213 0214 auto fjob = new ItemFetchJob(Item(1)); 0215 fjob->fetchScope().fetchAttribute<TestAttribute>(); 0216 fjob->fetchScope().fetchFullPayload(); 0217 AKVERIFYEXEC(fjob); 0218 QCOMPARE(fjob->items().count(), 1); 0219 item = fjob->items()[0]; 0220 QVERIFY(item.hasPayload<QByteArray>()); 0221 QCOMPARE(item.payload<QByteArray>(), QByteArray("testmailbody")); 0222 QVERIFY(item.hasAttribute<TestAttribute>()); 0223 QCOMPARE(item.attribute<TestAttribute>()->data, QByteArray("extra")); 0224 0225 // clean up 0226 item.removeAttribute("EXTRA"); 0227 sjob = new ItemModifyJob(item); 0228 AKVERIFYEXEC(sjob); 0229 } 0230 0231 void ItemStoreTest::testPartRemove() 0232 { 0233 auto prefetchjob = new ItemFetchJob(Item(2)); 0234 AKVERIFYEXEC(prefetchjob); 0235 Item item = prefetchjob->items()[0]; 0236 item.setMimeType(QStringLiteral("application/octet-stream")); 0237 item.attribute<TestAttribute>(Item::AddIfMissing)->data = "extra"; 0238 0239 // store item 0240 auto sjob = new ItemModifyJob(item); 0241 AKVERIFYEXEC(sjob); 0242 0243 // fetch item and its parts (should be RFC822, HEAD and EXTRA) 0244 auto fjob = new ItemFetchJob(Item(2)); 0245 fjob->fetchScope().fetchFullPayload(); 0246 fjob->fetchScope().fetchAllAttributes(); 0247 fjob->fetchScope().setCacheOnly(true); 0248 AKVERIFYEXEC(fjob); 0249 QCOMPARE(fjob->items().count(), 1); 0250 item = fjob->items()[0]; 0251 QCOMPARE(item.attributes().count(), 2); 0252 QVERIFY(item.hasAttribute<TestAttribute>()); 0253 0254 // remove a part 0255 item.removeAttribute<TestAttribute>(); 0256 sjob = new ItemModifyJob(item); 0257 AKVERIFYEXEC(sjob); 0258 0259 // fetch item again (should only have RFC822 and HEAD left) 0260 auto fjob2 = new ItemFetchJob(Item(2)); 0261 fjob2->fetchScope().fetchFullPayload(); 0262 fjob2->fetchScope().fetchAllAttributes(); 0263 fjob2->fetchScope().setCacheOnly(true); 0264 AKVERIFYEXEC(fjob2); 0265 QCOMPARE(fjob2->items().count(), 1); 0266 item = fjob2->items()[0]; 0267 QCOMPARE(item.attributes().count(), 1); 0268 QVERIFY(!item.hasAttribute<TestAttribute>()); 0269 } 0270 0271 void ItemStoreTest::testRevisionCheck() 0272 { 0273 // fetch same item twice 0274 Item ref(2); 0275 auto prefetchjob = new ItemFetchJob(ref); 0276 AKVERIFYEXEC(prefetchjob); 0277 QCOMPARE(prefetchjob->items().count(), 1); 0278 Item item1 = prefetchjob->items()[0]; 0279 Item item2 = prefetchjob->items()[0]; 0280 0281 // store first item unmodified 0282 auto sjob = new ItemModifyJob(item1); 0283 AKVERIFYEXEC(sjob); 0284 0285 // store the first item with modifications (should work) 0286 item1.attribute<TestAttribute>(Item::AddIfMissing)->data = "random stuff 1"; 0287 sjob = new ItemModifyJob(item1, this); 0288 AKVERIFYEXEC(sjob); 0289 0290 // try to store second item with modifications (should be detected as a conflict) 0291 item2.attribute<TestAttribute>(Item::AddIfMissing)->data = "random stuff 2"; 0292 auto sjob2 = new ItemModifyJob(item2); 0293 sjob2->disableAutomaticConflictHandling(); 0294 QVERIFY(!sjob2->exec()); 0295 0296 // fetch same again 0297 prefetchjob = new ItemFetchJob(ref); 0298 AKVERIFYEXEC(prefetchjob); 0299 item1 = prefetchjob->items()[0]; 0300 0301 // delete item 0302 auto djob = new ItemDeleteJob(ref, this); 0303 AKVERIFYEXEC(djob); 0304 0305 // try to store it 0306 sjob = new ItemModifyJob(item1); 0307 QVERIFY(!sjob->exec()); 0308 } 0309 0310 void ItemStoreTest::testModificationTime() 0311 { 0312 Item item; 0313 item.setMimeType(QStringLiteral("text/directory")); 0314 QVERIFY(item.modificationTime().isNull()); 0315 0316 auto job = new ItemCreateJob(item, res1_foo); 0317 AKVERIFYEXEC(job); 0318 0319 // The item should have a datetime set now. 0320 item = job->item(); 0321 QVERIFY(!item.modificationTime().isNull()); 0322 QDateTime initialDateTime = item.modificationTime(); 0323 0324 // Fetch the same item again. 0325 Item item2(item.id()); 0326 auto fjob = new ItemFetchJob(item2, this); 0327 AKVERIFYEXEC(fjob); 0328 item2 = fjob->items().first(); 0329 QCOMPARE(initialDateTime, item2.modificationTime()); 0330 0331 // Lets wait at least a second, which is the resolution of mtime 0332 QTest::qWait(1000); 0333 0334 // Modify the item 0335 item.attribute<TestAttribute>(Item::AddIfMissing)->data = "extra"; 0336 auto mjob = new ItemModifyJob(item); 0337 AKVERIFYEXEC(mjob); 0338 0339 // The item should still have a datetime set and that date should be somewhere 0340 // after the initialDateTime. 0341 item = mjob->item(); 0342 QVERIFY(!item.modificationTime().isNull()); 0343 QVERIFY(initialDateTime < item.modificationTime()); 0344 0345 // Fetch the item after modification. 0346 Item item3(item.id()); 0347 auto fjob2 = new ItemFetchJob(item3, this); 0348 AKVERIFYEXEC(fjob2); 0349 0350 // item3 should have the same modification time as item. 0351 item3 = fjob2->items().first(); 0352 QCOMPARE(item3.modificationTime(), item.modificationTime()); 0353 0354 // Clean up 0355 auto idjob = new ItemDeleteJob(item, this); 0356 AKVERIFYEXEC(idjob); 0357 } 0358 0359 void ItemStoreTest::testRemoteIdRace() 0360 { 0361 // Create an item and store it 0362 Item item; 0363 item.setMimeType(QStringLiteral("text/directory")); 0364 auto job = new ItemCreateJob(item, res1_foo); 0365 AKVERIFYEXEC(job); 0366 0367 // Fetch the same item again. It should not have a remote Id yet, as the resource 0368 // is offline. 0369 // The remote id should be null, not only empty, so that item modify jobs with this 0370 // item don't overwrite the remote id. 0371 Item item2(job->item().id()); 0372 auto fetchJob = new ItemFetchJob(item2); 0373 AKVERIFYEXEC(fetchJob); 0374 QCOMPARE(fetchJob->items().size(), 1); 0375 QVERIFY(fetchJob->items().first().remoteId().isEmpty()); 0376 } 0377 0378 void ItemStoreTest::itemModifyJobShouldOnlySendModifiedAttributes() 0379 { 0380 // Given an item with an attribute (created on the server) 0381 Item item; 0382 item.setMimeType(QStringLiteral("text/directory")); 0383 item.attribute<TestAttribute>(Item::AddIfMissing)->data = "initial"; 0384 auto job = new ItemCreateJob(item, res1_foo); 0385 AKVERIFYEXEC(job); 0386 item = job->item(); 0387 QCOMPARE(item.attributes().count(), 1); 0388 0389 // When one job modifies this attribute, and another one does an unrelated change 0390 Item item1(item.id()); 0391 item1.attribute<TestAttribute>(Item::AddIfMissing)->data = "modified"; 0392 auto mjob = new ItemModifyJob(item1); 0393 mjob->disableRevisionCheck(); 0394 AKVERIFYEXEC(mjob); 0395 0396 item.setFlag("added_test_flag_1"); 0397 // this job shouldn't send the old attribute again 0398 auto mjob2 = new ItemModifyJob(item); 0399 mjob2->disableRevisionCheck(); 0400 AKVERIFYEXEC(mjob2); 0401 0402 // Then the item has the new value for the attribute (the other one didn't send the old attribute value) 0403 { 0404 auto fetchJob = new ItemFetchJob(Item(item.id())); 0405 ItemFetchScope fetchScope; 0406 fetchScope.fetchAllAttributes(true); 0407 fetchJob->setFetchScope(fetchScope); 0408 AKVERIFYEXEC(fetchJob); 0409 QCOMPARE(fetchJob->items().size(), 1); 0410 const Item fetchedItem = fetchJob->items().first(); 0411 QCOMPARE(fetchedItem.flags().count(), 1); 0412 QCOMPARE(fetchedItem.attributes().count(), 1); 0413 QCOMPARE(fetchedItem.attribute<TestAttribute>()->data, "modified"); 0414 } 0415 } 0416 0417 class ParallelJobsRunner 0418 { 0419 public: 0420 explicit ParallelJobsRunner(int count) 0421 : numSessions(count) 0422 { 0423 sessions.reserve(numSessions); 0424 modifyJobs.reserve(numSessions); 0425 for (int i = 0; i < numSessions; ++i) { 0426 auto session = new Session(QByteArray::number(i)); 0427 sessions.push_back(session); 0428 } 0429 } 0430 0431 ~ParallelJobsRunner() 0432 { 0433 qDeleteAll(sessions); 0434 } 0435 0436 void addJob(ItemModifyJob *mjob) 0437 { 0438 modifyJobs.push_back(mjob); 0439 QObject::connect(mjob, &KJob::result, mjob, [mjob, this]() { 0440 if (mjob->error()) { 0441 errors.append(mjob->errorString()); 0442 } 0443 doneJobs.push_back(mjob); 0444 }); 0445 } 0446 0447 void waitForAllJobs() 0448 { 0449 for (int i = 0; i < modifyJobs.count(); ++i) { 0450 ItemModifyJob *mjob = modifyJobs.at(i); 0451 if (!doneJobs.contains(mjob)) { 0452 QSignalSpy spy(mjob, &ItemModifyJob::result); 0453 QVERIFY(spy.wait()); 0454 if (mjob->error()) { 0455 qWarning() << mjob->errorString(); 0456 } 0457 QCOMPARE(mjob->error(), KJob::NoError); 0458 } 0459 } 0460 QVERIFY2(errors.isEmpty(), qPrintable(errors.join(QLatin1StringView("; ")))); 0461 } 0462 0463 const int numSessions; 0464 std::vector<Session *> sessions; 0465 QList<ItemModifyJob *> modifyJobs, doneJobs; 0466 QStringList errors; 0467 }; 0468 0469 void ItemStoreTest::testParallelJobsAddingAttributes() 0470 { 0471 // Given an item (created on the server) 0472 Item::Id itemId; 0473 { 0474 Item item; 0475 item.setMimeType(QStringLiteral("text/directory")); 0476 auto job = new ItemCreateJob(item, res1_foo); 0477 AKVERIFYEXEC(job); 0478 itemId = job->item().id(); 0479 QVERIFY(itemId >= 0); 0480 } 0481 0482 // When adding N attributes from N different sessions (e.g. threads or processes) 0483 ParallelJobsRunner runner(10); 0484 for (int i = 0; i < runner.numSessions; ++i) { 0485 Item item(itemId); 0486 Attribute *attr = AttributeFactory::createAttribute("type" + QByteArray::number(i)); 0487 QVERIFY(attr); 0488 attr->deserialize("attr" + QByteArray::number(i)); 0489 item.addAttribute(attr); 0490 auto mjob = new ItemModifyJob(item, runner.sessions.at(i)); 0491 runner.addJob(mjob); 0492 } 0493 runner.waitForAllJobs(); 0494 0495 // Then the item should have all attributes 0496 auto fetchJob = new ItemFetchJob(Item(itemId)); 0497 ItemFetchScope fetchScope; 0498 fetchScope.fetchAllAttributes(true); 0499 fetchJob->setFetchScope(fetchScope); 0500 AKVERIFYEXEC(fetchJob); 0501 QCOMPARE(fetchJob->items().size(), 1); 0502 const Item fetchedItem = fetchJob->items().first(); 0503 QCOMPARE(fetchedItem.attributes().count(), runner.numSessions); 0504 } 0505 0506 #include "moc_itemstoretest.cpp"