tests/auto/qcontactasync/unittest/tst_qcontactasync.cpp
changeset 0 876b1a06bc25
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/auto/qcontactasync/unittest/tst_qcontactasync.cpp	Wed Aug 25 15:49:42 2010 +0300
@@ -0,0 +1,2358 @@
+/****************************************************************************
+**
+** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
+** All rights reserved.
+** Contact: Nokia Corporation (qt-info@nokia.com)
+**
+** This file is part of the Qt Mobility Components.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** No Commercial Usage
+** This file contains pre-release code and may not be distributed.
+** You may use this file in accordance with the terms and conditions
+** contained in the Technology Preview License Agreement accompanying
+** this package.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file.  Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Nokia gives you certain additional
+** rights.  These rights are described in the Nokia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** If you have questions regarding the use of this file, please contact
+** Nokia at qt-info@nokia.com.
+**
+**
+**
+**
+**
+**
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QCoreApplication>
+#include <QScopedPointer>
+
+#include "qtcontacts.h"
+#include "qcontactmanagerdataholder.h" //QContactManagerDataHolder
+
+QTM_USE_NAMESPACE
+/* Define an innocuous request (fetch ie doesn't mutate) to "fill up" any queues */
+#define FILL_QUEUE_WITH_FETCH_REQUESTS() QContactFetchRequest fqcfr1, fqcfr2, fqcfr3; \
+                                         QContactDetailDefinitionFetchRequest fqdfr1, fqdfr2, fqdfr3; \
+                                         fqcfr1.start(); \
+                                         fqcfr2.start(); \
+                                         fqcfr3.start(); \
+                                         fqdfr1.start(); \
+                                         fqdfr2.start(); \
+                                         fqdfr3.start();
+
+
+//TESTED_CLASS=
+//TESTED_FILES=
+
+// Unfortunately the plumbing isn't in place to allow cancelling requests at arbitrary points
+// in their processing.  So we do multiple loops until things work out.. or not
+#define MAX_OPTIMISTIC_SCHEDULING_LIMIT 100
+
+
+// Thread capable QThreadSignalSpy (to avoid data races with count/appendArgS)
+class QThreadSignalSpy: public QObject
+{
+public:
+    QThreadSignalSpy(QObject *obj, const char *aSignal)
+    {
+        QMutexLocker m(&lock);
+#ifdef Q_CC_BOR
+        const int memberOffset = QObject::staticMetaObject.methodCount();
+#else
+        static const int memberOffset = QObject::staticMetaObject.methodCount();
+#endif
+        Q_ASSERT(obj);
+        Q_ASSERT(aSignal);
+
+        if (((aSignal[0] - '0') & 0x03) != QSIGNAL_CODE) {
+            qWarning("QThreadSignalSpy: Not a valid signal, use the SIGNAL macro");
+            return;
+        }
+
+        QByteArray ba = QMetaObject::normalizedSignature(aSignal + 1);
+        const QMetaObject *mo = obj->metaObject();
+        int sigIndex = mo->indexOfMethod(ba.constData());
+        if (sigIndex < 0) {
+            qWarning("QThreadSignalSpy: No such signal: '%s'", ba.constData());
+            return;
+        }
+
+        if (!QMetaObject::connect(obj, sigIndex, this, memberOffset,
+                    Qt::DirectConnection, 0)) {
+            qWarning("QThreadSignalSpy: QMetaObject::connect returned false. Unable to connect.");
+            return;
+        }
+        sig = ba;
+        initArgs(mo->method(sigIndex));
+    }
+
+    inline bool isValid() const { return !sig.isEmpty(); }
+    inline QByteArray signal() const { return sig; }
+
+    int qt_metacall(QMetaObject::Call call, int methodId, void **a)
+    {
+        methodId = QObject::qt_metacall(call, methodId, a);
+        if (methodId < 0)
+            return methodId;
+
+        if (call == QMetaObject::InvokeMetaMethod) {
+            if (methodId == 0) {
+                appendArgs(a);
+            }
+            --methodId;
+        }
+        return methodId;
+    }
+
+    // The QList<QVariantList> API we actually use
+    int count() const
+    {
+        QMutexLocker m(&lock);
+        return savedArgs.count();
+    }
+    void clear()
+    {
+        QMutexLocker m(&lock);
+        savedArgs.clear();
+    }
+
+private:
+    void initArgs(const QMetaMethod &member)
+    {
+        QList<QByteArray> params = member.parameterTypes();
+        for (int i = 0; i < params.count(); ++i) {
+            int tp = QMetaType::type(params.at(i).constData());
+            if (tp == QMetaType::Void)
+                qWarning("Don't know how to handle '%s', use qRegisterMetaType to register it.",
+                         params.at(i).constData());
+            args << tp;
+        }
+    }
+
+    void appendArgs(void **a)
+    {
+        QMutexLocker m(&lock);
+        QList<QVariant> list;
+        for (int i = 0; i < args.count(); ++i) {
+            QMetaType::Type type = static_cast<QMetaType::Type>(args.at(i));
+            list << QVariant(type, a[i + 1]);
+        }
+        savedArgs.append(list);
+    }
+
+    // the full, normalized signal name
+    QByteArray sig;
+    // holds the QMetaType types for the argument list of the signal
+    QList<int> args;
+
+    mutable QMutex lock;
+    // Different API
+    QList< QVariantList> savedArgs;
+};
+
+class tst_QContactAsync : public QObject
+{
+    Q_OBJECT
+
+public:
+    tst_QContactAsync();
+    virtual ~tst_QContactAsync();
+
+public slots:
+    void initTestCase();
+    void cleanupTestCase();
+
+private:
+    void addManagers(QStringList includes = QStringList()); // add standard managers to the data
+
+private slots:
+    void testDestructor();
+    void testDestructor_data() { addManagers(QStringList(QString("maliciousplugin"))); }
+
+    void contactFetch();
+    void contactFetch_data() { addManagers(); }
+    void contactIdFetch();
+    void contactIdFetch_data() { addManagers(); }
+    void contactRemove();
+    void contactRemove_data() { addManagers(); }
+    void contactSave();
+    void contactSave_data() { addManagers(); }
+
+    void definitionFetch();
+    void definitionFetch_data() { addManagers(); }
+    void definitionRemove();
+    void definitionRemove_data() { addManagers(); }
+    void definitionSave();
+    void definitionSave_data() { addManagers(); }
+
+    void relationshipFetch();
+    void relationshipFetch_data() { addManagers(); }
+    void relationshipRemove();
+    void relationshipRemove_data() { addManagers(); }
+    void relationshipSave();
+    void relationshipSave_data() { addManagers(); }
+
+    void maliciousManager(); // uses it's own custom data (manager)
+
+    void testQuickDestruction();
+    void testQuickDestruction_data() { addManagers(QStringList(QString("maliciousplugin"))); }
+
+    void threadDelivery();
+    void threadDelivery_data() { addManagers(QStringList(QString("maliciousplugin"))); }
+protected slots:
+    void resultsAvailableReceived();
+    void deleteRequest();
+
+private:
+    bool compareContactLists(QList<QContact> lista, QList<QContact> listb);
+    bool compareContacts(QContact ca, QContact cb);
+    bool containsIgnoringTimestamps(const QList<QContact>& list, const QContact& c);
+    bool compareIgnoringTimestamps(const QContact& ca, const QContact& cb);
+    QContactManager* prepareModel(const QString& uri);
+
+    Qt::HANDLE m_mainThreadId;
+    Qt::HANDLE m_resultsAvailableSlotThreadId;
+    QScopedPointer<QContactManagerDataHolder> managerDataHolder;
+};
+
+tst_QContactAsync::tst_QContactAsync()
+{
+    // ensure we can load all of the plugins we need to.
+    QString path = QApplication::applicationDirPath() + "/dummyplugin/plugins";
+    QApplication::addLibraryPath(path);
+
+    qRegisterMetaType<QContactAbstractRequest::State>("QContactAbstractRequest::State");
+}
+
+tst_QContactAsync::~tst_QContactAsync()
+{
+}
+
+void tst_QContactAsync::initTestCase()
+{
+    managerDataHolder.reset(new QContactManagerDataHolder());
+}
+
+void tst_QContactAsync::cleanupTestCase()
+{
+    managerDataHolder.reset(0);
+}
+
+bool tst_QContactAsync::compareContactLists(QList<QContact> lista, QList<QContact> listb)
+{
+    // NOTE: This compare is contact order insensitive.  
+    
+    // Remove matching contacts
+    foreach (QContact a, lista) {
+        foreach (QContact b, listb) {
+            if (compareContacts(a, b)) {
+                lista.removeOne(a);
+                listb.removeOne(b);
+                break;
+            }
+        }
+    }    
+    return (lista.count() == 0 && listb.count() == 0);
+}
+
+bool tst_QContactAsync::compareContacts(QContact ca, QContact cb)
+{
+    // NOTE: This compare is contact detail order insensitive.
+    
+    if (ca.localId() != cb.localId())
+        return false;
+    
+    QList<QContactDetail> aDetails = ca.details();
+    QList<QContactDetail> bDetails = cb.details();
+
+    // Remove matching details
+    foreach (QContactDetail ad, aDetails) {
+        foreach (QContactDetail bd, bDetails) {
+            if (ad == bd) {
+                ca.removeDetail(&ad);
+                cb.removeDetail(&bd);
+                break;
+            }
+            
+            // Special handling for timestamp
+            if (ad.definitionName() == QContactTimestamp::DefinitionName &&
+                bd.definitionName() == QContactTimestamp::DefinitionName) {
+                QContactTimestamp at = static_cast<QContactTimestamp>(ad);
+                QContactTimestamp bt = static_cast<QContactTimestamp>(bd);
+                if (at.created().toString() == bt.created().toString() &&
+                    at.lastModified().toString() == bt.lastModified().toString()) {
+                    ca.removeDetail(&ad);
+                    cb.removeDetail(&bd);
+                    break;
+                }
+                    
+            }            
+        }
+    }
+    return (ca == cb);
+}
+
+bool tst_QContactAsync::containsIgnoringTimestamps(const QList<QContact>& list, const QContact& c)
+{
+    QList<QContact> cl = list;
+    QContact a(c);
+    for (int i = 0; i < cl.size(); i++) {
+        QContact b(cl.at(i));
+        if (compareIgnoringTimestamps(a, b))
+            return true;
+    }
+
+    return false;
+}
+
+bool tst_QContactAsync::compareIgnoringTimestamps(const QContact& ca, const QContact& cb)
+{
+    // Compares two contacts, ignoring any timestamp details
+    QContact a(ca);
+    QContact b(cb);
+    QList<QContactDetail> aDetails = a.details();
+    QList<QContactDetail> bDetails = b.details();
+
+    // They can be in any order, so loop
+    // First remove any matches, and any timestamps
+    foreach (QContactDetail d, aDetails) {
+        foreach (QContactDetail d2, bDetails) {
+            if (d == d2) {
+                a.removeDetail(&d);
+                b.removeDetail(&d2);
+                break;
+            }
+
+            if (d.definitionName() == QContactTimestamp::DefinitionName) {
+                a.removeDetail(&d);
+            }
+
+            if (d2.definitionName() == QContactTimestamp::DefinitionName) {
+                b.removeDetail(&d2);
+            }
+        }
+    }
+
+    if (a == b)
+        return true;
+    return false;
+}
+
+void tst_QContactAsync::testDestructor()
+{
+    QFETCH(QString, uri);
+    QContactManager* cm = prepareModel(uri);
+    QContactFetchRequest* req = new QContactFetchRequest;
+    req->setManager(cm);
+
+    QContactManager* cm2 = prepareModel(uri);
+    QContactFetchRequest* req2 = new QContactFetchRequest;
+    req2->setManager(cm2);
+
+    // first, delete manager then request
+    delete cm;
+    delete req;
+
+    // second, delete request then manager
+    delete req2;
+    delete cm2;
+}
+
+void tst_QContactAsync::deleteRequest()
+{
+    // Delete the sender (request) - check that it doesn't crash in this common coding error
+    delete sender();
+}
+
+void tst_QContactAsync::contactFetch()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+
+    QContactFetchRequest cfr;
+    QVERIFY(cfr.type() == QContactAbstractRequest::ContactFetchRequest);
+
+    // initial state - not started, no manager.
+    QVERIFY(!cfr.isActive());
+    QVERIFY(!cfr.isFinished());
+    QVERIFY(!cfr.start());
+    QVERIFY(!cfr.cancel());
+    QVERIFY(!cfr.waitForFinished());
+
+    // "all contacts" retrieval
+    QContactFilter fil;
+    cfr.setManager(cm.data());
+    QCOMPARE(cfr.manager(), cm.data());
+    QVERIFY(!cfr.isActive());
+    QVERIFY(!cfr.isFinished());
+    QVERIFY(!cfr.cancel());
+    QVERIFY(!cfr.waitForFinished());
+    qRegisterMetaType<QContactFetchRequest*>("QContactFetchRequest*");
+    QThreadSignalSpy spy(&cfr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    cfr.setFilter(fil);
+    QCOMPARE(cfr.filter(), fil);
+    QVERIFY(!cfr.cancel()); // not started
+
+    QVERIFY(cfr.start());
+    //QVERIFY(cfr.isFinished() || !cfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY((cfr.isActive() && cfr.state() == QContactAbstractRequest::ActiveState) || cfr.isFinished());
+    QVERIFY(cfr.waitForFinished());
+    QVERIFY(cfr.isFinished());
+
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QList<QContactLocalId> contactIds = cm->contactIds();
+    QList<QContact> contacts = cfr.contacts();
+    QCOMPARE(contactIds.size(), contacts.size());
+    for (int i = 0; i < contactIds.size(); i++) {
+        QContact curr = cm->contact(contactIds.at(i));
+        QVERIFY(contacts.at(i) == curr);
+    }
+
+    // asynchronous detail filtering
+    QContactDetailFilter dfil;
+    dfil.setDetailDefinitionName(QContactUrl::DefinitionName, QContactUrl::FieldUrl);
+    cfr.setFilter(dfil);
+    QVERIFY(cfr.filter() == dfil);
+    QVERIFY(!cfr.cancel()); // not started
+
+    QVERIFY(cfr.start());
+    QVERIFY((cfr.isActive() && cfr.state() == QContactAbstractRequest::ActiveState) || cfr.isFinished());
+    //QVERIFY(cfr.isFinished() || !cfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(cfr.waitForFinished());
+    QVERIFY(cfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    contactIds = cm->contactIds(dfil);
+    contacts = cfr.contacts();
+    QCOMPARE(contactIds.size(), contacts.size());
+    for (int i = 0; i < contactIds.size(); i++) {
+        QContact curr = cm->contact(contactIds.at(i));
+        QVERIFY(contacts.at(i) == curr);
+    }
+
+    // sort order
+    QContactSortOrder sortOrder;
+    sortOrder.setDetailDefinitionName(QContactPhoneNumber::DefinitionName, QContactPhoneNumber::FieldNumber);
+    QList<QContactSortOrder> sorting;
+    sorting.append(sortOrder);
+    cfr.setFilter(fil);
+    cfr.setSorting(sorting);
+    QCOMPARE(cfr.sorting(), sorting);
+    QVERIFY(!cfr.cancel()); // not started
+    QVERIFY(cfr.start());
+    QVERIFY((cfr.isActive() && cfr.state() == QContactAbstractRequest::ActiveState) || cfr.isFinished());
+    //QVERIFY(cfr.isFinished() || !cfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(cfr.waitForFinished());
+    QVERIFY(cfr.isFinished());
+
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    contactIds = cm->contactIds(sorting);
+    contacts = cfr.contacts();
+    QCOMPARE(contactIds.size(), contacts.size());
+    for (int i = 0; i < contactIds.size(); i++) {
+        QContact curr = cm->contact(contactIds.at(i));
+        QVERIFY(contacts.at(i) == curr);
+    }
+
+    // restrictions
+    sorting.clear();
+    cfr.setFilter(fil);
+    cfr.setSorting(sorting);
+    QContactFetchHint fetchHint;
+    fetchHint.setDetailDefinitionsHint(QStringList(QContactName::DefinitionName));
+    cfr.setFetchHint(fetchHint);
+    QCOMPARE(cfr.fetchHint().detailDefinitionsHint(), QStringList(QContactName::DefinitionName));
+    QVERIFY(!cfr.cancel()); // not started
+    QVERIFY(cfr.start());
+    QVERIFY((cfr.isActive() && cfr.state() == QContactAbstractRequest::ActiveState) || cfr.isFinished());
+    //QVERIFY(cfr.isFinished() || !cfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(cfr.waitForFinished());
+    QVERIFY(cfr.isFinished());
+
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    contactIds = cm->contactIds(sorting);
+    contacts = cfr.contacts();
+    QCOMPARE(contactIds.size(), contacts.size());
+    for (int i = 0; i < contactIds.size(); i++) {
+        // create a contact from the restricted data only (id + display label)
+        QContact currFull = cm->contact(contactIds.at(i));
+        QContact currRestricted;
+        currRestricted.setId(currFull.id());
+        QList<QContactName> names = currFull.details<QContactName>();
+        foreach (const QContactName& name, names) {
+            QContactName fullName = name;
+            if (!fullName.isEmpty()) {
+                currRestricted.saveDetail(&fullName);
+            }
+        }
+
+        // now find the contact in the retrieved list which our restricted contact mimics
+        QContact retrievedRestricted;
+        bool found = false;
+        foreach (const QContact& retrieved, contacts) {
+            if (retrieved.id() == currRestricted.id()) {
+                retrievedRestricted = retrieved;
+                found = true;
+            }
+        }
+
+        QVERIFY(found); // must exist or fail.
+
+        // ensure that the contact is the same (except synth fields)
+        QList<QContactDetail> retrievedDetails = retrievedRestricted.details();
+        QList<QContactDetail> expectedDetails = currRestricted.details();
+        foreach (const QContactDetail& det, expectedDetails) {
+            // ignore backend synthesised details
+            // again, this requires a "default contact details" function to work properly.
+            if (det.definitionName() == QContactDisplayLabel::DefinitionName
+                || det.definitionName() == QContactTimestamp::DefinitionName) {
+                continue;
+            }
+
+            // everything else in the expected contact should be in the retrieved one.
+            QVERIFY(retrievedDetails.contains(det));
+        }
+    }
+
+    // cancelling
+    sorting.clear();
+    cfr.setFilter(fil);
+    cfr.setSorting(sorting);
+    cfr.setFetchHint(QContactFetchHint());
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!cfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(cfr.start());
+        if (!cfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            spy.clear();
+            cfr.waitForFinished();
+            sorting.clear();
+            cfr.setFilter(fil);
+            cfr.setSorting(sorting);
+            cfr.setFetchHint(QContactFetchHint());
+            cfr.setFetchHint(QContactFetchHint());
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(cfr.waitForFinished());
+        QVERIFY(cfr.isCanceled());
+
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!cfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(cfr.start());
+        if (!cfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            cfr.waitForFinished();
+            sorting.clear();
+            cfr.setFilter(fil);
+            cfr.setSorting(sorting);
+            cfr.setFetchHint(QContactFetchHint());
+            bailoutCount -= 1;
+            spy.clear();
+            if (!bailoutCount) {
+                //qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            continue;
+        }
+        cfr.waitForFinished();
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        QVERIFY(!cfr.isActive());
+        QVERIFY(cfr.state() == QContactAbstractRequest::CanceledState);
+        break;
+    }
+
+    // Now test deletion in the first slot called
+    QContactFetchRequest *cfr2 = new QContactFetchRequest();
+    QPointer<QObject> obj(cfr2);
+    cfr2->setManager(cm.data());
+    connect(cfr2, SIGNAL(resultsAvailable()), this, SLOT(deleteRequest()));
+    QVERIFY(cfr2->start());
+    int i = 100;
+    // at this point we can't even call wait for finished..
+    while(obj && i > 0) {
+        QTest::qWait(50); // force it to process events at least once.
+        i--;
+    }
+    QVERIFY(obj == NULL);
+}
+
+void tst_QContactAsync::contactIdFetch()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    QContactLocalIdFetchRequest cfr;
+    QVERIFY(cfr.type() == QContactAbstractRequest::ContactLocalIdFetchRequest);
+
+    // initial state - not started, no manager.
+    QVERIFY(!cfr.isActive());
+    QVERIFY(!cfr.isFinished());
+    QVERIFY(!cfr.start());
+    QVERIFY(!cfr.cancel());
+    QVERIFY(!cfr.waitForFinished());
+
+    // "all contacts" retrieval
+    QContactFilter fil;
+    cfr.setManager(cm.data());
+    QCOMPARE(cfr.manager(), cm.data());
+    QVERIFY(!cfr.isActive());
+    QVERIFY(!cfr.isFinished());
+    QVERIFY(!cfr.cancel());
+    QVERIFY(!cfr.waitForFinished());
+    qRegisterMetaType<QContactLocalIdFetchRequest*>("QContactLocalIdFetchRequest*");
+
+    QThreadSignalSpy spy(&cfr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    cfr.setFilter(fil);
+    QCOMPARE(cfr.filter(), fil);
+    QVERIFY(!cfr.cancel()); // not started
+    QVERIFY(cfr.start());
+
+    QVERIFY((cfr.isActive() &&cfr.state() == QContactAbstractRequest::ActiveState) || cfr.isFinished());
+    //QVERIFY(cfr.isFinished() || !cfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(cfr.waitForFinished());
+    QVERIFY(cfr.isFinished());
+
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QList<QContactLocalId> contactIds = cm->contactIds();
+    QList<QContactLocalId> result = cfr.ids();
+    QCOMPARE(contactIds, result);
+
+    // asynchronous detail filtering
+    QContactDetailFilter dfil;
+    dfil.setDetailDefinitionName(QContactUrl::DefinitionName, QContactUrl::FieldUrl);
+    cfr.setFilter(dfil);
+    QVERIFY(cfr.filter() == dfil);
+    QVERIFY(!cfr.cancel()); // not started
+
+    QVERIFY(cfr.start());
+    QVERIFY((cfr.isActive() && cfr.state() == QContactAbstractRequest::ActiveState) || cfr.isFinished());
+    //QVERIFY(cfr.isFinished() || !cfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(cfr.waitForFinished());
+    QVERIFY(cfr.isFinished());
+
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    contactIds = cm->contactIds(dfil);
+    result = cfr.ids();
+    QCOMPARE(contactIds, result);
+
+    // sort order
+    QContactSortOrder sortOrder;
+    sortOrder.setDetailDefinitionName(QContactPhoneNumber::DefinitionName, QContactPhoneNumber::FieldNumber);
+    QList<QContactSortOrder> sorting;
+    sorting.append(sortOrder);
+    cfr.setFilter(fil);
+    cfr.setSorting(sorting);
+    QCOMPARE(cfr.sorting(), sorting);
+    QVERIFY(!cfr.cancel()); // not started
+    QVERIFY(cfr.start());
+    QVERIFY((cfr.isActive() && cfr.state() == QContactAbstractRequest::ActiveState) || cfr.isFinished());
+    //QVERIFY(cfr.isFinished() || !cfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(cfr.waitForFinished());
+    QVERIFY(cfr.isFinished());
+
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    contactIds = cm->contactIds(sorting);
+    result = cfr.ids();
+    QCOMPARE(contactIds, result);
+
+    // cancelling
+    sorting.clear();
+    cfr.setFilter(fil);
+    cfr.setSorting(sorting);
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!cfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(cfr.start());
+        if (!cfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            cfr.waitForFinished();
+            sorting.clear();
+            cfr.setFilter(fil);
+            cfr.setSorting(sorting);
+            bailoutCount -= 1;
+            spy.clear();
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(cfr.waitForFinished());
+        QVERIFY(cfr.isCanceled());
+
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!cfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(cfr.start());
+        if (!cfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            cfr.waitForFinished();
+            sorting.clear();
+            cfr.setFilter(fil);
+            cfr.setSorting(sorting);
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            continue;
+        }
+        cfr.waitForFinished();
+        QVERIFY(cfr.isCanceled());
+
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        break;
+    }
+
+}
+
+void tst_QContactAsync::contactRemove()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    QContactRemoveRequest crr;
+    QVERIFY(crr.type() == QContactAbstractRequest::ContactRemoveRequest);
+
+    // initial state - not started, no manager.
+    QVERIFY(!crr.isActive());
+    QVERIFY(!crr.isFinished());
+    QVERIFY(!crr.start());
+    QVERIFY(!crr.cancel());
+    QVERIFY(!crr.waitForFinished());
+
+    // specific contact set
+    crr.setContactId(QContactLocalId(3));
+    QVERIFY(crr.contactIds() == QList<QContactLocalId>() << QContactLocalId(3));
+
+    // specific contact removal via detail filter
+    int originalCount = cm->contactIds().size();
+    QContactDetailFilter dfil;
+    dfil.setDetailDefinitionName(QContactUrl::DefinitionName, QContactUrl::FieldUrl);
+    crr.setContactIds(cm->contactIds(dfil));
+    crr.setManager(cm.data());
+    QCOMPARE(crr.manager(), cm.data());
+    QVERIFY(!crr.isActive());
+    QVERIFY(!crr.isFinished());
+    QVERIFY(!crr.cancel());
+    QVERIFY(!crr.waitForFinished());
+    qRegisterMetaType<QContactRemoveRequest*>("QContactRemoveRequest*");
+    QThreadSignalSpy spy(&crr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    QVERIFY(!crr.cancel()); // not started
+
+    QVERIFY(!cm->contactIds(dfil).isEmpty());
+
+    QVERIFY(crr.start());
+
+    QVERIFY((crr.isActive() &&crr.state() == QContactAbstractRequest::ActiveState) || crr.isFinished());
+    //QVERIFY(crr.isFinished() || !crr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(crr.waitForFinished());
+    QVERIFY(crr.isFinished());
+
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QCOMPARE(cm->contactIds().size(), originalCount - 1);
+    QVERIFY(cm->contactIds(dfil).isEmpty());
+
+    // remove all contacts
+    dfil.setDetailDefinitionName(QContactDisplayLabel::DefinitionName); // delete everything.
+    crr.setContactIds(cm->contactIds(dfil));
+    
+    QVERIFY(!crr.cancel()); // not started
+    QVERIFY(crr.start());
+
+    QVERIFY((crr.isActive() && crr.state() == QContactAbstractRequest::ActiveState) || crr.isFinished());
+    //QVERIFY(crr.isFinished() || !crr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(crr.waitForFinished());
+    QVERIFY(crr.isFinished());
+
+    QCOMPARE(cm->contactIds().size(), 0); // no contacts should be left.
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    // cancelling
+    QContact temp;
+    QContactName nameDetail;
+    nameDetail.setFirstName("Should not be removed");
+    temp.saveDetail(&nameDetail);
+    cm->saveContact(&temp);
+    crr.setContactIds(cm->contactIds(dfil));
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!crr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(spy.count() == 0);
+        QVERIFY(crr.start());
+        if (!crr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            crr.waitForFinished();
+            crr.setContactIds(cm->contactIds(dfil));
+            temp.setId(QContactId());
+            if (!cm->saveContact(&temp)) {
+                QSKIP("Unable to save temporary contact for remove request cancellation test!", SkipSingle);
+            }
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(crr.waitForFinished());
+        QVERIFY(crr.isCanceled());
+        QCOMPARE(cm->contactIds().size(), 1);
+        QCOMPARE(cm->contactIds(), crr.contactIds());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!crr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(crr.start());
+        if (!crr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            crr.waitForFinished();
+            crr.setContactIds(cm->contactIds(dfil));
+            temp.setId(QContactId());
+            cm->saveContact(&temp);
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        crr.waitForFinished();
+        QVERIFY(crr.isCanceled());
+        QCOMPARE(cm->contactIds().size(), 1);
+        QCOMPARE(cm->contactIds(), crr.contactIds());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        break;
+    }
+
+}
+
+void tst_QContactAsync::contactSave()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    QContactSaveRequest csr;
+    QVERIFY(csr.type() == QContactAbstractRequest::ContactSaveRequest);
+
+    // initial state - not started, no manager.
+    QVERIFY(!csr.isActive());
+    QVERIFY(!csr.isFinished());
+    QVERIFY(!csr.start());
+    QVERIFY(!csr.cancel());
+    QVERIFY(!csr.waitForFinished());
+
+    // save a new contact
+    int originalCount = cm->contactIds().size();
+    QContact testContact;
+    QContactName nameDetail;
+    nameDetail.setFirstName("Test Contact");
+    testContact.saveDetail(&nameDetail);
+    QList<QContact> saveList;
+    saveList << testContact;
+    csr.setManager(cm.data());
+    QCOMPARE(csr.manager(), cm.data());
+    QVERIFY(!csr.isActive());
+    QVERIFY(!csr.isFinished());
+    QVERIFY(!csr.cancel());
+    QVERIFY(!csr.waitForFinished());
+    qRegisterMetaType<QContactSaveRequest*>("QContactSaveRequest*");
+    QThreadSignalSpy spy(&csr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    csr.setContact(testContact);
+    QCOMPARE(csr.contacts(), saveList);
+    QVERIFY(!csr.cancel()); // not started
+    QVERIFY(csr.start());
+
+    QVERIFY((csr.isActive() && csr.state() == QContactAbstractRequest::ActiveState) || csr.isFinished());
+    //QVERIFY(csr.isFinished() || !csr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(csr.waitForFinished());
+    QVERIFY(csr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QList<QContact> expected = csr.contacts();
+    QCOMPARE(expected.size(), 1);
+    QList<QContact> result;
+    result << cm->contact(expected.first().id().localId());
+    //some backends add extra fields, so this doesn't work:
+    //QCOMPARE(result, expected);
+    // XXX: really, we should use isSuperset() from tst_QContactManager, but this will do for now:
+    QVERIFY(result.first().detail<QContactName>() == nameDetail);
+    QCOMPARE(cm->contactIds().size(), originalCount + 1);
+
+    // update a previously saved contact
+    QContactPhoneNumber phn;
+    phn.setNumber("12345678");
+    testContact = result.first();
+    testContact.saveDetail(&phn);
+    saveList.clear();
+    saveList << testContact;
+    csr.setContacts(saveList);
+    QCOMPARE(csr.contacts(), saveList);
+    QVERIFY(!csr.cancel()); // not started
+    QVERIFY(csr.start());
+
+    QVERIFY((csr.isActive() && csr.state() == QContactAbstractRequest::ActiveState) || csr.isFinished());
+    //QVERIFY(csr.isFinished() || !csr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(csr.waitForFinished());
+
+    QVERIFY(csr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    expected = csr.contacts();
+    result.clear();
+    result << cm->contact(expected.first().id().localId());
+    //QVERIFY(compareContactLists(result, expected));
+
+    //here we can't compare the whole contact details, testContact would be updated by async call because we just use QThreadSignalSpy to receive signals.
+    //QVERIFY(containsIgnoringTimestamps(result, testContact));
+    // XXX: really, we should use isSuperset() from tst_QContactManager, but this will do for now:
+    QVERIFY(result.first().detail<QContactPhoneNumber>().number() == phn.number());
+    
+    QCOMPARE(cm->contactIds().size(), originalCount + 1);
+
+    // cancelling
+    QContact temp = testContact;
+    QContactUrl url;
+    url.setUrl("should not get saved");
+    temp.saveDetail(&url);
+    saveList.clear();
+    saveList << temp;
+    csr.setContacts(saveList);
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!csr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(csr.start());
+        if (!csr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            csr.waitForFinished();
+            saveList = csr.contacts();
+            if (cm->contactIds().size() > (originalCount + 1) && !cm->removeContact(saveList.at(0).localId())) {
+                QSKIP("Unable to remove saved contact to test cancellation of contact save request", SkipSingle);
+            }
+            saveList.clear();
+            saveList << temp;
+            csr.setContacts(saveList);
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(csr.waitForFinished());
+        QVERIFY(csr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        // verify that the changes were not saved
+        expected.clear();
+        QList<QContactLocalId> allContacts = cm->contactIds();
+        for (int i = 0; i < allContacts.size(); i++) {
+            expected.append(cm->contact(allContacts.at(i)));
+        }
+        QVERIFY(!expected.contains(temp));
+        QCOMPARE(cm->contactIds().size(), originalCount + 1);
+        break;
+    }
+    // restart, and wait for progress after cancel.
+
+    while (true) {
+        QVERIFY(!csr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(csr.start());
+        if (!csr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            csr.waitForFinished();
+            saveList = csr.contacts();
+            if (cm->contactIds().size() > (originalCount + 1) && !cm->removeContact(saveList.at(0).localId())) {
+                QSKIP("Unable to remove saved contact to test cancellation of contact save request", SkipSingle);
+            }
+            saveList.clear();
+            saveList << temp;
+            csr.setContacts(saveList);
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        csr.waitForFinished(); // now wait until finished (if it hasn't already).
+        QVERIFY(csr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        // verify that the changes were not saved
+        expected.clear();
+        QList<QContactLocalId> allContacts = cm->contactIds();
+        for (int i = 0; i < allContacts.size(); i++) {
+            expected.append(cm->contact(allContacts.at(i)));
+        }
+        QVERIFY(!expected.contains(temp));
+        QCOMPARE(cm->contactIds().size(), originalCount + 1);
+        break;
+    }
+}
+
+void tst_QContactAsync::definitionFetch()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    QContactDetailDefinitionFetchRequest dfr;
+    QVERIFY(dfr.type() == QContactAbstractRequest::DetailDefinitionFetchRequest);
+    QVERIFY(dfr.contactType() == QString(QLatin1String(QContactType::TypeContact))); // ensure ctor sets contact type correctly.
+    dfr.setContactType(QContactType::TypeContact);
+    QVERIFY(dfr.contactType() == QString(QLatin1String(QContactType::TypeContact)));
+
+    // initial state - not started, no manager.
+    QVERIFY(!dfr.isActive());
+    QVERIFY(!dfr.isFinished());
+    QVERIFY(!dfr.start());
+    QVERIFY(!dfr.cancel());
+    QVERIFY(!dfr.waitForFinished());
+
+    // "all definitions" retrieval
+    dfr.setManager(cm.data());
+    QCOMPARE(dfr.manager(), cm.data());
+    QVERIFY(!dfr.isActive());
+    QVERIFY(!dfr.isFinished());
+    QVERIFY(!dfr.cancel());
+    QVERIFY(!dfr.waitForFinished());
+    qRegisterMetaType<QContactDetailDefinitionFetchRequest*>("QContactDetailDefinitionFetchRequest*");
+    QThreadSignalSpy spy(&dfr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    dfr.setDefinitionNames(QStringList());
+    QVERIFY(!dfr.cancel()); // not started
+    QVERIFY(dfr.start());
+
+    QVERIFY((dfr.isActive() && dfr.state() == QContactAbstractRequest::ActiveState) || dfr.isFinished());
+    //QVERIFY(dfr.isFinished() || !dfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(dfr.waitForFinished());
+    QVERIFY(dfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QMap<QString, QContactDetailDefinition> defs = cm->detailDefinitions();
+    QMap<QString, QContactDetailDefinition> result = dfr.definitions();
+    QCOMPARE(defs, result);
+
+    // specific definition retrieval
+    QStringList specific;
+    specific << QContactUrl::DefinitionName;
+    dfr.setDefinitionName(QContactUrl::DefinitionName);
+    QVERIFY(dfr.definitionNames() == specific);
+    QVERIFY(!dfr.cancel()); // not started
+    QVERIFY(dfr.start());
+
+    QVERIFY((dfr.isActive() && dfr.state() == QContactAbstractRequest::ActiveState) || dfr.isFinished());
+    //QVERIFY(dfr.isFinished() || !dfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(dfr.waitForFinished());
+    QVERIFY(dfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    defs.clear();
+    defs.insert(QContactUrl::DefinitionName, cm->detailDefinition(QContactUrl::DefinitionName));
+    result = dfr.definitions();
+    QCOMPARE(defs, result);
+
+    // cancelling
+    dfr.setDefinitionNames(QStringList());
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!dfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(dfr.start());
+        if (!dfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            dfr.waitForFinished();
+            dfr.setDefinitionNames(QStringList());
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(dfr.waitForFinished());
+        QVERIFY(dfr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!dfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(dfr.start());
+        if (!dfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            dfr.waitForFinished();
+            dfr.setDefinitionNames(QStringList());
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        dfr.waitForFinished();
+        QVERIFY(dfr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        break;
+    }
+
+}
+
+void tst_QContactAsync::definitionRemove()
+{
+    QFETCH(QString, uri);
+
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    if (!cm->hasFeature(QContactManager::MutableDefinitions)) {
+       QSKIP("This contact manager does not support mutable definitions, can't remove a definition!", SkipSingle);
+    }
+    QContactDetailDefinitionRemoveRequest drr;
+    QVERIFY(drr.type() == QContactAbstractRequest::DetailDefinitionRemoveRequest);
+    QVERIFY(drr.contactType() == QString(QLatin1String(QContactType::TypeContact))); // ensure ctor sets contact type correctly.
+    drr.setContactType(QContactType::TypeContact);
+    drr.setDefinitionNames(QStringList());
+    QVERIFY(drr.contactType() == QString(QLatin1String(QContactType::TypeContact)));
+
+    // initial state - not started, no manager.
+    QVERIFY(!drr.isActive());
+    QVERIFY(!drr.isFinished());
+    QVERIFY(!drr.start());
+    QVERIFY(!drr.cancel());
+    QVERIFY(!drr.waitForFinished());
+
+    // specific group removal
+    int originalCount = cm->detailDefinitions().keys().size();
+    QStringList removeIds;
+    removeIds << cm->detailDefinitions().keys().first();
+    drr.setDefinitionName(cm->detailDefinitions().keys().first());
+    drr.setManager(cm.data());
+    QCOMPARE(drr.manager(), cm.data());
+    QVERIFY(!drr.isActive());
+    QVERIFY(!drr.isFinished());
+    QVERIFY(!drr.cancel());
+    QVERIFY(!drr.waitForFinished());
+    qRegisterMetaType<QContactDetailDefinitionRemoveRequest*>("QContactDetailDefinitionRemoveRequest*");
+    QThreadSignalSpy spy(&drr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    QVERIFY(drr.definitionNames() == removeIds);
+    QVERIFY(!drr.cancel()); // not started
+    QVERIFY(drr.start());
+
+    QVERIFY((drr.isActive() && drr.state() == QContactAbstractRequest::ActiveState) || drr.isFinished());
+    //QVERIFY(drr.isFinished() || !drr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(drr.waitForFinished());
+    QVERIFY(drr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QCOMPARE(cm->detailDefinitions().keys().size(), originalCount - 1);
+    cm->detailDefinition(removeIds.first()); // check that it has already been removed.
+    QCOMPARE(cm->error(), QContactManager::DoesNotExistError);
+
+    // remove (asynchronously) a nonexistent group - should fail.
+    drr.setDefinitionNames(removeIds);
+    QVERIFY(!drr.cancel()); // not started
+    QVERIFY(drr.start());
+
+    QVERIFY((drr.isActive() && drr.state() == QContactAbstractRequest::ActiveState) || drr.isFinished());
+    //QVERIFY(drr.isFinished() || !drr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(drr.waitForFinished());
+    QVERIFY(drr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QCOMPARE(cm->detailDefinitions().keys().size(), originalCount - 1); // hasn't changed
+    QCOMPARE(drr.error(), QContactManager::DoesNotExistError);
+
+    // remove with list containing one valid and one invalid id.
+    removeIds << cm->detailDefinitions().keys().first();
+    drr.setDefinitionNames(removeIds);
+    QVERIFY(!drr.cancel()); // not started
+    QVERIFY(drr.start());
+
+    QVERIFY((drr.isActive() && drr.state() == QContactAbstractRequest::ActiveState) || drr.isFinished());
+    //QVERIFY(drr.isFinished() || !drr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(drr.waitForFinished());
+    QVERIFY(drr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished signals
+    spy.clear();
+
+    QCOMPARE(cm->detailDefinitions().keys().size(), originalCount - 2); // only one more has been removed
+    QVERIFY(drr.errorMap().count() == 1);
+    QVERIFY(drr.errorMap().keys().contains(0));
+    QCOMPARE(drr.errorMap().value(0), QContactManager::DoesNotExistError);
+
+    // remove with empty list - nothing should happen.
+    removeIds.clear();
+    drr.setDefinitionNames(removeIds);
+    QVERIFY(!drr.cancel()); // not started
+    QVERIFY(drr.start());
+
+    QVERIFY(drr.isActive() || drr.isFinished());
+    //QVERIFY(drr.isFinished() || !drr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(drr.waitForFinished());
+
+    QVERIFY(drr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QCOMPARE(cm->detailDefinitions().keys().size(), originalCount - 2); // hasn't changed
+    QCOMPARE(drr.error(), QContactManager::NoError);  // no error but no effect.
+
+    // cancelling
+    removeIds.clear();
+    removeIds << cm->detailDefinitions().keys().first();
+    drr.setDefinitionNames(removeIds);
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!drr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(drr.start());
+        if (!drr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            drr.waitForFinished();
+            drr.setDefinitionNames(removeIds);
+
+            QCOMPARE(cm->detailDefinitions().keys().size(), originalCount - 3); // finished
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            // XXX should be readded
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(drr.waitForFinished());
+        QVERIFY(drr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        QCOMPARE(cm->detailDefinitions().keys().size(), originalCount - 2); // hasn't changed
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!drr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(drr.start());
+        if (!drr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            drr.waitForFinished();
+            drr.setDefinitionNames(removeIds);
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        drr.waitForFinished();
+        QVERIFY(drr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        QCOMPARE(cm->detailDefinitions().keys().size(), originalCount - 3); // hasn't changed
+        break;
+    }
+
+}
+
+void tst_QContactAsync::definitionSave()
+{
+    QFETCH(QString, uri);
+
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+
+    if (!cm->hasFeature(QContactManager::MutableDefinitions)) {
+
+       QSKIP("This contact manager does not support mutable definitions, can't save a definition!", SkipSingle);
+    }
+    
+    QContactDetailDefinitionSaveRequest dsr;
+    QVERIFY(dsr.type() == QContactAbstractRequest::DetailDefinitionSaveRequest);
+    QVERIFY(dsr.contactType() == QString(QLatin1String(QContactType::TypeContact))); // ensure ctor sets contact type correctly
+    dsr.setContactType(QContactType::TypeContact);
+    QVERIFY(dsr.contactType() == QString(QLatin1String(QContactType::TypeContact)));
+
+    // initial state - not started, no manager.
+    QVERIFY(!dsr.isActive());
+    QVERIFY(!dsr.isFinished());
+    QVERIFY(!dsr.start());
+    QVERIFY(!dsr.cancel());
+    QVERIFY(!dsr.waitForFinished());
+
+    // save a new detail definition
+    int originalCount = cm->detailDefinitions().keys().size();
+    QContactDetailDefinition testDef;
+    testDef.setName("TestDefinitionId");
+    QMap<QString, QContactDetailFieldDefinition> fields;
+    QContactDetailFieldDefinition f;
+    f.setDataType(QVariant::String);
+    fields.insert("TestDefinitionField", f);
+    testDef.setFields(fields);
+    QList<QContactDetailDefinition> saveList;
+    saveList << testDef;
+    dsr.setManager(cm.data());
+    QCOMPARE(dsr.manager(), cm.data());
+    QVERIFY(!dsr.isActive());
+    QVERIFY(!dsr.isFinished());
+    QVERIFY(!dsr.cancel());
+    QVERIFY(!dsr.waitForFinished());
+    qRegisterMetaType<QContactDetailDefinitionSaveRequest*>("QContactDetailDefinitionSaveRequest*");
+    QThreadSignalSpy spy(&dsr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    dsr.setDefinition(testDef);
+    QCOMPARE(dsr.definitions(), saveList);
+    QVERIFY(!dsr.cancel()); // not started
+    QVERIFY(dsr.start());
+
+    QVERIFY((dsr.isActive() && dsr.state() == QContactAbstractRequest::ActiveState) || dsr.isFinished());
+    //QVERIFY(dsr.isFinished() || !dsr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(dsr.waitForFinished());
+    QVERIFY(dsr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QList<QContactDetailDefinition> expected;
+    expected << cm->detailDefinition("TestDefinitionId");
+    QList<QContactDetailDefinition> result = dsr.definitions();
+    QCOMPARE(expected, result);
+    QVERIFY(expected.contains(testDef));
+    QCOMPARE(cm->detailDefinitions().values().size(), originalCount + 1);
+
+    // update a previously saved group
+    fields.insert("TestDefinitionFieldTwo", f);
+    testDef.setFields(fields);
+    saveList.clear();
+    saveList << testDef;
+    dsr.setDefinitions(saveList);
+    QCOMPARE(dsr.definitions(), saveList);
+    QVERIFY(!dsr.cancel()); // not started
+    QVERIFY(dsr.start());
+
+    QVERIFY((dsr.isActive() && dsr.state() == QContactAbstractRequest::ActiveState) || dsr.isFinished());
+    //QVERIFY(dsr.isFinished() || !dsr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(dsr.waitForFinished());
+    QVERIFY(dsr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    expected.clear();
+    expected << cm->detailDefinition("TestDefinitionId");
+    result = dsr.definitions();
+    QCOMPARE(expected, result);
+    QVERIFY(expected.contains(testDef));
+    QCOMPARE(cm->detailDefinitions().values().size(), originalCount + 1);
+
+    // cancelling
+    fields.insert("TestDefinitionFieldThree - shouldn't get saved", f);
+    testDef.setFields(fields);
+    saveList.clear();
+    saveList << testDef;
+    dsr.setDefinitions(saveList);
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!dsr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(dsr.start());
+        if (!dsr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            dsr.waitForFinished();
+            saveList.clear();
+            saveList << testDef;
+            dsr.setDefinitions(saveList);
+            cm->removeDetailDefinition(testDef.name());
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(dsr.waitForFinished());
+        QVERIFY(dsr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        // verify that the changes were not saved
+        QList<QContactDetailDefinition> allDefs = cm->detailDefinitions().values();
+        QVERIFY(!allDefs.contains(testDef));
+        QCOMPARE(cm->detailDefinitions().values().size(), originalCount + 1);
+
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!dsr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(dsr.start());
+        if (!dsr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            dsr.waitForFinished();
+            saveList.clear();
+            saveList << testDef;
+            dsr.setDefinitions(saveList);
+            cm->removeDetailDefinition(testDef.name());
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        dsr.waitForFinished();
+        QVERIFY(dsr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        // verify that the changes were not saved
+        QList<QContactDetailDefinition> allDefs = cm->detailDefinitions().values();
+        QVERIFY(!allDefs.contains(testDef));
+        QCOMPARE(cm->detailDefinitions().values().size(), originalCount + 1);
+
+        break;
+    }
+
+}
+
+void tst_QContactAsync::relationshipFetch()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+
+    if (!cm->hasFeature(QContactManager::Relationships)) {
+       QSKIP("This contact manager does not support relationships!", SkipSingle);
+    }
+    
+    if (cm->managerName() == "symbian") {
+        QSKIP("This contact manager does not support the required relationship types for this test to pass!", SkipSingle);
+    }
+    
+    QContactRelationshipFetchRequest rfr;
+    QVERIFY(rfr.type() == QContactAbstractRequest::RelationshipFetchRequest);
+
+    // initial state - not started, no manager.
+    QVERIFY(!rfr.isActive());
+    QVERIFY(!rfr.isFinished());
+    QVERIFY(!rfr.start());
+    QVERIFY(!rfr.cancel());
+    QVERIFY(!rfr.waitForFinished());
+
+    // "all relationships" retrieval
+    rfr.setManager(cm.data());
+    QCOMPARE(rfr.manager(), cm.data());
+    QVERIFY(!rfr.isActive());
+    QVERIFY(!rfr.isFinished());
+    QVERIFY(!rfr.cancel());
+    QVERIFY(!rfr.waitForFinished());
+    qRegisterMetaType<QContactRelationshipFetchRequest*>("QContactRelationshipFetchRequest*");
+    QThreadSignalSpy spy(&rfr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    QVERIFY(!rfr.cancel()); // not started
+    QVERIFY(rfr.start());
+
+    QVERIFY((rfr.isActive() && rfr.state() == QContactAbstractRequest::ActiveState) || rfr.isFinished());
+    //QVERIFY(rfr.isFinished() || !rfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rfr.waitForFinished());
+
+    QVERIFY(rfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QList<QContactRelationship> rels = cm->relationships();
+    QList<QContactRelationship> result = rfr.relationships();
+    QCOMPARE(rels, result);
+
+    // specific relationship type retrieval
+    rfr.setRelationshipType(QContactRelationship::HasManager);
+    QVERIFY(!rfr.cancel()); // not started
+    QVERIFY(rfr.start());
+
+    QVERIFY((rfr.isActive() && rfr.state() == QContactAbstractRequest::ActiveState) || rfr.isFinished());
+    //QVERIFY(rfr.isFinished() || !rfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rfr.waitForFinished());
+    QVERIFY(rfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    rels = cm->relationships(QContactRelationship::HasManager);
+    result = rfr.relationships();
+    QCOMPARE(rels, result);
+
+    // specific source contact retrieval
+    rfr.setRelationshipType(QString());
+    QList<QContactLocalId> contacts = cm->contactIds();
+    QContactId aId;
+    foreach (const QContactLocalId& currId, contacts) {
+        QContact curr = cm->contact(currId);
+        if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Aaron")) {
+            aId = curr.id();
+            break;
+        }
+    }
+    rfr.setFirst(aId);
+    QVERIFY(!rfr.cancel()); // not started
+    QVERIFY(rfr.start());
+
+    QVERIFY((rfr.isActive() && rfr.state() == QContactAbstractRequest::ActiveState) || rfr.isFinished());
+    //QVERIFY(rfr.isFinished() || !rfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rfr.waitForFinished());
+    QVERIFY(rfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    rels = cm->relationships(aId, QContactRelationship::First);
+    result = rfr.relationships();
+    QCOMPARE(rels, result);
+
+    // specific participant retrieval #1 - destination participant
+    rfr.setFirst(QContactId());
+    contacts = cm->contactIds();
+    QContactId bId;
+    foreach (const QContactLocalId& currId, contacts) {
+        QContact curr = cm->contact(currId);
+        if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Bob")) {
+            bId = curr.id();
+            break;
+        }
+    }
+    rfr.setSecond(bId);
+    
+    QVERIFY(!rfr.cancel()); // not started
+    QVERIFY(rfr.start());
+
+    QVERIFY((rfr.isActive() && rfr.state() == QContactAbstractRequest::ActiveState) || rfr.isFinished());
+    //QVERIFY(rfr.isFinished() || !rfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rfr.waitForFinished());
+    QVERIFY(rfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    // retrieve rels where second = id of B, and ensure that we get the same results
+    rels = cm->relationships(bId, QContactRelationship::Second);
+    result = rfr.relationships();
+    QCOMPARE(rels, result);
+
+    // specific participant retrieval #2 - source participant
+    rfr.setFirst(QContactId());
+    contacts = cm->contactIds();
+    QContactId cId;
+    foreach (const QContactLocalId& currId, contacts) {
+        QContact curr = cm->contact(currId);
+        if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Borris")) {
+            cId = curr.id();
+            break;
+        }
+    }
+    rfr.setSecond(cId);
+    
+    QVERIFY(!rfr.cancel()); // not started
+    QVERIFY(rfr.start());
+
+    QVERIFY((rfr.isActive() && rfr.state() == QContactAbstractRequest::ActiveState) || rfr.isFinished());
+    //QVERIFY(rfr.isFinished() || !rfr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rfr.waitForFinished());
+    QVERIFY(rfr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    // retrieve rels where first = id of C and compare the results
+    rfr.setFirst(cId);
+    rfr.setSecond(QContactId());
+    QVERIFY(rfr.start());
+    QVERIFY(rfr.waitForFinished());
+    result = rfr.relationships();
+    rels = cm->relationships(cId, QContactRelationship::First);
+    QCOMPARE(rels, result);
+
+    // cancelling
+    rfr.setRelationshipType(QString());
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!rfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(rfr.start());
+        if (!rfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            rfr.waitForFinished();
+            rfr.setRelationshipType(QString());
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(rfr.waitForFinished());
+        QVERIFY(rfr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!rfr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(rfr.start());
+        if (!rfr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            rfr.waitForFinished();
+            rfr.setRelationshipType(QString());
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        rfr.waitForFinished();
+        QVERIFY(rfr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+        break;
+    }
+}
+
+void tst_QContactAsync::relationshipRemove()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    
+    if (!cm->hasFeature(QContactManager::Relationships)) {
+       QSKIP("This contact manager does not support relationships!", SkipSingle);
+    }
+    
+    if (cm->managerName() == "symbian") {
+        QSKIP("This contact manager does not support the required relationship types for this test to pass!", SkipSingle);
+    }
+    
+    QContactRelationshipRemoveRequest rrr;
+    QVERIFY(rrr.type() == QContactAbstractRequest::RelationshipRemoveRequest);
+
+    // initial state - not started, no manager.
+    QVERIFY(!rrr.isActive());
+    QVERIFY(!rrr.isFinished());
+    QVERIFY(!rrr.start());
+    QVERIFY(!rrr.cancel());
+    QVERIFY(!rrr.waitForFinished());
+
+    QList<QContactLocalId> contacts = cm->contactIds();
+    QContactId aId, bId, cId;
+    foreach (const QContactLocalId& currId, contacts) {
+        QContact curr = cm->contact(currId);
+        if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Aaron")) {
+            aId = curr.id();
+            continue;
+        }
+        if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Bob")) {
+            bId = curr.id();
+            continue;
+        }
+        if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Borris")) {
+            cId = curr.id();
+            continue;
+        }
+    }
+
+    // specific source, destination and type removal
+    QList<QContactRelationship> relationships;
+    QContactRelationship r;
+    r.setFirst(aId);
+    r.setSecond(cId);
+    r.setRelationshipType(QContactRelationship::HasAssistant);
+    relationships.push_back(r);
+
+    rrr.setRelationships(relationships);
+    rrr.setManager(cm.data());
+    qRegisterMetaType<QContactRelationshipRemoveRequest*>("QContactRelationshipRemoveRequest*");
+    QThreadSignalSpy spy(&rrr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    QCOMPARE(rrr.manager(), cm.data());
+    QVERIFY(!rrr.isActive());
+    QVERIFY(!rrr.isFinished());
+    QVERIFY(!rrr.cancel());
+    QVERIFY(!rrr.waitForFinished());
+    
+    QVERIFY(!rrr.cancel()); // not started
+    QVERIFY(rrr.start());
+
+    QVERIFY((rrr.isActive() && rrr.state() == QContactAbstractRequest::ActiveState) || rrr.isFinished());
+    //QVERIFY(rrr.isFinished() || !rrr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rrr.waitForFinished());
+    QVERIFY(rrr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+    QCOMPARE(cm->relationships(QContactRelationship::HasAssistant, cId, QContactRelationship::Second).size(), 1);
+
+    // remove (asynchronously) a nonexistent relationship - should fail.
+    r.setFirst(cId);
+    r.setSecond(aId);
+    r.setRelationshipType(QContactRelationship::HasManager);
+    relationships.clear();
+    relationships.push_back(r);
+    rrr.setRelationship(r);
+    QVERIFY(rrr.relationships() == relationships);
+    rrr.setManager(cm.data());
+    QVERIFY(!rrr.cancel()); // not started
+    QVERIFY(rrr.start());
+
+    QVERIFY((rrr.isActive() && rrr.state() == QContactAbstractRequest::ActiveState) || rrr.isFinished());
+    //QVERIFY(rrr.isFinished() || !rrr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rrr.waitForFinished());
+    QVERIFY(rrr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QCOMPARE(cm->relationships(QContactRelationship::HasManager, cId, QContactRelationship::First).size(), 0);
+//    QCOMPARE(rrr.error(), QContactManager::DoesNotExistError);
+
+    // cancelling
+    r.setFirst(cId);
+    r.setSecond(QContactId());
+    relationships.clear();
+    relationships.push_back(r);
+    
+
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!rrr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(rrr.start());
+        if (!rrr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            rrr.waitForFinished();
+            rrr.setRelationships(relationships);
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(rrr.waitForFinished());
+        QVERIFY(rrr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        QVERIFY(cm->relationships(cId).size() != 0); // didn't remove them.
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!rrr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(rrr.start());
+        if (!rrr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            rrr.waitForFinished();
+            rrr.setRelationships(relationships);
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        rrr.waitForFinished();
+        QVERIFY(rrr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        QVERIFY(cm->relationships(cId).size() != 0); // didn't remove them.
+        break;
+    }
+}
+
+void tst_QContactAsync::relationshipSave()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    
+    if (!cm->hasFeature(QContactManager::Relationships)) {
+       QSKIP("This contact manager does not support relationships!", SkipSingle);
+    }
+    
+    if (cm->managerName() == "symbian") {
+        QSKIP("This contact manager does not support the required relationship types for this test to pass!", SkipSingle);
+    }    
+    
+    QContactRelationshipSaveRequest rsr;
+    QVERIFY(rsr.type() == QContactAbstractRequest::RelationshipSaveRequest);
+
+    // initial state - not started, no manager.
+    QVERIFY(!rsr.isActive());
+    QVERIFY(!rsr.isFinished());
+    QVERIFY(!rsr.start());
+    QVERIFY(!rsr.cancel());
+    QVERIFY(!rsr.waitForFinished());
+
+    QList<QContactLocalId> contacts = cm->contactIds();
+    QContactId cId, aId, bId;
+    foreach (const QContactLocalId& currId, contacts) {
+        QContact curr = cm->contact(currId);
+        if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Borris")) {
+            cId = curr.id();
+        } else if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Bob")) {
+            bId = curr.id();
+        } else if (curr.detail(QContactName::DefinitionName).value(QContactName::FieldFirstName) == QString("Aaron")) {
+            aId = curr.id();
+        }
+    }
+
+    // save a new relationship
+    int originalCount = cm->relationships(aId).size();
+    QContactRelationship testRel;
+    testRel.setFirst(aId);
+    testRel.setRelationshipType(QContactRelationship::HasSpouse);
+    testRel.setSecond(bId);
+    QList<QContactRelationship> saveList;
+    saveList << testRel;
+    rsr.setManager(cm.data());
+    QCOMPARE(rsr.manager(), cm.data());
+    QVERIFY(!rsr.isActive());
+    QVERIFY(!rsr.isFinished());
+    QVERIFY(!rsr.cancel());
+    QVERIFY(!rsr.waitForFinished());
+    qRegisterMetaType<QContactRelationshipSaveRequest*>("QContactRelationshipSaveRequest*");
+    QThreadSignalSpy spy(&rsr, SIGNAL(stateChanged(QContactAbstractRequest::State)));
+    rsr.setRelationship(testRel);
+    QCOMPARE(rsr.relationships(), saveList);
+    QVERIFY(!rsr.cancel()); // not started
+    QVERIFY(rsr.start());
+
+    QVERIFY((rsr.isActive() && rsr.state() == QContactAbstractRequest::ActiveState) || rsr.isFinished());
+    //QVERIFY(rsr.isFinished() || !rsr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rsr.waitForFinished());
+    QVERIFY(rsr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    QList<QContactRelationship> expected = cm->relationships(QContactRelationship::HasSpouse, aId, QContactRelationship::First);
+    QList<QContactRelationship> result = rsr.relationships();
+    QCOMPARE(expected, result);
+    QVERIFY(result.contains(testRel));
+    QCOMPARE(cm->relationships(aId).size(), originalCount + 1); // should be one extra
+
+    // save a new relationship
+    testRel.setSecond(cId);
+    saveList.clear();
+    saveList << testRel;
+    rsr.setRelationships(saveList);
+    QCOMPARE(rsr.relationships(), saveList);
+    QVERIFY(!rsr.cancel()); // not started
+    QVERIFY(rsr.start());
+
+    QVERIFY((rsr.isActive() && rsr.state() == QContactAbstractRequest::ActiveState) || rsr.isFinished());
+    //QVERIFY(rsr.isFinished() || !rsr.start());  // already started. // thread scheduling means this is untestable
+    QVERIFY(rsr.waitForFinished());
+    QVERIFY(rsr.isFinished());
+    QVERIFY(spy.count() >= 1); // active + finished progress signals
+    spy.clear();
+
+    expected.clear();
+    expected = cm->relationships(QContactRelationship::HasSpouse, aId, QContactRelationship::First);
+    result = rsr.relationships();
+    QCOMPARE(result, QList<QContactRelationship>() << testRel);
+    QVERIFY(expected.contains(testRel));
+    QCOMPARE(cm->relationships(aId).size(), originalCount + 2); // should now be two extra
+
+    // cancelling
+    testRel.setSecond(aId); // shouldn't get saved (circular anyway)
+    saveList.clear();
+    saveList << testRel;
+    rsr.setRelationships(saveList);
+    int bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT; // attempt to cancel 40 times.  If it doesn't work due to threading, bail out.
+    while (true) {
+        QVERIFY(!rsr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(rsr.start());
+        if (!rsr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            rsr.waitForFinished();
+            saveList.clear();
+            saveList << testRel;
+            rsr.setRelationships(saveList);
+            cm->removeRelationship(testRel); // probably shouldn't have been saved anyway (circular)
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+
+        // if we get here, then we are cancelling the request.
+        QVERIFY(rsr.waitForFinished());
+        QVERIFY(rsr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        // verify that the changes were not saved
+        QList<QContactRelationship> aRels = cm->relationships(aId, QContactRelationship::First);
+        QVERIFY(!aRels.contains(testRel));
+        QCOMPARE(cm->relationships(aId).size(), originalCount + 2); // should still only be two extra
+
+        break;
+    }
+
+    // restart, and wait for progress after cancel.
+    while (true) {
+        QVERIFY(!rsr.cancel()); // not started
+        FILL_QUEUE_WITH_FETCH_REQUESTS();
+        QVERIFY(rsr.start());
+        if (!rsr.cancel()) {
+            // due to thread scheduling, async cancel might be attempted
+            // after the request has already finished.. so loop and try again.
+            rsr.waitForFinished();
+            saveList.clear();
+            saveList << testRel;
+            rsr.setRelationships(saveList);
+            cm->removeRelationship(testRel); // probably shouldn't have been saved anyway (circular)
+            bailoutCount -= 1;
+            if (!bailoutCount) {
+//                qWarning("Unable to test cancelling due to thread scheduling!");
+                bailoutCount = MAX_OPTIMISTIC_SCHEDULING_LIMIT;
+                break;
+            }
+            spy.clear();
+            continue;
+        }
+        rsr.waitForFinished();
+        QVERIFY(rsr.isCanceled());
+        QVERIFY(spy.count() >= 1); // active + cancelled progress signals
+        spy.clear();
+
+        // verify that the changes were not saved
+        QList<QContactRelationship> aRels = cm->relationships(aId, QContactRelationship::First);
+        QVERIFY(!aRels.contains(testRel));
+        QCOMPARE(cm->relationships(aId).size(), originalCount + 2); // should still only be two extra
+
+        break;
+    }
+}
+
+void tst_QContactAsync::maliciousManager()
+{
+    // use the invalid manager: passes all requests through to base class
+    QContactManager cm("invalid");
+    QContactFilter fil; // matches everything
+    QContactFetchRequest cfr;
+    cfr.setFilter(fil);
+    cfr.setManager(&cm);
+    QVERIFY(!cfr.start());
+    QVERIFY(!cfr.cancel());
+    QVERIFY(!cfr.waitForFinished());
+    QVERIFY(!cfr.start());
+
+    // ensure that the base class implementation of requestDestroyed() is called
+    QContactFetchRequest *destroyedRequest = new QContactFetchRequest;
+    destroyedRequest->setManager(&cm);
+    destroyedRequest->setFilter(fil);
+    QVERIFY(!destroyedRequest->start());
+    delete destroyedRequest;
+
+    // now use a malicious manager that deliberately calls
+    // things in a different thread
+    QContactManager mcm("maliciousplugin");
+    QCOMPARE(mcm.managerName(), QString("maliciousplugin"));
+    QList<QContact> emptyCList;
+    QList<QContactLocalId> emptyIList;
+    QList<QContactDetailDefinition> emptyDList;
+    QStringList emptyDNList;
+    QMap<QString, QContactDetailDefinition> emptyDMap;
+    cfr.setFilter(fil);
+    cfr.setManager(&mcm);
+    QVERIFY(cfr.start());
+
+    QContactLocalIdFetchRequest cifr;
+    cifr.setFilter(fil);
+    cifr.setManager(&mcm);
+    QVERIFY(cifr.start());
+
+    QContactRemoveRequest crr;
+    crr.setContactIds(mcm.contactIds(fil));
+    crr.setManager(&mcm);
+    QVERIFY(crr.start());
+
+    QContactSaveRequest csr;
+    csr.setContacts(emptyCList);
+    csr.setManager(&mcm);
+    QVERIFY(csr.start());
+
+    {
+    QContactDetailDefinitionFetchRequest dfr;
+    dfr.setDefinitionNames(emptyDNList);
+    dfr.setManager(&mcm);
+    QVERIFY(dfr.start());
+    }
+
+    {
+    QContactDetailDefinitionFetchRequest dfr;
+    dfr.setDefinitionNames(emptyDNList);
+    dfr.setManager(&mcm);
+    }
+
+    QContactDetailDefinitionSaveRequest dsr;
+    dsr.setDefinitions(emptyDList);
+    dsr.setManager(&mcm);
+    QVERIFY(dsr.start());
+
+    QContactDetailDefinitionRemoveRequest drr;
+    drr.setDefinitionNames(emptyDNList);
+    drr.setManager(&mcm);
+    QVERIFY(drr.start());
+}
+
+void tst_QContactAsync::testQuickDestruction()
+{
+    QFETCH(QString, uri);
+
+    // in this test, we create a manager, fire off a request, and delete the manager, all in quick succession
+    // this is to test for segfaults etc.
+    for (int i = 0; i < 10; i++) {
+        QContactFetchRequest cfr;
+        QContactManager *cm = prepareModel(uri);
+        cfr.setManager(cm);
+        cfr.start();
+        delete cm;
+    }
+    // in this test, we create a manager, fire off a request, delete the request, then delete the manager, all in quick succession
+    // this is to test for segfaults, etc.
+    for (int i = 0; i < 10; i++) {
+        QContactFetchRequest *cfr = new QContactFetchRequest;
+        QContactManager *cm = prepareModel(uri);
+        cfr->setManager(cm);
+        cfr->start();
+        delete cfr;
+        delete cm;
+    }
+    // in this test, we create a manager, fire off a request, delete the manager, then delete the request, all in quick succession
+    // this is to test for segfaults, etc.
+    for (int i = 0; i < 10; i++) {
+        QContactFetchRequest *cfr = new QContactFetchRequest;
+        QContactManager *cm = prepareModel(uri);
+        cfr->setManager(cm);
+        cfr->start();
+        delete cm;
+        delete cfr;
+    }
+    // in this test, we create a manager, fire off a request, and delete the request, all in quick succession
+    // this is to test for segfaults, etc.
+    QContactManager *cm = prepareModel(uri);
+    for (int i = 0; i < 10; i++) {
+        QContactFetchRequest *cfr = new QContactFetchRequest;
+        cfr->setManager(cm);
+        cfr->start();
+        delete cfr;
+    }
+    delete cm;
+}
+
+void tst_QContactAsync::threadDelivery()
+{
+    QFETCH(QString, uri);
+    QScopedPointer<QContactManager> cm(prepareModel(uri));
+    m_mainThreadId = cm->thread()->currentThreadId();
+    m_resultsAvailableSlotThreadId = m_mainThreadId;
+
+    // now perform a fetch request and check that the progress is delivered to the correct thread.
+    QContactFetchRequest *req = new QContactFetchRequest;
+    req->setManager(cm.data());
+    connect(req, SIGNAL(resultsAvailable()), this, SLOT(resultsAvailableReceived()));
+    req->start();
+
+    int totalWaitTime = 0;
+    QTest::qWait(1); // force it to process events at least once.
+    while (req->state() != QContactAbstractRequest::FinishedState) {
+        // ensure that the progress signal was delivered to the main thread.
+        QCOMPARE(m_mainThreadId, m_resultsAvailableSlotThreadId);
+
+        QTest::qWait(5); // spin until done
+        totalWaitTime += 5;
+
+        // break after 30 seconds.
+        if (totalWaitTime > 30000) {
+            delete req;
+            QSKIP("Asynchronous request not complete after 30 seconds!", SkipSingle);
+        }
+    }
+
+    // ensure that the progress signal was delivered to the main thread.
+    QCOMPARE(m_mainThreadId, m_resultsAvailableSlotThreadId);
+    delete req;
+}
+
+void tst_QContactAsync::resultsAvailableReceived()
+{
+    QContactFetchRequest *req = qobject_cast<QContactFetchRequest *>(QObject::sender());
+    if (req)
+        m_resultsAvailableSlotThreadId = req->thread()->currentThreadId();
+    else
+        qWarning() << "resultsAvailableReceived() : request deleted; unable to set thread id!";
+}
+
+void tst_QContactAsync::addManagers(QStringList stringlist)
+{
+    QTest::addColumn<QString>("uri");
+
+    // retrieve the list of available managers
+    QStringList managers = QContactManager::availableManagers();
+
+    // remove ones that we know will not pass
+    if (!stringlist.contains("invalid"))
+        managers.removeAll("invalid");
+    if (!stringlist.contains("maliciousplugin"))
+        managers.removeAll("maliciousplugin");
+    if (!stringlist.contains("testdummy"))
+        managers.removeAll("testdummy");
+    if (!stringlist.contains("symbiansim"))
+        managers.removeAll("symbiansim"); // SIM backend does not support all the required details for tests to pass.
+
+    foreach(QString mgr, managers) {
+        QMap<QString, QString> params;
+        QTest::newRow(QString("mgr='%1'").arg(mgr).toLatin1().constData()) << QContactManager::buildUri(mgr, params);
+        if (mgr == "memory") {
+            params.insert("id", "tst_QContactManager");
+            QTest::newRow(QString("mgr='%1', params").arg(mgr).toLatin1().constData()) << QContactManager::buildUri(mgr, params);
+        }
+    }
+}
+
+QContactManager* tst_QContactAsync::prepareModel(const QString& managerUri)
+{
+    QContactManager* cm = QContactManager::fromUri(managerUri);
+
+    // XXX TODO: ensure that this is the case:
+    // there should be no contacts in the database.
+    QList<QContactLocalId> toRemove = cm->contactIds();
+    foreach (const QContactLocalId& removeId, toRemove)
+        cm->removeContact(removeId);
+
+    QContact a, b, c;
+    QContactName aNameDetail;
+    aNameDetail.setFirstName("Aaron");
+    aNameDetail.setLastName("Aaronson");
+    a.saveDetail(&aNameDetail);
+    QContactName bNameDetail;
+    bNameDetail.setFirstName("Bob");
+    bNameDetail.setLastName("Aaronsen");
+    b.saveDetail(&bNameDetail);
+    QContactName cNameDetail;
+    cNameDetail.setFirstName("Borris");
+    cNameDetail.setLastName("Aaronsun");
+    c.saveDetail(&cNameDetail);
+
+    QContactPhoneNumber phn;
+    phn.setNumber("0123");
+    c.saveDetail(&phn);
+    phn.setNumber("3456");
+    b.saveDetail(&phn);
+    phn.setNumber("6789");
+    a.saveDetail(&phn);
+
+    QContactUrl url;
+    url.setUrl("http://test.nokia.com");
+    a.saveDetail(&url);
+
+    cm->saveContact(&a);
+    cm->saveContact(&b);
+    cm->saveContact(&c);
+    
+    if (!cm->hasFeature(QContactManager::Relationships)) {
+        return cm;
+    }
+    
+    if (cm->managerName() == "symbian") {
+        // Symbian backend does not support other relationships than HasMember (which is same as groups)
+        return cm;
+    }
+
+    QContactRelationship arb;
+    arb.setFirst(a.id());
+    arb.setSecond(b.id());
+    arb.setRelationshipType(QContactRelationship::HasManager);
+    cm->saveRelationship(&arb);
+
+    QContactRelationship brc;
+    brc.setFirst(b.id());
+    brc.setSecond(c.id());
+    brc.setRelationshipType(QContactRelationship::HasAssistant);
+    cm->saveRelationship(&brc);
+
+    QContactRelationship cra;
+    cra.setFirst(c.id());
+    cra.setSecond(a.id());
+    cra.setRelationshipType(QContactRelationship::HasSpouse);
+    cm->saveRelationship(&cra);
+
+    QContactRelationship arc;
+    arc.setFirst(a.id());
+    arc.setSecond(c.id());
+    arc.setRelationshipType(QContactRelationship::HasAssistant);
+    cm->saveRelationship(&arc);
+
+    QContactRelationship crb;
+    crb.setFirst(c.id());
+    crb.setSecond(b.id());
+    crb.setRelationshipType(QContactRelationship::IsSameAs);
+    cm->saveRelationship(&crb);
+
+    return cm;
+
+    // TODO: cleanup once test is complete
+}
+
+QTEST_MAIN(tst_QContactAsync)
+#include "tst_qcontactasync.moc"