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"