diff --git a/akonadi/collectionsync.cpp b/akonadi/collectionsync.cpp index 91915be52..62a3f05d9 100644 --- a/akonadi/collectionsync.cpp +++ b/akonadi/collectionsync.cpp @@ -1,509 +1,532 @@ /* Copyright (c) 2007, 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionsync_p.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionfetchscope.h" #include #include using namespace Akonadi; struct RemoteNode; /** LocalNode is used to build a tree structure of all our locally existing collections. */ struct LocalNode { LocalNode( const Collection &col ) : collection( col ), parentNode( 0 ), processed( false ) {} ~LocalNode() { qDeleteAll( childNodes ); qDeleteAll( pendingRemoteNodes ); } Collection collection; LocalNode *parentNode; // ### really needed? QList childNodes; QHash childRidMap; /** When using hierarchical RIDs we attach a list of not yet processable remote nodes to the closest already existing local ancestor node. They will be re-evaluated once a new child node is added. */ QList pendingRemoteNodes; bool processed; }; Q_DECLARE_METATYPE( LocalNode* ) static const char LOCAL_NODE[] = "LocalNode"; /** RemoteNode is used as a container for remote collections which typically don't have a UID set and thus cannot easily be compared or put into maps etc. */ struct RemoteNode { RemoteNode( const Collection &col ) : collection( col ) {} Collection collection; }; Q_DECLARE_METATYPE( RemoteNode* ) static const char REMOTE_NODE[] = "RemoteNode"; /** * @internal */ class CollectionSync::Private { public: Private( CollectionSync *parent ) : q( parent ), pendingJobs( 0 ), incremental( false ), streaming( false ), hierarchicalRIDs( false ), localListDone( false ), deliveryDone( false ) { localRoot = new LocalNode( Collection::root() ); localUidMap.insert( localRoot->collection.id(), localRoot ); if ( !hierarchicalRIDs ) localRidMap.insert( QString(), localRoot ); } ~Private() { delete localRoot; } /** Create a local node from the given local collection and integrate it into the local tree structure. */ LocalNode* createLocalNode( const Collection &col ) { LocalNode *node = new LocalNode( col ); Q_ASSERT( !localUidMap.contains( col.id() ) ); localUidMap.insert( node->collection.id(), node ); if ( !hierarchicalRIDs ) localRidMap.insert( node->collection.remoteId(), node ); // add already existing children if ( localPendingCollections.contains( col.id() ) ) { QList childIds = localPendingCollections.take( col.id() ); foreach ( Collection::Id childId, childIds ) { Q_ASSERT( localUidMap.contains( childId ) ); LocalNode *childNode = localUidMap.value( childId ); node->childNodes.append( childNode ); node->childRidMap.insert( childNode->collection.remoteId(), childNode ); } } // set our parent and add ourselves as child if ( localUidMap.contains( col.parentCollection().id() ) ) { node->parentNode = localUidMap.value( col.parentCollection().id() ); node->parentNode->childNodes.append( node ); } else { localPendingCollections[ col.parentCollection().id() ].append( col.id() ); } return node; } /** Same as createLocalNode() for remote collections. */ void createRemoteNode( const Collection &col ) { if ( col.remoteId().isEmpty() ) { kWarning() << "Collection '" << col.name() << "' does not have a remote identifier - skipping"; return; } RemoteNode *node = new RemoteNode( col ); localRoot->pendingRemoteNodes.append( node ); } + /** Create local nodes as we receive the local listing from the Akonadi server. */ + void localCollectionsReceived( const Akonadi::Collection::List &localCols ) + { + foreach ( const Collection &c, localCols ) + createLocalNode( c ); + } + + /** Once the local collection listing finished we can continue with the interesting stuff. */ + void localCollectionFetchResult( KJob *job ) + { + if ( job->error() ) + return; // handled by the base class + + // safety check: the local tree has to be connected + if ( !localPendingCollections.isEmpty() ) { + q->setError( Unknown ); + q->setErrorText( QLatin1String( "Inconsistent local collection tree detected! OMG, what have you done to my database?!?!" ) ); + q->emitResult(); + return; + } + + localListDone = true; + execute(); + } + /** Find the local node that matches the given remote collection, returns 0 if that doesn't exist (yet). */ LocalNode* findMatchingLocalNode( const Collection &collection ) { if ( !hierarchicalRIDs ) { if ( localRidMap.contains( collection.remoteId() ) ) return localRidMap.value( collection.remoteId() ); return 0; } else { LocalNode *localParent = 0; + if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { + kWarning() << "Remote collection without valid parent found: " << collection; + return 0; + } if ( collection.parentCollection() == Collection::root() ) localParent = localRoot; else localParent = findMatchingLocalNode( collection.parentCollection() ); - if ( localParent->childRidMap.contains( collection.remoteId() ) ) + if ( localParent && localParent->childRidMap.contains( collection.remoteId() ) ) return localParent->childRidMap.value( collection.remoteId() ); return 0; } } /** Find the local node that is the nearest ancestor of the given remote collection (when using hierarchical RIDs only, otherwise it's always the local root node). Never returns 0. */ LocalNode* findBestLocalAncestor( const Collection &collection, bool *exactMatch = 0 ) { if ( !hierarchicalRIDs ) return localRoot; + if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { + kWarning() << "Remote collection without valid parent found: " << collection; + return 0; + } if ( collection.parentCollection() == Collection::root() ) { if ( exactMatch ) *exactMatch = true; return localRoot; } bool parentIsExact = false; LocalNode *localParent = findBestLocalAncestor( collection.parentCollection(), &parentIsExact ); if ( !parentIsExact ) { if ( exactMatch ) *exactMatch = false; return localParent; } if ( localParent->childRidMap.contains( collection.remoteId() ) ) { if ( exactMatch ) *exactMatch = true; return localParent->childRidMap.value( collection.remoteId() ); } if ( exactMatch ) *exactMatch = false; return localParent; } /** Checks the pending remote nodes attached to the given local root node to see if any of them can be processed by now. If not, they are moved to the closest ancestor available. */ void processPendingRemoteNodes( LocalNode *localRoot ) { QList pendingRemoteNodes( localRoot->pendingRemoteNodes ); localRoot->pendingRemoteNodes.clear(); QHash > pendingCreations; foreach ( RemoteNode *remoteNode, pendingRemoteNodes ) { // step 1: see if we have a matching local node already LocalNode *localNode = findMatchingLocalNode( remoteNode->collection ); if ( localNode ) { Q_ASSERT( !localNode->processed ); // TODO: moving when using global RIDs updateLocalCollection( localNode, remoteNode ); continue; } // step 2: check if we have the parent at least, then we can create it localNode = findMatchingLocalNode( remoteNode->collection.parentCollection() ); if ( localNode ) { pendingCreations[localNode].append( remoteNode ); continue; } // step 3: find the best matching ancestor and enqueue it for later processing localNode = findBestLocalAncestor( remoteNode->collection ); - Q_ASSERT( localNode ); + if ( !localNode ) { + q->setError( Unknown ); + q->setErrorText( QLatin1String( "Remote collection without root-terminated ancestor chain provided, fix your resource dude!" ) ); + q->emitResult(); + return; + } localNode->pendingRemoteNodes.append( remoteNode ); } // process the now possible collection creations for ( QHash >::const_iterator it = pendingCreations.constBegin(); it != pendingCreations.constEnd(); ++it ) { createLocalCollections( it.key(), it.value() ); } } /** Performs a local update for the given node pair. */ void updateLocalCollection( LocalNode *localNode, RemoteNode *remoteNode ) { ++pendingJobs; Collection upd( remoteNode->collection ); upd.setId( localNode->collection.id() ); CollectionModifyJob *mod = new CollectionModifyJob( upd, q ); mod->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); connect( mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); localNode->processed = true; } void updateLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); delete remoteNode; checkDone(); } /** Creates local folders for the given local parent and remote nodes. @todo group CollectionCreateJobs into a single one once it supports that */ void createLocalCollections( LocalNode* localParent, QList remoteNodes ) { foreach ( RemoteNode *remoteNode, remoteNodes ) { ++pendingJobs; Collection col( remoteNode->collection ); col.setParentCollection( localParent->collection ); CollectionCreateJob *create = new CollectionCreateJob( col, q ); create->setProperty( LOCAL_NODE, QVariant::fromValue( localParent ) ); create->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); connect( create, SIGNAL(result(KJob*)), q, SLOT(createLocalCollectionResult(KJob*)) ); } } void createLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class const Collection newLocal = static_cast( job )->collection(); LocalNode* localNode = createLocalNode( newLocal ); localNode->processed = true; LocalNode* localParent = job->property( LOCAL_NODE ).value(); Q_ASSERT( localParent->childNodes.contains( localNode ) ); RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); delete remoteNode; processPendingRemoteNodes( localParent ); if ( !hierarchicalRIDs ) processPendingRemoteNodes( localRoot ); checkDone(); } /** Find all local nodes that are not marked as processed. */ Collection::List findUnprocessedLocalCollections( LocalNode *localNode ) { Collection::List rv; if ( !localNode->processed ) { rv.append( localNode->collection ); return rv; } foreach ( LocalNode *child, localNode->childNodes ) rv.append( findUnprocessedLocalCollections( child ) ); return rv; } /** Deletes unprocessed local nodes, in non-incremental mode. */ void deleteUnprocessedLocalNodes() { if ( incremental ) return; Collection::List cols = findUnprocessedLocalCollections( localRoot ); } /** Deletes the given collection list. @todo optimite delete job to support batch operations */ void deleteLocalCollections( const Collection::List &cols ) { foreach ( const Collection &col, cols ) { ++pendingJobs; CollectionDeleteJob *job = new CollectionDeleteJob( col, q ); connect( job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*)) ); } } void deleteLocalCollectionsResult( KJob *job ) { if ( job->error() ) return; // handled by the base class --pendingJobs; checkDone(); } /** Process what's currently available. */ void execute() { if ( !localListDone ) return; processPendingRemoteNodes( localRoot ); if ( !incremental && deliveryDone ) deleteUnprocessedLocalNodes(); if ( !hierarchicalRIDs ) { deleteLocalCollections( removedRemoteCollections ); } else { Collection::List localCols; foreach ( const Collection &c, removedRemoteCollections ) { LocalNode *node = findMatchingLocalNode( c ); if ( node ) localCols.append( node->collection ); } deleteLocalCollections( localCols ); } removedRemoteCollections.clear(); checkDone(); } /** Finds pending remote nodes, which at the end of the day should be an empty set. */ QList findPendingRemoteNodes( LocalNode *localNode ) { QList rv; rv.append( localNode->pendingRemoteNodes ); foreach ( LocalNode *child, localNode->childNodes ) rv.append( findPendingRemoteNodes( child ) ); return rv; } /** Are we there yet?? @todo progress reporting */ void checkDone() { // still running jobs or not fully delivered local/remote state if ( !deliveryDone || pendingJobs > 0 || !localListDone ) return; // safety check: there must be no pending remote nodes anymore QList orphans = findPendingRemoteNodes( localRoot ); if ( !orphans.isEmpty() ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Found unresolved orphan collections" ) ); foreach ( RemoteNode* orphan, orphans ) kDebug() << "found orphan collection:" << orphan->collection; } q->commit(); } CollectionSync *q; QString resourceId; int pendingJobs; LocalNode* localRoot; QHash localUidMap; QHash localRidMap; // temporary during build-up of the local node tree, must be empty afterwards QHash > localPendingCollections; // removed remote collections in incremental mode Collection::List removedRemoteCollections; bool incremental; bool streaming; bool hierarchicalRIDs; bool localListDone; bool deliveryDone; }; CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : TransactionSequence( parent ), d( new Private( this ) ) { d->resourceId = resourceId; } CollectionSync::~CollectionSync() { delete d; } void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) { foreach ( const Collection &c, remoteCollections ) d->createRemoteNode( c ); if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) { d->incremental = true; foreach ( const Collection &c, changedCollections ) d->createRemoteNode( c ); d->removedRemoteCollections += removedCollections; if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::doStart() { CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); job->fetchScope().setResource( d->resourceId ); - connect( job, SIGNAL(result(KJob*)), SLOT(slotLocalListDone(KJob*)) ); -} - -// TODO make private, create local nodes when retrieving the streaming signal instead -void CollectionSync::slotLocalListDone(KJob * job) -{ - if ( job->error() ) - return; - - Collection::List list = static_cast( job )->collections(); - foreach ( const Collection &c, list ) - d->createLocalNode( c ); - - Q_ASSERT( d->localPendingCollections.isEmpty() ); - d->localListDone = true; - - d->execute(); + connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(localCollectionsReceived(Akonadi::Collection::List)) ); + connect( job, SIGNAL(result(KJob*)), SLOT(localCollectionFetchResult(KJob*)) ); } void CollectionSync::setStreamingEnabled( bool streaming ) { d->streaming = streaming; } void CollectionSync::retrievalDone() { d->deliveryDone = true; d->execute(); } void CollectionSync::setHierarchicalRemoteIds( bool hierarchical ) { d->hierarchicalRIDs = hierarchical; } #include "collectionsync_p.moc" diff --git a/akonadi/collectionsync_p.h b/akonadi/collectionsync_p.h index 73d9b425f..75ed693a1 100644 --- a/akonadi/collectionsync_p.h +++ b/akonadi/collectionsync_p.h @@ -1,122 +1,122 @@ /* Copyright (c) 2007, 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_COLLECTIONSYNC_P_H #define AKONADI_COLLECTIONSYNC_P_H #include #include namespace Akonadi { /** @internal Syncs remote and local collections. Basic terminology: - "local": The current state in the Akonadi server - "remote": The state in the backend, which is also the state the Akonadi server is supposed to have afterwards. There are three options to influence the way syncing is done: - Streaming vs. complete delivery: If streaming is enabled remote collections do not need to be delivered in a single batch but can be delivered in multiple chunks. This improves performance but requires an explicit notification when delivery has been completed. - Incremental vs. non-incremental: In the incremental case only remote changes since the last sync have to be delivered, in the non-incremental mode the full remote state has to be provided. The first is obviously the preferred way, but requires support by the backend. - Hierarchical vs. global RIDs: The first requires RIDs to be unique per parent collection, the second one requires globally unique RIDs (per resource). Those have different advantages and disadvantages, esp. regarding moving. Which one to chose mostly depends on what the backend provides in this regard. */ class CollectionSync : public TransactionSequence { Q_OBJECT public: /** Creates a new collection synchronzier. @param resourceId The identifier of the resource we are syncing. @param parent The parent object. */ explicit CollectionSync( const QString &resourceId, QObject *parent = 0 ); /** Destroys this job. */ ~CollectionSync(); /** Sets the result of a full remote collection listing. @param remoteCollections A list of collections. Important: All of these need a unique remote identifier and parent remote identifier. */ void setRemoteCollections( const Collection::List &remoteCollections ); /** Sets the result of an incremental remote collection listing. @param changedCollections A list of remotely added or changed collections. @param removedCollections A lost of remotely deleted collections. */ void setRemoteCollections( const Collection::List &changedCollections, const Collection::List &removedCollections ); /** Enables streaming, that is not all collections are delivered at once. Use setRemoteCollections() multiple times when streaming is enabled and call retrievalDone() when all collections have been retrieved. Must be called before the first call to setRemoteCollections(). */ void setStreamingEnabled( bool streaming ); /** Indicate that all collections have been retrieved in streaming mode. */ void retrievalDone(); /** Indicate whether the resource supplies collections with hierachical or global remote identifiers. @c false by default. + Must be called before the first call to setRemoteCollections(). */ void setHierarchicalRemoteIds( bool hierarchical ); protected: void doStart(); - private Q_SLOTS: - void slotLocalListDone( KJob *job ); - private: class Private; Private* const d; + Q_PRIVATE_SLOT( d, void localCollectionsReceived( const Akonadi::Collection::List &localCols ) ) + Q_PRIVATE_SLOT( d, void localCollectionFetchResult( KJob* job ) ) Q_PRIVATE_SLOT( d, void updateLocalCollectionResult(KJob* job) ) Q_PRIVATE_SLOT( d, void createLocalCollectionResult(KJob* job) ) Q_PRIVATE_SLOT( d, void deleteLocalCollectionsResult(KJob* job) ) }; } #endif diff --git a/akonadi/tests/collectionsynctest.cpp b/akonadi/tests/collectionsynctest.cpp index 52bcb585c..74851db97 100644 --- a/akonadi/tests/collectionsynctest.cpp +++ b/akonadi/tests/collectionsynctest.cpp @@ -1,197 +1,260 @@ /* Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "test_utils.h" #include #include #include #include #include #include #include "../akonadi/collectionsync.cpp" #include #include #include #include using namespace Akonadi; Q_DECLARE_METATYPE( KJob* ) class CollectionSyncTest : public QObject { Q_OBJECT private: Collection::List fetchCollections( const QString &res ) { CollectionFetchJob *fetch = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); fetch->fetchScope().setResource( res ); Q_ASSERT( fetch->exec() ); Q_ASSERT( !fetch->collections().isEmpty() ); return fetch->collections(); } + void makeTestData() + { + QTest::addColumn( "hierarchicalRIDs" ); + QTest::addColumn( "resource" ); + + QTest::newRow( "akonadi_knut_resource_0 global RID" ) << false << "akonadi_knut_resource_0"; + QTest::newRow( "akonadi_knut_resource_1 global RID" ) << false << "akonadi_knut_resource_1"; + QTest::newRow( "akonadi_knut_resource_2 global RID" ) << false << "akonadi_knut_resource_2"; + +/* QTest::newRow( "akonadi_knut_resource_0 hierarchical RID" ) << true << "akonadi_knut_resource_0"; + QTest::newRow( "akonadi_knut_resource_1 hierarchical RID" ) << true << "akonadi_knut_resource_1"; + QTest::newRow( "akonadi_knut_resource_2 hierarchical RID" ) << true << "akonadi_knut_resource_2";*/ + } + private slots: void initTestCase() { Control::start(); qRegisterMetaType(); // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } + void testFullSync_data() + { + makeTestData(); + } + void testFullSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( origCols ); AKVERIFYEXEC( syncer ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); } + void testFullStreamingSync_data() + { + makeTestData(); + } + void testFullStreamingSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setAutoDelete( false ); QSignalSpy spy( syncer, SIGNAL(result(KJob*)) ); QVERIFY( spy.isValid() ); syncer->setStreamingEnabled( true ); QTest::qWait( 10 ); QCOMPARE( spy.count(), 0 ); for ( int i = 0; i < origCols.count(); ++i ) { Collection::List l; l << origCols[i]; syncer->setRemoteCollections( l ); if ( i < origCols.count() - 1 ) QTest::qWait( 10 ); // enter the event loop so itemsync actually can do something QCOMPARE( spy.count(), 0 ); } syncer->retrievalDone(); QTest::qWait( 1000 ); // let it finish its job QCOMPARE( spy.count(), 1 ); KJob *job = spy.at( 0 ).at( 0 ).value(); QCOMPARE( job, syncer ); + QCOMPARE( job->errorText(), QString() ); QCOMPARE( job->error(), 0 ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); delete syncer; } + void testIncrementalSync_data() + { + makeTestData(); + } + void testIncrementalSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + if ( resource == QLatin1String( "akonadi_knut_resource_2" ) ) + QSKIP( "test requires more than one collection", SkipSingle ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + Collection::List origCols = fetchCollections( resource ); + + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( origCols, Collection::List() ); AKVERIFYEXEC( syncer ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); Collection::List delCols; delCols << resultCols.front(); resultCols.pop_front(); // ### not implemented yet I guess #if 0 Collection colWithOnlyRemoteId; colWithOnlyRemoteId.setRemoteId( resultCols.front().remoteId() ); delCols << colWithOnlyRemoteId; resultCols.pop_front(); #endif #if 0 // ### should this work? Collection colWithRandomRemoteId; colWithRandomRemoteId.setRemoteId( KRandom::randomString( 100 ) ); delCols << colWithRandomRemoteId; #endif - syncer = new CollectionSync( "akonadi_knut_resource_0" ); + syncer = new CollectionSync( resource, this ); syncer->setRemoteCollections( resultCols, delCols ); AKVERIFYEXEC( syncer ); - Collection::List resultCols2 = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols2 = fetchCollections( resource ); QCOMPARE( resultCols2.count(), resultCols.count() ); } + void testIncrementalStreamingSync_data() + { + makeTestData(); + } + void testIncrementalStreamingSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setAutoDelete( false ); QSignalSpy spy( syncer, SIGNAL(result(KJob*)) ); QVERIFY( spy.isValid() ); syncer->setStreamingEnabled( true ); QTest::qWait( 10 ); QCOMPARE( spy.count(), 0 ); for ( int i = 0; i < origCols.count(); ++i ) { Collection::List l; l << origCols[i]; syncer->setRemoteCollections( l, Collection::List() ); if ( i < origCols.count() - 1 ) QTest::qWait( 10 ); // enter the event loop so itemsync actually can do something QCOMPARE( spy.count(), 0 ); } syncer->retrievalDone(); QTest::qWait( 1000 ); // let it finish its job QCOMPARE( spy.count(), 1 ); KJob *job = spy.at( 0 ).at( 0 ).value(); QCOMPARE( job, syncer ); + QCOMPARE( job->errorText(), QString() ); QCOMPARE( job->error(), 0 ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); delete syncer; } + void testEmptyIncrementalSync_data() + { + makeTestData(); + } + void testEmptyIncrementalSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( Collection::List(), Collection::List() ); AKVERIFYEXEC( syncer ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); } }; QTEST_AKONADIMAIN( CollectionSyncTest, NoGUI ) #include "collectionsynctest.moc"