File indexing completed on 2025-01-05 04:59:52
0001 /* 0002 * SPDX-FileCopyrightText: 2015 Kevin Ottens <ervin@kde.org> 0003 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 0007 #include <testlib/qtest_zanshin.h> 0008 0009 #include <KCalendarCore/Todo> 0010 0011 #include "akonadi/akonadicollectionfetchjobinterface.h" 0012 #include "akonadi/akonadiitemfetchjobinterface.h" 0013 0014 #include "akonadi/akonadilivequeryintegrator.h" 0015 #include "akonadi/akonadiserializer.h" 0016 #include "akonadi/akonadistorage.h" 0017 0018 #include "testlib/akonadifakedata.h" 0019 #include "testlib/gencollection.h" 0020 #include "testlib/gentodo.h" 0021 #include "testlib/testhelpers.h" 0022 0023 #include "utils/jobhandler.h" 0024 0025 using namespace Testlib; 0026 0027 static QString titleFromItem(const Akonadi::Item &item) 0028 { 0029 if (item.hasPayload<KCalendarCore::Todo::Ptr>()) { 0030 const auto todo = item.payload<KCalendarCore::Todo::Ptr>(); 0031 return todo->summary(); 0032 } else { 0033 return QString(); 0034 } 0035 } 0036 0037 class AkonadiLiveQueryIntegratorTest : public QObject 0038 { 0039 Q_OBJECT 0040 0041 private: 0042 Akonadi::LiveQueryIntegrator::Ptr createIntegrator(AkonadiFakeData &data) 0043 { 0044 return Akonadi::LiveQueryIntegrator::Ptr( 0045 new Akonadi::LiveQueryIntegrator(createSerializer(), 0046 Akonadi::MonitorInterface::Ptr(data.createMonitor()) 0047 ) 0048 ); 0049 } 0050 0051 Akonadi::StorageInterface::Ptr createStorage(AkonadiFakeData &data) 0052 { 0053 return Akonadi::StorageInterface::Ptr(data.createStorage()); 0054 } 0055 0056 Akonadi::SerializerInterface::Ptr createSerializer() 0057 { 0058 return Akonadi::SerializerInterface::Ptr(new Akonadi::Serializer); 0059 } 0060 0061 auto fetchCollectionsFunction(Akonadi::StorageInterface::Ptr storage, QObject *parent) { 0062 return [storage, parent] (const Domain::LiveQueryInput<Akonadi::Collection>::AddFunction &add) { 0063 auto job = storage->fetchCollections(Akonadi::Collection::root(), Akonadi::Storage::Recursive, parent); 0064 Utils::JobHandler::install(job->kjob(), [add, job] { 0065 foreach (const auto &col, job->collections()) { 0066 add(col); 0067 } 0068 }); 0069 }; 0070 } 0071 0072 auto fetchItemsInAllCollectionsFunction(Akonadi::StorageInterface::Ptr storage, QObject *parent) { 0073 return [storage, parent] (const Domain::LiveQueryInput<Akonadi::Item>::AddFunction &add) { 0074 auto job = storage->fetchCollections(Akonadi::Collection::root(), Akonadi::Storage::Recursive, parent); 0075 Utils::JobHandler::install(job->kjob(), [add, job, storage] { 0076 foreach (const auto &col, job->collections()) { 0077 auto itemJob = storage->fetchItems(col, nullptr); 0078 Utils::JobHandler::install(itemJob->kjob(), [add, itemJob] { 0079 foreach (const auto &item, itemJob->items()) 0080 add(item); 0081 }); 0082 } 0083 }); 0084 }; 0085 } 0086 0087 auto fetchItemsInSelectedCollectionsFunction(Akonadi::StorageInterface::Ptr storage, Akonadi::SerializerInterface::Ptr serializer, QObject *parent) 0088 { 0089 return [storage, serializer, parent] (const Domain::LiveQueryInput<Akonadi::Item>::AddFunction &add) { 0090 auto job = storage->fetchCollections(Akonadi::Collection::root(), Akonadi::Storage::Recursive, parent); 0091 Utils::JobHandler::install(job->kjob(), [add, job, storage, serializer] { 0092 foreach (const auto &col, job->collections()) { 0093 if (!serializer->isSelectedCollection(col)) 0094 continue; 0095 0096 auto itemJob = storage->fetchItems(col, nullptr); 0097 Utils::JobHandler::install(itemJob->kjob(), [add, itemJob] { 0098 foreach (const auto &item, itemJob->items()) 0099 add(item); 0100 }); 0101 } 0102 }); 0103 }; 0104 } 0105 0106 private slots: 0107 void shouldBindContextQueries() 0108 { 0109 // GIVEN 0110 AkonadiFakeData data; 0111 0112 // One toplevel collection 0113 const auto collection = Akonadi::Collection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("folder")).withTaskContent()); 0114 data.createCollection(collection); 0115 0116 // Three context todos, one not matching the predicate 0117 data.createItem(GenTodo().withParent(42).withId(42).withUid("ctx-42").asContext().withTitle(QStringLiteral("42-in"))); 0118 data.createItem(GenTodo().withParent(42).withId(43).withUid("ctx-43").asContext().withTitle(QStringLiteral("43-in"))); 0119 data.createItem(GenTodo().withParent(42).withId(44).withUid("ctx-44").asContext().withTitle(QStringLiteral("44-ex"))); 0120 0121 auto integrator = createIntegrator(data); 0122 auto storage = createStorage(data); 0123 0124 auto query = Domain::LiveQueryOutput<Domain::Context::Ptr>::Ptr(); 0125 auto fetch = [storage, collection] (const Domain::LiveQueryInput<Akonadi::Item>::AddFunction &add) { 0126 auto job = storage->fetchItems(collection, nullptr); 0127 Utils::JobHandler::install(job->kjob(), [add, job] { 0128 foreach (const auto &item, job->items()) { 0129 add(item); 0130 } 0131 }); 0132 }; 0133 auto predicate = [] (const Akonadi::Item &contextItem) { 0134 auto todo = contextItem.payload<KCalendarCore::Todo::Ptr>(); 0135 return todo->summary().endsWith(QLatin1StringView("-in")); 0136 }; 0137 0138 // Initial listing 0139 // WHEN 0140 integrator->bind("context1", query, fetch, predicate); 0141 auto result = query->result(); 0142 result->data(); 0143 integrator->bind("context2", query, fetch, predicate); 0144 result = query->result(); // Should not cause any problem or wrong data 0145 0146 // THEN 0147 QVERIFY(result->data().isEmpty()); 0148 TestHelpers::waitForEmptyJobQueue(); 0149 0150 QCOMPARE(result->data().size(), 2); 0151 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0152 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0153 0154 // Reacts to add 0155 // WHEN 0156 data.createItem(GenTodo().withId(45).withUid("ctx-45").asContext().withTitle(QStringLiteral("45-in"))); 0157 0158 // THEN 0159 QCOMPARE(result->data().size(), 3); 0160 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0161 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0162 QCOMPARE(result->data().at(2)->name(), QStringLiteral("45-in")); 0163 0164 // Reacts to remove 0165 // WHEN 0166 data.removeItem(Akonadi::Item(45)); 0167 0168 // THEN 0169 QCOMPARE(result->data().size(), 2); 0170 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0171 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0172 0173 // Reacts to change 0174 // WHEN 0175 data.modifyItem(GenTodo(data.item(42)).withTitle(QStringLiteral("42-bis-in"))); 0176 0177 // THEN 0178 QCOMPARE(result->data().size(), 2); 0179 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0180 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0181 0182 // Reacts to change (which adds) 0183 // WHEN 0184 data.modifyItem(GenTodo(data.item(44)).withTitle(QStringLiteral("44-in"))); 0185 0186 // THEN 0187 QCOMPARE(result->data().size(), 3); 0188 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0189 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0190 QCOMPARE(result->data().at(2)->name(), QStringLiteral("44-in")); 0191 0192 // Reacts to change (which removes) 0193 // WHEN 0194 data.modifyItem(GenTodo(data.item(44)).withTitle(QStringLiteral("44-ex"))); 0195 0196 // THEN 0197 QCOMPARE(result->data().size(), 2); 0198 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0199 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0200 0201 // Don't keep a reference on any result 0202 result.clear(); 0203 0204 // The bug we're trying to hit here is the following: 0205 // - when bind is called the first time a provider is created internally 0206 // - result is deleted at the end of the loop, no one holds the provider with 0207 // a strong reference anymore so it is deleted as well 0208 // - when bind is called the second time, there's a risk of a dangling 0209 // pointer if the recycling of providers is wrongly implemented which can lead 0210 // to a crash, if it is properly done no crash will occur 0211 for (int i = 0; i < 2; i++) { 0212 // WHEN * 2 0213 integrator->bind("contextN", query, fetch, predicate); 0214 auto result = query->result(); 0215 0216 // THEN * 2 0217 QVERIFY(result->data().isEmpty()); 0218 TestHelpers::waitForEmptyJobQueue(); 0219 QVERIFY(!result->data().isEmpty()); 0220 } 0221 } 0222 0223 void shouldMoveContextBetweenQueries() 0224 { 0225 // GIVEN 0226 AkonadiFakeData data; 0227 0228 // One toplevel collection 0229 const auto collection = Akonadi::Collection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("folder")).withTaskContent()); 0230 data.createCollection(collection); 0231 0232 // One context which shows in one query not the other 0233 data.createItem(GenTodo().withParent(42).withId(42).withUid("ctx-42").asContext().withTitle(QStringLiteral("42-in"))); 0234 0235 auto integrator = createIntegrator(data); 0236 auto storage = createStorage(data); 0237 0238 auto inQuery = Domain::LiveQueryOutput<Domain::Context::Ptr>::Ptr(); 0239 auto exQuery = Domain::LiveQueryOutput<Domain::Context::Ptr>::Ptr(); 0240 auto fetch = [storage, collection] (const Domain::LiveQueryInput<Akonadi::Item>::AddFunction &add) { 0241 auto job = storage->fetchItems(collection, nullptr); 0242 Utils::JobHandler::install(job->kjob(), [add, job] { 0243 foreach (const auto &item, job->items()) { 0244 add(item); 0245 } 0246 }); 0247 }; 0248 auto inPredicate = [] (const Akonadi::Item &contextItem) { 0249 auto todo = contextItem.payload<KCalendarCore::Todo::Ptr>(); 0250 return todo->summary().endsWith(QLatin1StringView("-in")); 0251 }; 0252 auto exPredicate = [] (const Akonadi::Item &contextItem) { 0253 auto todo = contextItem.payload<KCalendarCore::Todo::Ptr>(); 0254 return todo->summary().endsWith(QLatin1StringView("-ex")); 0255 }; 0256 0257 integrator->bind("context-in", inQuery, fetch, inPredicate); 0258 auto inResult = inQuery->result(); 0259 0260 integrator->bind("context-ex", exQuery, fetch, exPredicate); 0261 auto exResult = exQuery->result(); 0262 0263 TestHelpers::waitForEmptyJobQueue(); 0264 0265 QCOMPARE(inResult->data().size(), 1); 0266 QCOMPARE(exResult->data().size(), 0); 0267 0268 // WHEN 0269 data.modifyItem(GenTodo(data.item(42)).withTitle(QStringLiteral("42-ex"))); 0270 0271 // THEN 0272 QCOMPARE(inResult->data().size(), 0); 0273 QCOMPARE(exResult->data().size(), 1); 0274 } 0275 0276 0277 0278 0279 void shouldBindDataSourceQueries() 0280 { 0281 // GIVEN 0282 AkonadiFakeData data; 0283 0284 // Three top level collections, one not matching the predicate 0285 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42-in")).withTaskContent()); 0286 data.createCollection(GenCollection().withId(43).withRootAsParent().withName(QStringLiteral("43-in")).withTaskContent()); 0287 data.createCollection(GenCollection().withId(44).withRootAsParent().withName(QStringLiteral("44-ex")).withTaskContent()); 0288 0289 auto integrator = createIntegrator(data); 0290 auto storage = createStorage(data); 0291 0292 auto query = Domain::LiveQueryOutput<Domain::DataSource::Ptr>::Ptr(); 0293 auto fetch = fetchCollectionsFunction(storage, nullptr); 0294 auto predicate = [] (const Akonadi::Collection &collection) { 0295 return collection.name().endsWith(QLatin1StringView("-in")); 0296 }; 0297 0298 // Initial listing 0299 // WHEN 0300 integrator->bind("ds1", query, fetch, predicate); 0301 auto result = query->result(); 0302 result->data(); 0303 integrator->bind("ds2", query, fetch, predicate); 0304 result = query->result(); // Should not cause any problem or wrong data 0305 0306 // THEN 0307 QVERIFY(result->data().isEmpty()); 0308 TestHelpers::waitForEmptyJobQueue(); 0309 0310 QCOMPARE(result->data().size(), 2); 0311 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0312 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0313 0314 // Reacts to add 0315 // WHEN 0316 data.createCollection(GenCollection().withId(45).withRootAsParent().withName(QStringLiteral("45-in"))); 0317 0318 // THEN 0319 QCOMPARE(result->data().size(), 3); 0320 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0321 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0322 QCOMPARE(result->data().at(2)->name(), QStringLiteral("45-in")); 0323 0324 // Reacts to remove 0325 // WHEN 0326 data.removeCollection(Akonadi::Collection(45)); 0327 0328 // THEN 0329 QCOMPARE(result->data().size(), 2); 0330 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0331 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0332 0333 // Reacts to change 0334 // WHEN 0335 data.modifyCollection(GenCollection(data.collection(42)).withName(QStringLiteral("42-bis-in"))); 0336 0337 // THEN 0338 QCOMPARE(result->data().size(), 2); 0339 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0340 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0341 0342 // Reacts to change (which adds) 0343 // WHEN 0344 data.modifyCollection(GenCollection(data.collection(44)).withName(QStringLiteral("44-in"))); 0345 0346 // THEN 0347 QCOMPARE(result->data().size(), 3); 0348 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0349 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0350 QCOMPARE(result->data().at(2)->name(), QStringLiteral("44-in")); 0351 0352 // Reacts to change (which removes) 0353 // WHEN 0354 data.modifyCollection(GenCollection(data.collection(44)).withName(QStringLiteral("44-ex"))); 0355 0356 // THEN 0357 QCOMPARE(result->data().size(), 2); 0358 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0359 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0360 0361 // Don't keep a reference on any result 0362 result.clear(); 0363 0364 // The bug we're trying to hit here is the following: 0365 // - when bind is called the first time a provider is created internally 0366 // - result is deleted at the end of the loop, no one holds the provider with 0367 // a strong reference anymore so it is deleted as well 0368 // - when bind is called the second time, there's a risk of a dangling 0369 // pointer if the recycling of providers is wrongly implemented which can lead 0370 // to a crash, if it is properly done no crash will occur 0371 for (int i = 0; i < 2; i++) { 0372 // WHEN * 2 0373 integrator->bind("dsN", query, fetch, predicate); 0374 auto result = query->result(); 0375 0376 // THEN * 2 0377 QVERIFY(result->data().isEmpty()); 0378 TestHelpers::waitForEmptyJobQueue(); 0379 QVERIFY(!result->data().isEmpty()); 0380 } 0381 } 0382 0383 void shouldMoveDataSourceBetweenQueries() 0384 { 0385 // GIVEN 0386 AkonadiFakeData data; 0387 0388 // One top level collection which shows in one query not the other 0389 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42-in")).withTaskContent()); 0390 0391 auto integrator = createIntegrator(data); 0392 auto storage = createStorage(data); 0393 0394 auto inQuery = Domain::LiveQueryOutput<Domain::DataSource::Ptr>::Ptr(); 0395 auto exQuery = Domain::LiveQueryOutput<Domain::DataSource::Ptr>::Ptr(); 0396 auto fetch = fetchCollectionsFunction(storage, nullptr); 0397 auto inPredicate = [] (const Akonadi::Collection &collection) { 0398 return collection.name().endsWith(QLatin1StringView("-in")); 0399 }; 0400 auto exPredicate = [] (const Akonadi::Collection &collection) { 0401 return collection.name().endsWith(QLatin1StringView("-ex")); 0402 }; 0403 0404 integrator->bind("ds-in", inQuery, fetch, inPredicate); 0405 auto inResult = inQuery->result(); 0406 0407 integrator->bind("ds-ex", exQuery, fetch, exPredicate); 0408 auto exResult = exQuery->result(); 0409 0410 TestHelpers::waitForEmptyJobQueue(); 0411 0412 QCOMPARE(inResult->data().size(), 1); 0413 QCOMPARE(exResult->data().size(), 0); 0414 0415 // WHEN 0416 data.modifyCollection(GenCollection(data.collection(42)).withName(QStringLiteral("42-ex"))); 0417 0418 // THEN 0419 QCOMPARE(inResult->data().size(), 0); 0420 QCOMPARE(exResult->data().size(), 1); 0421 } 0422 0423 0424 0425 0426 void shouldBindProjectQueries() 0427 { 0428 // GIVEN 0429 AkonadiFakeData data; 0430 0431 // One top level collection 0432 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42")).withTaskContent()); 0433 0434 // Three projects in the collection, one not matching the predicate 0435 data.createItem(GenTodo().withId(42).withParent(42).asProject().withTitle(QStringLiteral("42-in"))); 0436 data.createItem(GenTodo().withId(43).withParent(42).asProject().withTitle(QStringLiteral("43-in"))); 0437 data.createItem(GenTodo().withId(44).withParent(42).asProject().withTitle(QStringLiteral("44-ex"))); 0438 0439 // Couple of tasks in the collection which should not appear or create trouble 0440 data.createItem(GenTodo().withId(40).withParent(42).withTitle(QStringLiteral("40"))); 0441 data.createItem(GenTodo().withId(41).withParent(42).withTitle(QStringLiteral("41-in"))); 0442 0443 auto integrator = createIntegrator(data); 0444 auto storage = createStorage(data); 0445 0446 auto query = Domain::LiveQueryOutput<Domain::Project::Ptr>::Ptr(); 0447 auto fetch = fetchItemsInAllCollectionsFunction(storage, nullptr); 0448 auto predicate = [] (const Akonadi::Item &item) { 0449 return titleFromItem(item).endsWith(QLatin1StringView("-in")); 0450 }; 0451 0452 // Initial listing 0453 // WHEN 0454 integrator->bind("project1", query, fetch, predicate); 0455 auto result = query->result(); 0456 result->data(); 0457 integrator->bind("project2", query, fetch, predicate); 0458 result = query->result(); // Should not cause any problem or wrong data 0459 0460 // THEN 0461 QVERIFY(result->data().isEmpty()); 0462 TestHelpers::waitForEmptyJobQueue(); 0463 0464 QCOMPARE(result->data().size(), 2); 0465 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0466 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0467 0468 // Reacts to add 0469 // WHEN 0470 data.createItem(GenTodo().withId(45).withParent(42).asProject().withTitle(QStringLiteral("45-in"))); 0471 0472 // THEN 0473 QCOMPARE(result->data().size(), 3); 0474 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0475 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0476 QCOMPARE(result->data().at(2)->name(), QStringLiteral("45-in")); 0477 0478 // Reacts to remove 0479 // WHEN 0480 data.removeItem(Akonadi::Item(45)); 0481 0482 // THEN 0483 QCOMPARE(result->data().size(), 2); 0484 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-in")); 0485 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0486 0487 // Reacts to change 0488 // WHEN 0489 data.modifyItem(GenTodo(data.item(42)).withTitle(QStringLiteral("42-bis-in"))); 0490 0491 // THEN 0492 QCOMPARE(result->data().size(), 2); 0493 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0494 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0495 0496 // Reacts to change (which adds) 0497 // WHEN 0498 data.modifyItem(GenTodo(data.item(44)).withTitle(QStringLiteral("44-in"))); 0499 0500 // THEN 0501 QCOMPARE(result->data().size(), 3); 0502 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0503 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0504 QCOMPARE(result->data().at(2)->name(), QStringLiteral("44-in")); 0505 0506 // Reacts to change (which removes) 0507 // WHEN 0508 data.modifyItem(GenTodo(data.item(44)).withTitle(QStringLiteral("44-ex"))); 0509 0510 // THEN 0511 QCOMPARE(result->data().size(), 2); 0512 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42-bis-in")); 0513 QCOMPARE(result->data().at(1)->name(), QStringLiteral("43-in")); 0514 0515 // Don't keep a reference on any result 0516 result.clear(); 0517 0518 // The bug we're trying to hit here is the following: 0519 // - when bind is called the first time a provider is created internally 0520 // - result is deleted at the end of the loop, no one holds the provider with 0521 // a strong reference anymore so it is deleted as well 0522 // - when bind is called the second time, there's a risk of a dangling 0523 // pointer if the recycling of providers is wrongly implemented which can lead 0524 // to a crash, if it is properly done no crash will occur 0525 for (int i = 0; i < 2; i++) { 0526 // WHEN * 2 0527 integrator->bind("projectN", query, fetch, predicate); 0528 auto result = query->result(); 0529 0530 // THEN * 2 0531 QVERIFY(result->data().isEmpty()); 0532 TestHelpers::waitForEmptyJobQueue(); 0533 QVERIFY(!result->data().isEmpty()); 0534 } 0535 } 0536 0537 void shouldMoveProjectsBetweenQueries() 0538 { 0539 // GIVEN 0540 AkonadiFakeData data; 0541 0542 // One top level collection 0543 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42")).withTaskContent()); 0544 0545 // One project which shows in one query and not the other 0546 data.createItem(GenTodo().withId(42).withParent(42).asProject().withTitle(QStringLiteral("42-in"))); 0547 0548 // Couple of tasks in the collection which should not appear or create trouble 0549 data.createItem(GenTodo().withId(39).withParent(42).withTitle(QStringLiteral("39"))); 0550 data.createItem(GenTodo().withId(40).withParent(42).withTitle(QStringLiteral("40-ex"))); 0551 data.createItem(GenTodo().withId(41).withParent(42).withTitle(QStringLiteral("41-in"))); 0552 0553 auto integrator = createIntegrator(data); 0554 auto storage = createStorage(data); 0555 0556 auto inQuery = Domain::LiveQueryOutput<Domain::Project::Ptr>::Ptr(); 0557 auto exQuery = Domain::LiveQueryOutput<Domain::Project::Ptr>::Ptr(); 0558 auto fetch = fetchItemsInAllCollectionsFunction(storage, nullptr); 0559 auto inPredicate = [] (const Akonadi::Item &item) { 0560 return titleFromItem(item).endsWith(QLatin1StringView("-in")); 0561 }; 0562 auto exPredicate = [] (const Akonadi::Item &item) { 0563 return titleFromItem(item).endsWith(QLatin1StringView("-ex")); 0564 }; 0565 0566 integrator->bind("project-in", inQuery, fetch, inPredicate); 0567 auto inResult = inQuery->result(); 0568 0569 integrator->bind("project-ex", exQuery, fetch, exPredicate); 0570 auto exResult = exQuery->result(); 0571 0572 TestHelpers::waitForEmptyJobQueue(); 0573 0574 QCOMPARE(inResult->data().size(), 1); 0575 QCOMPARE(exResult->data().size(), 0); 0576 0577 // WHEN 0578 data.modifyItem(GenTodo(data.item(42)).withTitle(QStringLiteral("42-ex"))); 0579 0580 // THEN 0581 QCOMPARE(inResult->data().size(), 0); 0582 QCOMPARE(exResult->data().size(), 1); 0583 } 0584 0585 void shouldReactToCollectionSelectionChangesForProjectQueries() 0586 { 0587 // GIVEN 0588 AkonadiFakeData data; 0589 0590 // Two top level collections 0591 data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); 0592 data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); 0593 0594 // One project in each collection 0595 data.createItem(GenTodo().withId(42).withParent(42).asProject().withTitle(QStringLiteral("42"))); 0596 data.createItem(GenTodo().withId(43).withParent(43).asProject().withTitle(QStringLiteral("43"))); 0597 0598 // Couple of tasks in the collections which should not appear or create trouble 0599 data.createItem(GenTodo().withId(40).withParent(42).withTitle(QStringLiteral("40"))); 0600 data.createItem(GenTodo().withId(41).withParent(43).withTitle(QStringLiteral("41"))); 0601 0602 auto integrator = createIntegrator(data); 0603 auto storage = createStorage(data); 0604 auto serializer = createSerializer(); 0605 0606 auto query = Domain::LiveQueryOutput<Domain::Project::Ptr>::Ptr(); 0607 auto fetch = fetchItemsInSelectedCollectionsFunction(storage, serializer, nullptr); 0608 auto predicate = [] (const Akonadi::Item &) { 0609 return true; 0610 }; 0611 0612 integrator->bind("project query", query, fetch, predicate); 0613 auto result = query->result(); 0614 TestHelpers::waitForEmptyJobQueue(); 0615 QCOMPARE(result->data().size(), 2); 0616 0617 // WHEN 0618 data.modifyCollection(GenCollection(data.collection(43)).selected(false)); 0619 TestHelpers::waitForEmptyJobQueue(); 0620 0621 // THEN 0622 QCOMPARE(result->data().size(), 1); 0623 QCOMPARE(result->data().at(0)->name(), QStringLiteral("42")); 0624 } 0625 0626 0627 0628 0629 0630 void shouldBindTaskQueries() 0631 { 0632 // GIVEN 0633 AkonadiFakeData data; 0634 0635 // One top level collection 0636 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42")).withTaskContent()); 0637 0638 // Three tasks in the collection, one not matching the predicate 0639 data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42-in"))); 0640 data.createItem(GenTodo().withId(43).withParent(42).withTitle(QStringLiteral("43-in"))); 0641 data.createItem(GenTodo().withId(44).withParent(42).withTitle(QStringLiteral("44-ex"))); 0642 0643 // Couple of projects in the collection which should not appear or create trouble 0644 data.createItem(GenTodo().withId(38).withParent(42).asProject().withTitle(QStringLiteral("38"))); 0645 data.createItem(GenTodo().withId(39).withParent(42).asProject().withTitle(QStringLiteral("39-in"))); 0646 0647 auto integrator = createIntegrator(data); 0648 auto storage = createStorage(data); 0649 0650 auto query = Domain::LiveQueryOutput<Domain::Task::Ptr>::Ptr(); 0651 auto fetch = fetchItemsInAllCollectionsFunction(storage, nullptr); 0652 auto predicate = [] (const Akonadi::Item &item) { 0653 return titleFromItem(item).endsWith(QLatin1StringView("-in")); 0654 }; 0655 0656 // Initial listing 0657 // WHEN 0658 integrator->bind("task1", query, fetch, predicate); 0659 auto result = query->result(); 0660 result->data(); 0661 integrator->bind("task2", query, fetch, predicate); 0662 result = query->result(); // Should not cause any problem or wrong data 0663 0664 // THEN 0665 QVERIFY(result->data().isEmpty()); 0666 TestHelpers::waitForEmptyJobQueue(); 0667 0668 QCOMPARE(result->data().size(), 2); 0669 QCOMPARE(result->data().at(0)->title(), QStringLiteral("42-in")); 0670 QCOMPARE(result->data().at(1)->title(), QStringLiteral("43-in")); 0671 0672 // Reacts to add 0673 // WHEN 0674 data.createItem(GenTodo().withId(45).withParent(42).withTitle(QStringLiteral("45-in"))); 0675 0676 // THEN 0677 QCOMPARE(result->data().size(), 3); 0678 QCOMPARE(result->data().at(0)->title(), QStringLiteral("42-in")); 0679 QCOMPARE(result->data().at(1)->title(), QStringLiteral("43-in")); 0680 QCOMPARE(result->data().at(2)->title(), QStringLiteral("45-in")); 0681 0682 // Reacts to remove 0683 // WHEN 0684 data.removeItem(Akonadi::Item(45)); 0685 0686 // THEN 0687 QCOMPARE(result->data().size(), 2); 0688 QCOMPARE(result->data().at(0)->title(), QStringLiteral("42-in")); 0689 QCOMPARE(result->data().at(1)->title(), QStringLiteral("43-in")); 0690 0691 // Reacts to change 0692 // WHEN 0693 data.modifyItem(GenTodo(data.item(42)).withTitle(QStringLiteral("42-bis-in"))); 0694 0695 // THEN 0696 QCOMPARE(result->data().size(), 2); 0697 QCOMPARE(result->data().at(0)->title(), QStringLiteral("42-bis-in")); 0698 QCOMPARE(result->data().at(1)->title(), QStringLiteral("43-in")); 0699 0700 // Reacts to change (which adds) 0701 // WHEN 0702 data.modifyItem(GenTodo(data.item(44)).withTitle(QStringLiteral("44-in"))); 0703 0704 // THEN 0705 QCOMPARE(result->data().size(), 3); 0706 QCOMPARE(result->data().at(0)->title(), QStringLiteral("42-bis-in")); 0707 QCOMPARE(result->data().at(1)->title(), QStringLiteral("43-in")); 0708 QCOMPARE(result->data().at(2)->title(), QStringLiteral("44-in")); 0709 0710 // Reacts to change (which removes) 0711 // WHEN 0712 data.modifyItem(GenTodo(data.item(44)).withTitle(QStringLiteral("44-ex"))); 0713 0714 // THEN 0715 QCOMPARE(result->data().size(), 2); 0716 QCOMPARE(result->data().at(0)->title(), QStringLiteral("42-bis-in")); 0717 QCOMPARE(result->data().at(1)->title(), QStringLiteral("43-in")); 0718 0719 // Don't keep a reference on any result 0720 result.clear(); 0721 0722 // The bug we're trying to hit here is the following: 0723 // - when bind is called the first time a provider is created internally 0724 // - result is deleted at the end of the loop, no one holds the provider with 0725 // a strong reference anymore so it is deleted as well 0726 // - when bind is called the second time, there's a risk of a dangling 0727 // pointer if the recycling of providers is wrongly implemented which can lead 0728 // to a crash, if it is properly done no crash will occur 0729 for (int i = 0; i < 2; i++) { 0730 // WHEN * 2 0731 integrator->bind("taskN", query, fetch, predicate); 0732 auto result = query->result(); 0733 0734 // THEN * 2 0735 QVERIFY(result->data().isEmpty()); 0736 TestHelpers::waitForEmptyJobQueue(); 0737 QVERIFY(!result->data().isEmpty()); 0738 } 0739 } 0740 0741 void shouldMoveTasksBetweenQueries() 0742 { 0743 // GIVEN 0744 AkonadiFakeData data; 0745 0746 // One top level collection 0747 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42")).withTaskContent()); 0748 0749 // One task which shows in one query and not the other 0750 data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42-in"))); 0751 0752 auto integrator = createIntegrator(data); 0753 auto storage = createStorage(data); 0754 0755 auto inQuery = Domain::LiveQueryOutput<Domain::Task::Ptr>::Ptr(); 0756 auto exQuery = Domain::LiveQueryOutput<Domain::Task::Ptr>::Ptr(); 0757 auto fetch = fetchItemsInAllCollectionsFunction(storage, nullptr); 0758 auto inPredicate = [] (const Akonadi::Item &item) { 0759 return titleFromItem(item).endsWith(QLatin1StringView("-in")); 0760 }; 0761 auto exPredicate = [] (const Akonadi::Item &item) { 0762 return titleFromItem(item).endsWith(QLatin1StringView("-ex")); 0763 }; 0764 0765 integrator->bind("task-in", inQuery, fetch, inPredicate); 0766 auto inResult = inQuery->result(); 0767 0768 integrator->bind("task-ex", exQuery, fetch, exPredicate); 0769 auto exResult = exQuery->result(); 0770 0771 TestHelpers::waitForEmptyJobQueue(); 0772 0773 QCOMPARE(inResult->data().size(), 1); 0774 QCOMPARE(exResult->data().size(), 0); 0775 0776 // WHEN 0777 data.modifyItem(GenTodo(data.item(42)).withTitle(QStringLiteral("42-ex"))); 0778 0779 // THEN 0780 QCOMPARE(inResult->data().size(), 0); 0781 QCOMPARE(exResult->data().size(), 1); 0782 } 0783 0784 void shouldReactToCollectionSelectionChangesForTaskQueries() 0785 { 0786 // GIVEN 0787 AkonadiFakeData data; 0788 0789 // Two top level collections 0790 data.createCollection(GenCollection().withId(42).withRootAsParent().withTaskContent()); 0791 data.createCollection(GenCollection().withId(43).withRootAsParent().withTaskContent()); 0792 0793 // One task in each collection 0794 data.createItem(GenTodo().withId(42).withParent(42).withTitle(QStringLiteral("42"))); 0795 data.createItem(GenTodo().withId(43).withParent(43).withTitle(QStringLiteral("43"))); 0796 0797 // Couple of projects in the collections which should not appear or create trouble 0798 data.createItem(GenTodo().withId(40).withParent(42).asProject().withTitle(QStringLiteral("40"))); 0799 data.createItem(GenTodo().withId(41).withParent(43).asProject().withTitle(QStringLiteral("41"))); 0800 0801 auto integrator = createIntegrator(data); 0802 auto storage = createStorage(data); 0803 auto serializer = createSerializer(); 0804 0805 auto query = Domain::LiveQueryOutput<Domain::Task::Ptr>::Ptr(); 0806 auto fetch = fetchItemsInSelectedCollectionsFunction(storage, serializer, nullptr); 0807 auto predicate = [] (const Akonadi::Item &) { 0808 return true; 0809 }; 0810 0811 integrator->bind("task query", query, fetch, predicate); 0812 auto result = query->result(); 0813 TestHelpers::waitForEmptyJobQueue(); 0814 QCOMPARE(result->data().size(), 2); 0815 0816 // WHEN 0817 data.modifyCollection(GenCollection(data.collection(43)).selected(false)); 0818 TestHelpers::waitForEmptyJobQueue(); 0819 0820 // THEN 0821 QCOMPARE(result->data().size(), 1); 0822 QCOMPARE(result->data().at(0)->title(), QStringLiteral("42")); 0823 } 0824 0825 0826 0827 0828 void shouldCallCollectionRemoveHandlers() 0829 { 0830 // GIVEN 0831 AkonadiFakeData data; 0832 0833 // One top level collection 0834 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42"))); 0835 0836 auto integrator = createIntegrator(data); 0837 qint64 removedId = -1; 0838 integrator->addRemoveHandler([&removedId] (const Akonadi::Collection &collection) { 0839 removedId = collection.id(); 0840 }); 0841 0842 // WHEN 0843 data.removeCollection(Akonadi::Collection(42)); 0844 0845 // THEN 0846 QCOMPARE(removedId, qint64(42)); 0847 } 0848 0849 void shouldCallItemRemoveHandlers() 0850 { 0851 // GIVEN 0852 AkonadiFakeData data; 0853 0854 // One top level collection 0855 data.createCollection(GenCollection().withId(42).withRootAsParent().withName(QStringLiteral("42"))); 0856 0857 // One item in the collection 0858 data.createItem(GenTodo().withId(42).withParent(42)); 0859 0860 auto integrator = createIntegrator(data); 0861 qint64 removedId = -1; 0862 integrator->addRemoveHandler([&removedId] (const Akonadi::Item &item) { 0863 removedId = item.id(); 0864 }); 0865 0866 // WHEN 0867 data.removeItem(Akonadi::Item(42)); 0868 0869 // THEN 0870 QCOMPARE(removedId, qint64(42)); 0871 } 0872 0873 }; 0874 0875 ZANSHIN_TEST_MAIN(AkonadiLiveQueryIntegratorTest) 0876 0877 #include "akonadilivequeryintegratortest.moc"