qtmobility/tests/auto/qversitreader/tst_qversitreader.cpp
author Dremov Kirill (Nokia-D-MSW/Tampere) <kirill.dremov@nokia.com>
Thu, 27 May 2010 13:42:11 +0300
changeset 8 71781823f776
parent 5 453da2cfceef
child 11 06b8e2af4411
permissions -rw-r--r--
Revision: 201019 Kit: 2010121

/****************************************************************************
**
** Copyright (C) 2009 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 "tst_qversitreader.h"
#include "qversitreader.h"
#include "qversitproperty.h"
#ifdef QT_BUILD_INTERNAL
#include "qversitreader_p.h"
#include "versitutils_p.h"
#endif
#include <QtTest/QtTest>
#include <QSignalSpy>

// Copied from tst_qcontactmanager.cpp
// Waits until __expr is true and fails if it doesn't happen within 5s.
#ifndef QTRY_VERIFY
#define QTRY_VERIFY(__expr) \
        do { \
        const int __step = 50; \
        const int __timeout = 5000; \
        if (!(__expr)) { \
            QTest::qWait(0); \
        } \
        for (int __i = 0; __i < __timeout && !(__expr); __i+=__step) { \
            QTest::qWait(__step); \
        } \
        QVERIFY(__expr); \
    } while(0)
#endif

// This says "NOKIA" in Katakana encoded with UTF-8
const QByteArray KATAKANA_NOKIA("\xe3\x83\x8e\xe3\x82\xad\xe3\x82\xa2");

Q_DECLARE_METATYPE(QVersitDocument::VersitType);
Q_DECLARE_METATYPE(QVersitProperty);

QTM_USE_NAMESPACE

void tst_QVersitReader::init()
{
    mInputDevice = new QBuffer;
    mInputDevice->open(QBuffer::ReadWrite);
    mReader = new QVersitReader;
#ifdef QT_BUILD_INTERNAL    
    mReaderPrivate = new QVersitReaderPrivate;
#endif
    mSignalCatcher = new SignalCatcher;
    connect(mReader, SIGNAL(stateChanged(QVersitReader::State)),
            mSignalCatcher, SLOT(stateChanged(QVersitReader::State)));
    connect(mReader, SIGNAL(resultsAvailable()),
            mSignalCatcher, SLOT(resultsAvailable()));
    mAsciiCodec = QTextCodec::codecForName("ISO 8859-1");
}

void tst_QVersitReader::cleanup()
{
#ifdef QT_BUILD_INTERNAL
    delete mReaderPrivate;
#endif
    delete mReader;
    delete mInputDevice;
    delete mSignalCatcher;
}

void tst_QVersitReader::testDevice()
{
    // No device
    QVERIFY(mReader->device() == NULL);

    // Device has been set
    mReader->setDevice(mInputDevice);
    QVERIFY(mReader->device() == mInputDevice);

    delete mInputDevice;
    QVERIFY(mReader->device() == NULL);

    mInputDevice = new QBuffer;
    mInputDevice->open(QBuffer::ReadWrite);

    QVERIFY(mReader->device() == NULL);
    mReader->setDevice(mInputDevice);
    QVERIFY(mReader->device() == mInputDevice);
}

void tst_QVersitReader::testDefaultCodec()
{
    QVERIFY(mReader->defaultCodec() == QTextCodec::codecForName("UTF-8"));
    mReader->setDefaultCodec(QTextCodec::codecForName("UTF-16BE"));
    QVERIFY(mReader->defaultCodec() == QTextCodec::codecForName("UTF-16BE"));
}


void tst_QVersitReader::testReading()
{
    // No I/O device set
    QVERIFY(!mReader->startReading());
    QCOMPARE(mReader->error(), QVersitReader::IOError);

    // Device set, no data
    mReader->setDevice(mInputDevice);
    mInputDevice->open(QBuffer::ReadOnly);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    QList<QVersitDocument> results(mReader->results());
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    QCOMPARE(mReader->error(), QVersitReader::NoError);
    QCOMPARE(results.count(),0);

    // Device set, one document
    const QByteArray& oneDocument =
        "BEGIN:VCARD\r\nVERSION:2.1\r\nFN:John\r\nEND:VCARD\r\n";
    mInputDevice->close();
    mInputDevice->setData(oneDocument);
    mInputDevice->open(QBuffer::ReadOnly);
    mInputDevice->seek(0);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    results = mReader->results();
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    QCOMPARE(mReader->error(), QVersitReader::NoError);
    QCOMPARE(results.count(),1);

    // Wide charset with no byte-order mark
    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    QTextCodec::ConverterState converterState(QTextCodec::IgnoreHeader);
    QString document = QString::fromAscii("BEGIN:VCARD\r\nVERSION:2.1\r\nFN:John\r\nEND:VCARD\r\n");
    const QByteArray& wideDocument =
        codec->fromUnicode(document.data(), document.length(), &converterState);
    mInputDevice->close();
    mInputDevice->setData(wideDocument);
    mInputDevice->open(QBuffer::ReadOnly);
    mInputDevice->seek(0);
    mReader->setDefaultCodec(codec);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    results = mReader->results();
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    QCOMPARE(mReader->error(), QVersitReader::NoError);
    QCOMPARE(mReader->results().count(),1);
    mReader->setDefaultCodec(NULL);

    // Two documents
    const QByteArray& twoDocuments =
        " \r\n BEGIN:VCARD\r\nFN:Jenny\r\nEND:VCARD\r\nBEGIN:VCARD\r\nFN:Jake\r\nEND:VCARD\r\n";
    mInputDevice->close();
    mInputDevice->setData(twoDocuments);
    mInputDevice->open(QBuffer::ReadOnly);
    mInputDevice->seek(0);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    results = mReader->results();
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    QCOMPARE(mReader->error(), QVersitReader::NoError);
    QCOMPARE(results.count(),2);

    // Erroneous document (missing property name)
    mInputDevice->close();
    mInputDevice->setData(QByteArray(
            "BEGIN:VCARD\r\nFN:Jenny\r\n;Jenny;;;\r\nEND:VCARD\r\n"
            "BEGIN:VCARD\r\nFN:Jake\r\nEND:VCARD\r\n"));
    mInputDevice->open(QBuffer::ReadOnly);
    mInputDevice->seek(0);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    results = mReader->results();
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    QCOMPARE(mReader->error(), QVersitReader::ParseError);
    QCOMPARE(results.count(), 1);

    // Valid documents and a grouped document between them
    const QByteArray& validDocumentsAndGroupedDocument =
"BEGIN:VCARD\r\nFN:Jenny\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\n\
X-GROUPING:pub gang\r\n\
BEGIN:VCARD\r\nFN:Jeremy\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\nFN:Jeffery\r\nEND:VCARD\r\n\
END:VCARD\r\n\
BEGIN:VCARD\r\nFN:Jake\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\nFN:James\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\nFN:Jane\r\nEND:VCARD\r\n";
    mInputDevice->close();
    mInputDevice->setData(validDocumentsAndGroupedDocument);
    mInputDevice->open(QBuffer::ReadWrite);
    mInputDevice->seek(0);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    results = mReader->results();
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    // An error is logged because one failed, but the rest are readable.
    QCOMPARE(mReader->error(), QVersitReader::ParseError);
    QCOMPARE(results.count(),4);

    // Valid documents and a grouped document between them
    const QByteArray& validDocumentsAndGroupedDocument2 =
"BEGIN:VCARD\r\nFN:Jenny\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\n\
X-GROUPING:pub gang\r\n\
BEGIN:VCARD\r\nFN:Jeremy\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\nFN:Jeffery\r\nEND:VCARD\r\n\
END:VCARD\r\n\
BEGIN:VCARD\r\nFN:Jake\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\nFN:James\r\nEND:VCARD\r\n\
BEGIN:VCARD\r\nFN:Jane\r\nEND:VCARD";
    mInputDevice->close();
    mInputDevice->setData(validDocumentsAndGroupedDocument2);
    mInputDevice->open(QBuffer::ReadWrite);
    mInputDevice->seek(0);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    results = mReader->results();
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    // An error is logged because one failed, but the rest are readable.
    QCOMPARE(mReader->error(), QVersitReader::ParseError);
    QCOMPARE(mReader->results().count(),4);

    qApp->processEvents(); // clean up before we start sniffing signals

    // Asynchronous reading
    mInputDevice->close();
    mInputDevice->setData(twoDocuments);
    mInputDevice->open(QBuffer::ReadWrite);
    mInputDevice->seek(0);
    mSignalCatcher->mStateChanges.clear();
    mSignalCatcher->mResultsCount = 0;
    QVERIFY(mReader->startReading());
    QTRY_VERIFY(mSignalCatcher->mStateChanges.count() >= 2);
    QCOMPARE(mSignalCatcher->mStateChanges.at(0), QVersitReader::ActiveState);
    QCOMPARE(mSignalCatcher->mStateChanges.at(1), QVersitReader::FinishedState);
    QVERIFY(mSignalCatcher->mResultsCount >= 2);
    QCOMPARE(mReader->results().size(), 2);
    QCOMPARE(mReader->error(), QVersitReader::NoError);

    // Cancelling
    mInputDevice->close();
    mInputDevice->setData(twoDocuments);
    mInputDevice->open(QBuffer::ReadOnly);
    mInputDevice->seek(0);
    mSignalCatcher->mStateChanges.clear();
    mSignalCatcher->mResultsCount = 0;
    QVERIFY(mReader->startReading());
    mReader->cancel();
    mReader->waitForFinished();
    QTRY_VERIFY(mSignalCatcher->mStateChanges.count() >= 2);
    QCOMPARE(mSignalCatcher->mStateChanges.at(0), QVersitReader::ActiveState);
    QVersitReader::State state(mSignalCatcher->mStateChanges.at(1));
    // It's possible that it finishes before it cancels.
    QVERIFY(state == QVersitReader::CanceledState
            || state == QVersitReader::FinishedState);
}

void tst_QVersitReader::testResult()
{
    QCOMPARE(mReader->results().count(),0);
}

void tst_QVersitReader::testSetVersionFromProperty()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QVersitDocument document;

    // Some other property than VERSION
    QVersitProperty property;
    property.setName(QString::fromAscii("N"));
    QVERIFY(mReaderPrivate->setVersionFromProperty(document,property));

    // VERSION property with 2.1
    property.setName(QString::fromAscii("VERSION"));
    property.setValue(QString::fromAscii("2.1"));
    QVERIFY(mReaderPrivate->setVersionFromProperty(document,property));
    QVERIFY(document.type() == QVersitDocument::VCard21Type);

    // VERSION property with 3.0
    property.setValue(QString::fromAscii("3.0"));
    QVERIFY(mReaderPrivate->setVersionFromProperty(document,property));
    QVERIFY(document.type() == QVersitDocument::VCard30Type);

    // VERSION property with a not supported value
    property.setValue(QString::fromAscii("4.0"));
    QVERIFY(!mReaderPrivate->setVersionFromProperty(document,property));

    // VERSION property with BASE64 encoded supported value
    property.setValue(QString::fromAscii(QByteArray("2.1").toBase64()));
    property.insertParameter(QString::fromAscii("ENCODING"),QString::fromAscii("BASE64"));
    QVERIFY(mReaderPrivate->setVersionFromProperty(document,property));
    QVERIFY(document.type() == QVersitDocument::VCard21Type);

    // VERSION property with BASE64 encoded not supported value
    property.setValue(QString::fromAscii(QByteArray("4.0").toBase64()));
    QVERIFY(!mReaderPrivate->setVersionFromProperty(document,property));
#endif
}

void tst_QVersitReader::testParseNextVersitProperty()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QFETCH(QVersitDocument::VersitType, documentType);
    QFETCH(QByteArray, input);
    QFETCH(QVersitProperty, expectedProperty);

    QBuffer buffer(&input);
    buffer.open(QIODevice::ReadOnly);
    LineReader lineReader(&buffer, mAsciiCodec);
    QVersitProperty property = mReaderPrivate->parseNextVersitProperty(documentType, lineReader);
    if (property != expectedProperty) {
        // compare each part of the property separately for easier debugging
        QCOMPARE(property.groups(), expectedProperty.groups());
        QCOMPARE(property.name(), expectedProperty.name());
        QCOMPARE(property.valueType(), expectedProperty.valueType());

        // QVariant doesn't support == on QVersitDocuments - do it manually
        if (property.variantValue().userType() == qMetaTypeId<QVersitDocument>()) {
            QVERIFY(expectedProperty.variantValue().userType() == qMetaTypeId<QVersitDocument>());
            QCOMPARE(property.value<QVersitDocument>(), expectedProperty.value<QVersitDocument>());
        }
        else
            QCOMPARE(property.variantValue(), expectedProperty.variantValue());

        // Don't check parameters because the reader can add random parameters of its own (like CHARSET)
        // QCOMPARE(property.parameters(), expectedProperty.parameters());
    }
#endif
}

void tst_QVersitReader::testParseNextVersitProperty_data()
{
#ifdef QT_BUILD_INTERNAL
    QTest::addColumn<QVersitDocument::VersitType>("documentType");
    QTest::addColumn<QByteArray>("input");
    QTest::addColumn<QVersitProperty>("expectedProperty");

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("BEGIN"));
        expectedProperty.setValue(QLatin1String("vcard"));
        QTest::newRow("begin")
            << QVersitDocument::VCard21Type
            << QByteArray("Begin:vcard\r\n")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("VERSION"));
        expectedProperty.setValue(QLatin1String("2.1"));
        expectedProperty.setValueType(QVersitProperty::PlainType);
        QTest::newRow("version")
            << QVersitDocument::VCard21Type
            << QByteArray("VERSION:2.1\r\n")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("FN"));
        expectedProperty.setValue(QLatin1String("John"));
        expectedProperty.setValueType(QVersitProperty::PlainType);
        QTest::newRow("fn")
            << QVersitDocument::VCard21Type
            << QByteArray("FN:John\r\n")
            << expectedProperty;
    }

    {
        // "NOTE:\;\,\:\\"
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("NOTE"));
        expectedProperty.setValue(QLatin1String("\\;\\,\\:\\\\"));
        expectedProperty.setValueType(QVersitProperty::PlainType);
        QTest::newRow("vcard21 note")
            << QVersitDocument::VCard21Type
            << QByteArray("NOTE:\\;\\,\\:\\\\\r\n")
            << expectedProperty;

        expectedProperty.setValue(QLatin1String(";,:\\"));
        QTest::newRow("vcard30 note")
            << QVersitDocument::VCard30Type
            << QByteArray("NOTE:\\;\\,\\:\\\\\r\n")
            << expectedProperty;
    }

    {
        // "N:foo\;bar;foo\,bar;foo\:bar;foo\\bar;foo\\\;bar"
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("N"));
        QStringList components;
        components << QLatin1String("foo;bar")
            << QLatin1String("foo\\,bar")
            << QLatin1String("foo\\:bar")
            << QLatin1String("foo\\\\bar")
            << QLatin1String("foo\\\\;bar");
        expectedProperty.setValue(components);
        expectedProperty.setValueType(QVersitProperty::CompoundType);
        QTest::newRow("vcard21 n")
            << QVersitDocument::VCard21Type
            << QByteArray("N:foo\\;bar;foo\\,bar;foo\\:bar;foo\\\\bar;foo\\\\\\;bar\r\n")
            << expectedProperty;

        components.clear();
        components << QLatin1String("foo;bar")
            << QLatin1String("foo,bar")
            << QLatin1String("foo:bar")
            << QLatin1String("foo\\bar")
            << QLatin1String("foo\\;bar");
        expectedProperty.setValue(components);
        QTest::newRow("vcard30 n")
            << QVersitDocument::VCard30Type
            << QByteArray("N:foo\\;bar;foo\\,bar;foo\\:bar;foo\\\\bar;foo\\\\\\;bar\r\n")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("ADR"));
        expectedProperty.setValue(QStringList(QString()));
        expectedProperty.setValueType(QVersitProperty::CompoundType);
        QTest::newRow("empty structured")
            << QVersitDocument::VCard21Type
            << QByteArray("ADR:\r\n")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("X-CHILDREN"));
        expectedProperty.setValue(QStringList() << QLatin1String("Child1") << QLatin1String("Child2"));
        expectedProperty.setValueType(QVersitProperty::ListType);
        QTest::newRow("children")
            << QVersitDocument::VCard21Type
            << QByteArray("X-CHILDREN:Child1,Child2\r\n")
            << expectedProperty;
    }

    {
        // "NICKNAME:foo\;bar,foo\,bar,foo\:bar,foo\\bar,foo\\\,bar"
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("NICKNAME"));
        QStringList components;
        components << QLatin1String("foo\\;bar")
            << QLatin1String("foo,bar")
            << QLatin1String("foo\\:bar")
            << QLatin1String("foo\\\\bar")
            << QLatin1String("foo\\\\,bar");
        expectedProperty.setValue(components);
        expectedProperty.setValueType(QVersitProperty::ListType);
        QTest::newRow("vcard21 nickname")
            << QVersitDocument::VCard21Type
            << QByteArray("NICKNAME:foo\\;bar,foo\\,bar,foo\\:bar,foo\\\\bar,foo\\\\\\,bar\r\n")
            << expectedProperty;

        components.clear();
        components << QLatin1String("foo;bar")
            << QLatin1String("foo,bar")
            << QLatin1String("foo:bar")
            << QLatin1String("foo\\bar")
            << QLatin1String("foo\\,bar");
        expectedProperty.setValue(components);
        QTest::newRow("vcard30 nickname")
            << QVersitDocument::VCard30Type
            << QByteArray("NICKNAME:foo\\;bar,foo\\,bar,foo\\:bar,foo\\\\bar,foo\\\\\\,bar\r\n")
            << expectedProperty;
    }

    {
        // "CATEGORIES:foo\;bar,foo\,bar,foo\:bar,foo\\bar,foo\\\,bar"
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("CATEGORIES"));
        QStringList components;
        components << QLatin1String("foo\\;bar")
            << QLatin1String("foo,bar")
            << QLatin1String("foo\\:bar")
            << QLatin1String("foo\\\\bar")
            << QLatin1String("foo\\\\,bar");
        expectedProperty.setValue(components);
        expectedProperty.setValueType(QVersitProperty::ListType);
        QTest::newRow("vcard21 categories")
            << QVersitDocument::VCard21Type
            << QByteArray("CATEGORIES:foo\\;bar,foo\\,bar,foo\\:bar,foo\\\\bar,foo\\\\\\,bar\r\n")
            << expectedProperty;

        components.clear();
        components << QLatin1String("foo;bar")
            << QLatin1String("foo,bar")
            << QLatin1String("foo:bar")
            << QLatin1String("foo\\bar")
            << QLatin1String("foo\\,bar");
        expectedProperty.setValue(components);
        QTest::newRow("vcard30 categories")
            << QVersitDocument::VCard30Type
            << QByteArray("CATEGORIES:foo\\;bar,foo\\,bar,foo\\:bar,foo\\\\bar,foo\\\\\\,bar\r\n")
            << expectedProperty;

        // "CATEGORIES:foobar\\,foobar\\\\,foo\\\\\,bar"
        components.clear();
        components << QLatin1String("foobar\\")
            << QLatin1String("foobar\\\\")
            << QLatin1String("foo\\\\,bar");
        expectedProperty.setValue(components);
        QTest::newRow("vcard30 unescaping")
            << QVersitDocument::VCard30Type
            << QByteArray("CATEGORIES:foobar\\\\,foobar\\\\\\\\,foo\\\\\\\\\\,bar")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("ORG"));
        expectedProperty.setValue(QString::fromUtf8(KATAKANA_NOKIA));
        expectedProperty.setValueType(QVersitProperty::CompoundType);
        QTest::newRow("org utf8")
            << QVersitDocument::VCard21Type
            << QByteArray("ORG;CHARSET=UTF-8:" + KATAKANA_NOKIA + "\r\n")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("PHOTO"));
        expectedProperty.setValue(QLatin1String("Qt is great!"));
        expectedProperty.setValueType(QVersitProperty::BinaryType);
        QTest::newRow("vcard21 photo1")
            << QVersitDocument::VCard21Type
            << QByteArray("PHOTO;ENCODING=BASE64: U\t XQgaX MgZ\t3Jl YXQh\r\n\r\n")
            << expectedProperty;

        QTest::newRow("vcard30 photo1")
            << QVersitDocument::VCard30Type
            << QByteArray("PHOTO;ENCODING=B: U\t XQgaX MgZ\t3Jl YXQh\r\n\r\n")
            << expectedProperty;
    }

    // Again, but without the explicit "ENCODING" parameter
    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("PHOTO"));
        expectedProperty.setValue(QLatin1String("Qt is great!"));
        expectedProperty.setValueType(QVersitProperty::BinaryType);
        QTest::newRow("photo2")
            << QVersitDocument::VCard21Type
            << QByteArray("PHOTO;BASE64: U\t XQgaX MgZ\t3Jl YXQh\r\n\r\n")
            << expectedProperty;

        QTest::newRow("photo2")
            << QVersitDocument::VCard30Type
            << QByteArray("PHOTO;B: U\t XQgaX MgZ\t3Jl YXQh\r\n\r\n")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setGroups(QStringList() << QLatin1String("HOME") << QLatin1String("Springfield"));
        expectedProperty.setName(QLatin1String("EMAIL"));
        expectedProperty.setValue(QLatin1String("john.citizen@example.com"));
        expectedProperty.setValueType(QVersitProperty::PlainType);
        QTest::newRow("email qp")
            << QVersitDocument::VCard21Type
            << QByteArray("HOME.Springfield.EMAIL;Encoding=Quoted-Printable:john.citizen=40exam=\r\nple.com\r\n")
            << expectedProperty;
    }

    {
        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("EMAIL"));
        expectedProperty.setValue(QLatin1String("john.citizen@example.com"));
        expectedProperty.setValueType(QVersitProperty::PlainType);
        QTest::newRow("email qp utf16")
            << QVersitDocument::VCard21Type
            << QByteArray("EMAIL;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-16BE:" + QTextCodec::codecForName("UTF-16BE")->fromUnicode(QLatin1String("john.citizen=40exam=\r\nple.com")) + "\r\n")
            << expectedProperty;
    }

    {
        QVersitDocument subDocument;
        subDocument.setType(QVersitDocument::VCard30Type);
        QVersitProperty subProperty;
        subProperty.setName(QLatin1String("FN"));
        subProperty.setValue(QLatin1String("Jenny"));
        subDocument.addProperty(subProperty);

        QVersitProperty expectedProperty;
        expectedProperty.setName(QLatin1String("AGENT"));
        expectedProperty.setValue(QVariant::fromValue(subDocument));
        expectedProperty.setValueType(QVersitProperty::VersitDocumentType);
        QTest::newRow("agent")
            << QVersitDocument::VCard21Type
            << QByteArray("AGENT:\r\nBEGIN:VCARD\r\nFN:Jenny\r\nEND:VCARD\r\n\r\n")
            << expectedProperty;
    }
#endif
}

void tst_QVersitReader::testParseVersitDocument()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QFETCH(QByteArray, vCard);
    QFETCH(bool, expectedSuccess);
    QFETCH(int, expectedProperties);

    QBuffer buffer(&vCard);
    buffer.open(QIODevice::ReadOnly);
    LineReader lineReader(&buffer, QTextCodec::codecForName("UTF-8"));

    QVersitDocument document;
    QCOMPARE(mReaderPrivate->parseVersitDocument(lineReader, document), expectedSuccess);
    QCOMPARE(document.properties().count(), expectedProperties);
    QCOMPARE(mReaderPrivate->mDocumentNestingLevel, 0);
#endif
}

void tst_QVersitReader::testParseVersitDocument_data()
{
#ifdef QT_BUILD_INTERNAL
    QTest::addColumn<QByteArray>("vCard");
    QTest::addColumn<bool>("expectedSuccess");
    QTest::addColumn<int>("expectedProperties");

    QTest::newRow("Basic vCard 2.1")
            << QByteArray(
                    "BEGIN:VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "FN:John\r\n"
                    "END:VCARD\r\n")
            << true
            << 1;

    QTest::newRow("vCard 2.1 with Agent")
            << QByteArray(
                    "BEGIN:VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "FN:John\r\n"
                    "AGENT:BEGIN:VCARD\r\nN:Jenny\r\nEND:VCARD\r\n\r\n"
                    "EMAIL;ENCODING=QUOTED-PRINTABLE:john.citizen=40exam=\r\nple.com\r\n"
                    "END:VCARD\r\n")
            << true
            << 3;

    QTest::newRow("vCard 3.0 with Agent")
            << QByteArray(
                    "BEGIN:VCARD\r\n"
                    "VERSION:3.0\r\n"
                    "FN:John\r\n"
                    "AGENT:BEGIN\\:VCARD\\nN\\:Jenny\\nEND\\:VCARD\\n\r\n"
                    "EMAIL:john.citizen@example.com\r\n"
                    "END:VCARD\r\n")
            << true
            << 3;

    QTest::newRow("No BEGIN found")
            << QByteArray(
                    "VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "FN:Nobody\r\n"
                    "END:VCARD\r\n")
            << false
            << 0;

    QTest::newRow("Wrong card type")
            << QByteArray(
                    "BEGIN:VCAL\r\n"
                    "END:VCAL\r\n")
            << false
            << 0;

    QTest::newRow("Wrong version")
            << QByteArray(
                    "BEGIN:VCARD\r\n"
                    "VERSION:4.0\r\n"
                    "FN:Nobody\r\n"
                    "END:VCARD\r\n")
            << false
            << 0;

    QTest::newRow("No trailing crlf")
            << QByteArray(
                    "BEGIN:VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "FN:Nobody\r\n"
                    "END:VCARD")
            << true
            << 1;

    QTest::newRow("No end")
            << QByteArray(
                    "BEGIN:VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "FN:Nobody\r\n")
            << false
            << 0;

    QTest::newRow("Grouped vCards are not supported. The whole vCard will be discarded.")
            << QByteArray(
                    "BEGIN:VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "X-EXAMPLES:Family vCard\r\n"
                    "BEGIN:VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "N:Citizen;John\r\n"
                    "TEL;CELL:1111\r\n"
                    "EMAIL;ENCODING=QUOTED-PRINTABLE:john.citizen=40example.com\r\n"
                    "END:VCARD\r\n"
                    "BEGIN:VCARD\r\n"
                    "VERSION:2.1\r\n"
                    "N:Citizen;Jenny\r\n"
                    "TEL;CELL:7777\r\n"
                    "END:VCARD\r\n"
                    "END:VCARD")
            << false
            << 0;
#endif
}

void tst_QVersitReader::testDecodeQuotedPrintable()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QFETCH(QString, encoded);

    QFETCH(QString, decoded);
    mReaderPrivate->decodeQuotedPrintable(encoded);
    QCOMPARE(encoded, decoded);
#endif
}

void tst_QVersitReader::testDecodeQuotedPrintable_data()
{
#ifdef QT_BUILD_INTERNAL
    QTest::addColumn<QString>("encoded");
    QTest::addColumn<QString>("decoded");


    QTest::newRow("Soft line breaks")
            << QString::fromLatin1("This=\r\n is =\r\none line.")
            << QString::fromLatin1("This is one line.");

    QTest::newRow("Characters recommended to be encoded according to RFC 1521")
            << QString::fromLatin1("To be decoded: =0A=0D=21=22=23=24=3D=40=5B=5C=5D=5E=60=7B=7C=7D=7E")
            << QString::fromLatin1("To be decoded: \n\r!\"#$=@[\\]^`{|}~");

    QTest::newRow("Characters recommended to be encoded according to RFC 1521(lower case)")
            << QString::fromLatin1("To be decoded: =0a=0d=21=22=23=24=3d=40=5b=5c=5d=5e=60=7b=7c=7d=7e")
            << QString::fromLatin1("To be decoded: \n\r!\"#$=@[\\]^`{|}~");

    QTest::newRow("random characters encoded")
            << QString::fromLatin1("=45=6E=63=6F=64=65=64 =64=61=74=61")
            << QString::fromLatin1("Encoded data");

    QTest::newRow("short string1")
            << QString::fromLatin1("-=_")
            << QString::fromLatin1("-=_");

    QTest::newRow("short string2")
            << QString::fromLatin1("=0")
            << QString::fromLatin1("=0");

    QTest::newRow("short string2")
            << QString::fromLatin1("\r")
            << QString::fromLatin1("\r");

    QTest::newRow("short string2")
            << QString::fromLatin1("\n")
            << QString::fromLatin1("\n");

    QTest::newRow("short string2")
            << QString::fromLatin1("\n\r")
            << QString::fromLatin1("\n\r");

    QTest::newRow("White spaces")
            << QString::fromLatin1("=09=20")
            << QString::fromLatin1("\t ");
#endif
}
void tst_QVersitReader::testParamName()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    // Empty value
    QByteArray param;
    QCOMPARE(mReaderPrivate->paramName(param, mAsciiCodec),QString());

    // Only value present
    param = "WORK";
    QCOMPARE(mReaderPrivate->paramName(param, mAsciiCodec),
             QString::fromAscii("TYPE"));

    // The below tests intentionally use the misspelling TIPE to avoid the default behaviour of
    // returning TYPE when the name can't be parsed.
    // Both name and value, spaces after the name
    param = "TIPE \t =WORK";
    QCOMPARE(mReaderPrivate->paramName(param, mAsciiCodec),
             QString::fromAscii("TIPE"));

    // Both name and value, no spaces after the name
    param = "TIPE=WORK";
    QCOMPARE(mReaderPrivate->paramName(param, mAsciiCodec),
             QString::fromAscii("TIPE"));

    // Test wide character support.
    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    param = codec->fromUnicode(QString::fromAscii("TIPE=WORK"));
    QCOMPARE(mReaderPrivate->paramName(param, codec),
             QString::fromAscii("TIPE"));
#endif
}

void tst_QVersitReader::testParamValue()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    // Empty value
    QByteArray param;
    QCOMPARE(mReaderPrivate->paramValue(param, mAsciiCodec),QString());

    // Only value present
    param = "WORK";
    QCOMPARE(mReaderPrivate->paramValue(param, mAsciiCodec),
             QString::fromAscii("WORK"));

    // Name and equals sign, but no value
    param = "TYPE=";
    QCOMPARE(mReaderPrivate->paramValue(param, mAsciiCodec),QString());

    // Name and equals sign, but value has only spaces
    param = "TYPE=  \t ";
    QCOMPARE(mReaderPrivate->paramValue(param, mAsciiCodec),QString());

    // Both name and value, spaces before the value
    param = "TYPE= \t  WORK";
    QCOMPARE(mReaderPrivate->paramValue(param, mAsciiCodec),
             QString::fromAscii("WORK"));

    // Both name and value, no spaces before the value
    param = "ENCODING=QUOTED-PRINTABLE";
    QCOMPARE(mReaderPrivate->paramValue(param, mAsciiCodec),
             QString::fromAscii("QUOTED-PRINTABLE"));

    // Test wide character support.
    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    param = codec->fromUnicode(QString::fromAscii("TYPE=WORK"));
    QCOMPARE(mReaderPrivate->paramValue(param, codec),
             QString::fromAscii("WORK"));
#endif
}

void tst_QVersitReader::testExtractPart()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QByteArray originalStr;

    // Negative starting position
    QCOMPARE(mReaderPrivate->extractPart(originalStr,-1,1), QByteArray());

    // Empty original string
    QCOMPARE(mReaderPrivate->extractPart(originalStr,0,1), QByteArray());

    // Trimmed substring empty
    originalStr = " \t \t";
    QCOMPARE(mReaderPrivate->extractPart(originalStr,0,4), QByteArray());

    // The given substring length is greater than the original string length
    originalStr = "ENCODING=7BIT";
    QCOMPARE(mReaderPrivate->extractPart(originalStr,0,100), originalStr);

    // Non-empty substring, from the beginning
    originalStr = " TYPE=WORK ; X-PARAM=X-VALUE; ENCODING=8BIT";
    QCOMPARE(mReaderPrivate->extractPart(originalStr,0,11),
             QByteArray("TYPE=WORK"));

    // Non-empty substring, from the middle
    QCOMPARE(mReaderPrivate->extractPart(originalStr,12,16),
             QByteArray("X-PARAM=X-VALUE"));

    // Non-empty substring, from the middle to the end
    QCOMPARE(mReaderPrivate->extractPart(originalStr,29),
             QByteArray("ENCODING=8BIT"));
#endif
}

void tst_QVersitReader::testExtractParts()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QList<QByteArray> parts;

    // Empty value
    QByteArray text;
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QVERIFY(parts.isEmpty());

    // Only separator
    text = ";";
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QVERIFY(parts.isEmpty());

    // One part
    text = "part";
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QCOMPARE(parts.count(),1);
    QCOMPARE(QString::fromAscii(parts[0]),QString::fromAscii("part"));

    // Separator in the beginning, one part
    text = ";part";
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QCOMPARE(parts.count(),1);
    QCOMPARE(QString::fromAscii(parts[0]),QString::fromAscii("part"));

    // Separator in the end, one part
    text = "part;";
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QCOMPARE(parts.count(),1);
    QCOMPARE(QString::fromAscii(parts[0]),QString::fromAscii("part"));

    // One part that contains escaped separator
    text = "part\\;";
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QCOMPARE(parts.count(),1);
    QCOMPARE(QString::fromAscii(parts[0]),QString::fromAscii("part\\;"));

    // Two parts
    text = "part1;part2";
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QCOMPARE(parts.count(),2);
    QCOMPARE(QString::fromAscii(parts[0]),QString::fromAscii("part1"));
    QCOMPARE(QString::fromAscii(parts[1]),QString::fromAscii("part2"));

    // Two parts that contain escaped separators
    text = "pa\\;rt1;par\\;t2";
    parts = mReaderPrivate->extractParts(text,";", mAsciiCodec);
    QCOMPARE(parts.count(),2);
    QCOMPARE(QString::fromAscii(parts[0]),QString::fromAscii("pa\\;rt1"));
    QCOMPARE(QString::fromAscii(parts[1]),QString::fromAscii("par\\;t2"));

    // Test wide character support
    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    text = codec->fromUnicode(QString::fromAscii("part1;part2"));
    parts = mReaderPrivate->extractParts(text,";", codec);
    QCOMPARE(parts.count(),2);
    QCOMPARE(codec->toUnicode(parts[0]),QString::fromAscii("part1"));
    QCOMPARE(codec->toUnicode(parts[1]),QString::fromAscii("part2"));
#endif
}

void tst_QVersitReader::testExtractPropertyGroupsAndName()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QPair<QStringList,QString> groupsAndName;

    // Empty string
    VersitCursor cursor(QByteArray(" "));
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString());

    // No value -> returns empty string and no groups
    QByteArray property("TEL");
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString());

    // Simple name and value
    property = "TEL:123";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString::fromAscii("TEL"));

    // One whitespace before colon
    property = "TEL :123";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString::fromAscii("TEL"));

    // Several whitespaces before colon
    property = "TEL \t  :123";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString::fromAscii("TEL"));

    // Name contains a group
    property = "group1.TEL:1234";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),1);
    QCOMPARE(groupsAndName.first.takeFirst(),QString::fromAscii("group1"));
    QCOMPARE(groupsAndName.second,QString::fromAscii("TEL"));

    // Name contains more than one group
    property = "group1.group2.TEL:12345";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),2);
    QCOMPARE(groupsAndName.first.takeFirst(),QString::fromAscii("group1"));
    QCOMPARE(groupsAndName.first.takeFirst(),QString::fromAscii("group2"));
    QCOMPARE(groupsAndName.second,QString::fromAscii("TEL"));
    QCOMPARE(cursor.position, 17);

    // Property contains one parameter
    property = "TEL;WORK:123";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString::fromAscii("TEL"));

    // Property contains several parameters
    property = "EMAIL;INTERNET;ENCODING=QUOTED-PRINTABLE:user=40ovi.com";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString::fromAscii("EMAIL"));

    // Name contains an escaped semicolon
    property = "X-proper\\;ty:value";
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, mAsciiCodec);
    QCOMPARE(groupsAndName.first.count(),0);
    QCOMPARE(groupsAndName.second,QString::fromAscii("X-proper\\;ty"));

    // Test wide character support
    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    property = codec->fromUnicode(QString::fromAscii("group1.group2.TEL;WORK:123"));
    cursor.setData(property);
    cursor.selection = property.size();
    groupsAndName = mReaderPrivate->extractPropertyGroupsAndName(cursor, codec);
    QCOMPARE(groupsAndName.first.count(),2);
    QCOMPARE(groupsAndName.first.takeFirst(),QString::fromAscii("group1"));
    QCOMPARE(groupsAndName.first.takeFirst(),QString::fromAscii("group2"));
    QCOMPARE(groupsAndName.second,QString::fromAscii("TEL"));
    QCOMPARE(cursor.position, 36); // 2 bytes * 17 characters + 2 byte BOM.
#endif
}

void tst_QVersitReader::testExtractVCard21PropertyParams()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    // No parameters
    VersitCursor cursor(QByteArray(":123"));
    cursor.setSelection(cursor.data.size());
    QCOMPARE(mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec).count(), 0);

    // "Empty" parameter
    cursor.setData(QByteArray(";:123"));
    cursor.setSelection(cursor.data.size());
    QCOMPARE(mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec).count(), 0);

    // Semicolon found, but no value for the property
    cursor.setData(QByteArray(";TYPE=X-TYPE"));
    cursor.setSelection(cursor.data.size());
    QCOMPARE(mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec).count(), 0);

    // The property name contains an escaped semicolon, no parameters
    cursor.setData(QByteArray(":value"));
    cursor.setSelection(cursor.data.size());
    QCOMPARE(mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec).count(), 0);

    // The property value contains a semicolon, no parameters
    cursor.setData(QByteArray(":va;lue"));
    cursor.setSelection(cursor.data.size());
    QCOMPARE(mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec).count(), 0);

    // One parameter
    cursor.setData(QByteArray(";HOME:123"));
    cursor.setSelection(cursor.data.size());
    QMultiHash<QString,QString> params = mReaderPrivate->extractVCard21PropertyParams(cursor,
                                                                                   mAsciiCodec);
    QCOMPARE(1, params.count());
    QCOMPARE(1, params.values(QString::fromAscii("TYPE")).count());
    QCOMPARE(params.values(QString::fromAscii("TYPE"))[0],QString::fromAscii("HOME"));

    // Two parameters of the same type
    cursor.setData(QByteArray(";HOME;VOICE:123"));
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(2, params.count());
    QCOMPARE(2, params.values(QString::fromAscii("TYPE")).count());
    QCOMPARE(params.values(QString::fromAscii("TYPE"))[0],QString::fromAscii("HOME"));
    QCOMPARE(params.values(QString::fromAscii("TYPE"))[1],QString::fromAscii("VOICE"));

    // Two parameters, several empty parameters (extra semicolons)
    cursor.setData(QByteArray(";;;;HOME;;;;;VOICE;;;:123"));
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(2, params.count());
    QCOMPARE(2, params.values(QString::fromAscii("TYPE")).count());
    QCOMPARE(params.values(QString::fromAscii("TYPE"))[0],QString::fromAscii("HOME"));
    QCOMPARE(params.values(QString::fromAscii("TYPE"))[1],QString::fromAscii("VOICE"));

    // Two parameters with different types
    cursor.setData(QByteArray(";INTERNET;ENCODING=QUOTED-PRINTABLE:user=40ovi.com"));
    cursor.setSelection(cursor.data.size());
    params.clear();
    params = mReaderPrivate->extractVCard21PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(2, params.count());
    QList<QString> typeParams = params.values(QString::fromAscii("TYPE"));
    QCOMPARE(1, typeParams.count());
    QCOMPARE(typeParams[0],QString::fromAscii("INTERNET"));
    QList<QString> encodingParams = params.values(QString::fromAscii("ENCODING"));
    QCOMPARE(1, encodingParams.count());
    QCOMPARE(encodingParams[0],QString::fromAscii("QUOTED-PRINTABLE"));

    // Test wide character support.
    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    QByteArray data = VersitUtils::encode(";HOME;CHARSET=UTF-16:123", codec);
    cursor.setData(data);
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard21PropertyParams(cursor, codec);
    QCOMPARE(2, params.count());
    typeParams = params.values(QString::fromAscii("TYPE"));
    QCOMPARE(1, typeParams.count());
    QCOMPARE(typeParams[0],QString::fromAscii("HOME"));
    encodingParams = params.values(QString::fromAscii("CHARSET"));
    QCOMPARE(1, encodingParams.count());
    QCOMPARE(encodingParams[0],QString::fromAscii("UTF-16"));
#endif
}

void tst_QVersitReader::testExtractVCard30PropertyParams()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    // No parameters
    VersitCursor cursor(QByteArray(":123"));
    cursor.setSelection(cursor.data.size());
    QCOMPARE(mReaderPrivate->extractVCard30PropertyParams(cursor, mAsciiCodec).count(), 0);

    // One parameter
    cursor.setData(QByteArray(";TYPE=HOME:123"));
    cursor.setSelection(cursor.data.size());
    QMultiHash<QString,QString> params = mReaderPrivate->extractVCard30PropertyParams(cursor,
                                                                                   mAsciiCodec);
    QCOMPARE(params.count(), 1);
    QCOMPARE(params.values(QString::fromAscii("TYPE")).count(), 1);
    QCOMPARE(params.values(QString::fromAscii("TYPE"))[0], QString::fromAscii("HOME"));

    // One parameter with an escaped semicolon
    cursor.setData(QByteArray(";para\\;meter:value"));
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard30PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(params.count(), 1);
    QCOMPARE(params.values(QString::fromAscii("TYPE")).count(), 1);
    QCOMPARE(params.values(QString::fromAscii("TYPE"))[0], QString::fromAscii("para;meter"));

    // One parameter with and escaped comma in the name and the value
    cursor.setData(QByteArray(";X-PA\\,RAM=VAL\\,UE:123"));
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard30PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(params.count(), 1);
    QCOMPARE(params.values(QString::fromAscii("X-PA,RAM")).count(), 1);
    QCOMPARE(params.values(QString::fromAscii("X-PA,RAM"))[0], QString::fromAscii("VAL,UE"));

    // Two parameters of the same type
    cursor.setData(QByteArray(";TYPE=HOME,VOICE:123"));
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard30PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(params.count(), 2);
    QCOMPARE(params.values(QString::fromAscii("TYPE")).count(), 2);
    QVERIFY(params.values(QString::fromAscii("TYPE")).contains(QString::fromAscii("HOME")));
    QVERIFY(params.values(QString::fromAscii("TYPE")).contains(QString::fromAscii("VOICE")));

    // Two parameters of the same type in separate name-values
    cursor.setData(QByteArray(";TYPE=HOME;TYPE=VOICE:123"));
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard30PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(params.count(), 2);
    QCOMPARE(params.values(QString::fromAscii("TYPE")).count(), 2);
    QVERIFY(params.values(QString::fromAscii("TYPE")).contains(QString::fromAscii("HOME")));
    QVERIFY(params.values(QString::fromAscii("TYPE")).contains(QString::fromAscii("VOICE")));

    // Three parameters of the same type
    cursor.setData(QByteArray(";TYPE=PREF,HOME,VOICE:123"));
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard30PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(params.count(), 3);
    QCOMPARE(params.values(QString::fromAscii("TYPE")).count(), 3);
    QVERIFY(params.values(QString::fromAscii("TYPE")).contains(QString::fromAscii("PREF")));
    QVERIFY(params.values(QString::fromAscii("TYPE")).contains(QString::fromAscii("HOME")));
    QVERIFY(params.values(QString::fromAscii("TYPE")).contains(QString::fromAscii("VOICE")));

    // Two parameters with different types
    cursor.setData(QByteArray(";TYPE=HOME;X-PARAM=X-VALUE:Home Street 1"));
    cursor.setSelection(cursor.data.size());
    params.clear();
    params = mReaderPrivate->extractVCard30PropertyParams(cursor, mAsciiCodec);
    QCOMPARE(params.count(), 2);
    QList<QString> typeParams = params.values(QString::fromAscii("TYPE"));
    QCOMPARE(typeParams.count(), 1);
    QCOMPARE(typeParams[0],QString::fromAscii("HOME"));
    QList<QString> encodingParams = params.values(QString::fromAscii("X-PARAM"));
    QCOMPARE(encodingParams.count(), 1);
    QCOMPARE(encodingParams[0],QString::fromAscii("X-VALUE"));

    // Test wide character support.
    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    QByteArray data = VersitUtils::encode(";TIPE=HOME,VOICE;CHARSET=UTF-16:123", codec);
    cursor.setData(data);
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractVCard30PropertyParams(cursor, codec);
    QCOMPARE(params.count(), 3);
    typeParams = params.values(QString::fromAscii("TIPE"));
    QCOMPARE(params.values(QString::fromAscii("TIPE")).count(), 2);
    QVERIFY(params.values(QString::fromAscii("TIPE")).contains(QString::fromAscii("HOME")));
    QVERIFY(params.values(QString::fromAscii("TIPE")).contains(QString::fromAscii("VOICE")));
    encodingParams = params.values(QString::fromAscii("CHARSET"));
    QCOMPARE(1, encodingParams.count());
    QCOMPARE(encodingParams[0],QString::fromAscii("UTF-16"));
#endif
}

void tst_QVersitReader::testExtractParams()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    VersitCursor cursor;
    QByteArray data = ":123";
    cursor.setData(data);
    cursor.setPosition(0);
    cursor.setSelection(cursor.data.size());
    QList<QByteArray> params = mReaderPrivate->extractParams(cursor, mAsciiCodec);
    QCOMPARE(params.size(), 0);
    QCOMPARE(cursor.position, 1);

    data = "a;b:123";
    cursor.setData(data);
    cursor.setPosition(0);
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractParams(cursor, mAsciiCodec);
    QCOMPARE(params.size(), 2);
    QCOMPARE(cursor.position, 4);
    QCOMPARE(params.at(0), QByteArray("a"));
    QCOMPARE(params.at(1), QByteArray("b"));

    QTextCodec* codec = QTextCodec::codecForName("UTF-16BE");
    data = VersitUtils::encode(":123", codec);
    cursor.setData(data);
    cursor.setPosition(0);
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractParams(cursor, codec);
    QCOMPARE(params.size(), 0);
    QCOMPARE(cursor.position, 2);

    data = VersitUtils::encode("a;b:123", codec);
    cursor.setData(data);
    cursor.setPosition(0);
    cursor.setSelection(cursor.data.size());
    params = mReaderPrivate->extractParams(cursor, codec);
    QCOMPARE(params.size(), 2);
    QCOMPARE(cursor.position, 8);
#endif
}

Q_DECLARE_METATYPE(QList<QString>)

void tst_QVersitReader::testReadLine()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    QFETCH(QByteArray, codecName);
    QFETCH(QString, data);
    QFETCH(QList<QString>, expectedLines);

    QTextCodec* codec = QTextCodec::codecForName(codecName);
    QTextEncoder* encoder = codec->makeEncoder();
    encoder->fromUnicode(QString());

    QByteArray bytes(encoder->fromUnicode(data));

    mInputDevice->close();
    mInputDevice->setData(bytes);
    mInputDevice->open(QIODevice::ReadWrite);

    LineReader lineReader(mInputDevice, codec, 10);

    // Check that all expected lines are read.
    foreach (QString expectedLine, expectedLines) {
        QByteArray expectedBytes(encoder->fromUnicode(expectedLine));
        QVERIFY(!lineReader.atEnd());
        VersitCursor line = lineReader.readLine();
        QVERIFY(line.data.indexOf(expectedBytes) == line.position);
        QCOMPARE(line.selection - line.position, expectedBytes.length());
    }
    // And that there are no more lines
    VersitCursor line = lineReader.readLine();
    QCOMPARE(line.selection, line.position);
    QVERIFY(lineReader.atEnd());

    delete encoder;
#endif
}

void tst_QVersitReader::testReadLine_data()
{
#ifdef QT_BUILD_INTERNAL
    // Note: for this test, we set mLineReader to read 10 bytes at a time.  Lines of multiples of
    // 10 bytes are hence border cases.
    QTest::addColumn<QByteArray>("codecName");
    QTest::addColumn<QString>("data");
    QTest::addColumn<QList<QString> >("expectedLines");

    QList<QByteArray> codecNames;
    codecNames << "UTF-8" << "UTF-16";

    foreach (QByteArray codecName, codecNames) {
        QTest::newRow("empty " + codecName)
                << codecName
                << ""
                << QList<QString>();

        QTest::newRow("one line " + codecName)
                << codecName
                << "line"
                << (QList<QString>() << QLatin1String("line"));

        QTest::newRow("one ten-byte line " + codecName)
                << codecName
                << "tenletters"
                << (QList<QString>() << QLatin1String("tenletters"));

        QTest::newRow("one long line " + codecName)
                << codecName
                << "one line longer than ten characters"
                << (QList<QString>() << QLatin1String("one line longer than ten characters"));

        QTest::newRow("one terminated line " + codecName)
                << codecName
                << "one line longer than ten characters\r\n"
                << (QList<QString>() << QLatin1String("one line longer than ten characters"));

        QTest::newRow("two lines " + codecName)
                << codecName
                << "two\r\nlines"
                << (QList<QString>() << QLatin1String("two") << QLatin1String("lines"));

        QTest::newRow("two terminated lines " + codecName)
                << codecName
                << "two\r\nlines\r\n"
                << (QList<QString>() << QLatin1String("two") << QLatin1String("lines"));

        QTest::newRow("two long lines " + codecName)
                << codecName
                << "one line longer than ten characters\r\nanother line\r\n"
                << (QList<QString>() << QLatin1String("one line longer than ten characters") << QLatin1String("another line"));

        QTest::newRow("two full lines " + codecName)
                << codecName
                << "tenletters\r\n8letters\r\n"
                << (QList<QString>() << QLatin1String("tenletters") << QLatin1String("8letters"));

        QTest::newRow("a nine-byte line " + codecName)
                << codecName
                << "9 letters\r\nanother line\r\n"
                << (QList<QString>() << QLatin1String("9 letters") << QLatin1String("another line"));

        QTest::newRow("a blank line " + codecName)
                << codecName
                << "one\r\n\r\ntwo\r\n"
                << (QList<QString>() << QLatin1String("one") << QLatin1String("two"));

        QTest::newRow("folded lines " + codecName)
                << codecName
                << "folded\r\n  line\r\nsecond line\r\n"
                << (QList<QString>() << QLatin1String("folded line") << QLatin1String("second line"));

        QTest::newRow("multiply folded lines " + codecName)
                << codecName
                << "fo\r\n lded\r\n  line\r\nseco\r\n\tnd l\r\n ine\r\n"
                << (QList<QString>() << QLatin1String("folded line") << QLatin1String("second line"));

        QTest::newRow("fold hidden after a chunk " + codecName)
                << codecName
                << "8letters\r\n  on one line\r\n"
                << (QList<QString>() << QLatin1String("8letters on one line"));

        QTest::newRow("three mac lines " + codecName)
                << codecName
                << "one\rtwo\rthree\r"
                << (QList<QString>() << QLatin1String("one") << QLatin1String("two") << QLatin1String("three"));
    }
#endif
}

void tst_QVersitReader::testByteArrayInput()
{
    delete mReader;
    const QByteArray& oneDocument =
        "BEGIN:VCARD\r\nVERSION:2.1\r\nFN:John\r\nEND:VCARD\r\n";

    mReader = new QVersitReader(oneDocument);
    QVERIFY(mReader->device() == 0);
    QVERIFY(mReader->startReading());
    QVERIFY(mReader->waitForFinished());
    QList<QVersitDocument> results = mReader->results();
    QCOMPARE(mReader->state(), QVersitReader::FinishedState);
    QCOMPARE(mReader->error(), QVersitReader::NoError);
    QCOMPARE(results.count(),1);
    QVersitDocument result = results.first();
    QCOMPARE(result.type(), QVersitDocument::VCard21Type);
    QList<QVersitProperty> properties = result.properties();
    QCOMPARE(properties.length(), 1);
    QCOMPARE(properties.first().name(), QLatin1String("FN"));
    QCOMPARE(properties.first().value(), QLatin1String("John"));
}

void tst_QVersitReader::testRemoveBackSlashEscaping()
{
#ifndef QT_BUILD_INTERNAL
    QSKIP("Testing private API", SkipSingle);
#else
    // Empty string
    QString input;
    QVersitReaderPrivate::removeBackSlashEscaping(input);
    QCOMPARE(input,QString());

    // Nothing to escape in the string
    input = QString::fromAscii("Nothing to escape");
    QVersitReaderPrivate::removeBackSlashEscaping(input);
    QCOMPARE(input,QString::fromAscii("Nothing to escape"));

    // Line break, semicolon, backslash and comma in the string
    input = QString::fromAscii("These should be unescaped \\n \\N \\; \\, \\\\");
    QVersitReaderPrivate::removeBackSlashEscaping(input);
    QCOMPARE(input, QString::fromAscii("These should be unescaped \r\n \r\n ; , \\"));

    // Don't remove escaping within quotes
    input = QString::fromAscii("\"Quoted \\n \\N \\; \\,\"");
    QVersitReaderPrivate::removeBackSlashEscaping(input);
    QCOMPARE(input, QString::fromAscii("\"Quoted \\n \\N \\; \\,\""));
#endif
}

QTEST_MAIN(tst_QVersitReader)