/****************************************************************************
**
** 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 test suite of the Qt Toolkit.
**
** $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 <QTest>

#include <QtAlgorithms>
#include <QFile>
#include <QFileInfo>
#include <qplatformdefs.h>

#include <QDebug>

#include <cstdlib>
#include <cstdio>

#ifdef Q_OS_WIN

#include <windows.h>

#ifndef Q_OS_WINCE
#include <io.h>
#endif

#ifndef FSCTL_SET_SPARSE
// MinGW doesn't define this.
#define FSCTL_SET_SPARSE (0x900C4)
#endif

#endif // Q_OS_WIN

class tst_LargeFile
    : public QObject
{
    Q_OBJECT

public:
    tst_LargeFile()
        : blockSize(1 << 12)
        , maxSizeBits()
        , fd_(-1)
        , stream_(0)
    {
    #if defined(QT_LARGEFILE_SUPPORT) && !defined(Q_OS_MAC)
        maxSizeBits = 36; // 64 GiB
    #elif defined(Q_OS_MAC)
        // HFS+ does not support sparse files, so we limit file size for the test
        // on Mac OS.
        maxSizeBits = 32; // 4 GiB
    #else
        maxSizeBits = 24; // 16 MiB
    #endif
    }

private:
    void sparseFileData();
    QByteArray const &getDataBlock(int index, qint64 position);

private slots:
    // The LargeFile test case was designed to be run in order as a single unit

    void initTestCase();
    void cleanupTestCase();

    void init();
    void cleanup();

    // Create and fill large file
    void createSparseFile();
    void fillFileSparsely();
    void closeSparseFile();

    // Verify file was created
    void fileCreated();

    // Positioning in large files
    void filePositioning();
    void fdPositioning();
    void streamPositioning();

    // Read data from file
    void openFileForReading();
    void readFile();

    // Map/unmap large file
    void mapFile();
    void mapOffsetOverflow();

    void closeFile() { largeFile.close(); }

    // Test data
    void fillFileSparsely_data() { sparseFileData(); }
    void filePositioning_data() { sparseFileData(); }
    void fdPositioning_data() { sparseFileData(); }
    void streamPositioning_data() { sparseFileData(); }
    void readFile_data() { sparseFileData(); }
    void mapFile_data() { sparseFileData(); }

private:
    const int blockSize;
    int maxSizeBits;

    QFile largeFile;

    QVector<QByteArray> generatedBlocks;

    int fd_;
    FILE *stream_;
};

/*
    Convenience function to hide reinterpret_cast when copying a POD directly
    into a QByteArray.
 */
template <class T>
static inline void appendRaw(QByteArray &array, T data)
{
    array.append(reinterpret_cast<char *>(&data), sizeof(T));
}

/*
    Pad array with filler up to size. On return, array.size() returns size.
 */
static inline void topUpWith(QByteArray &array, QByteArray filler, int size)
{
    Q_ASSERT(filler.size() > 0);

    for (int i = (size - array.size()) / filler.size(); i > 0; --i)
        array.append(filler);

    if (array.size() < size) {
        Q_ASSERT(size - array.size() < filler.size());
        array.append(filler.left(size - array.size()));
    }
}

/*
   Generate a unique data block containing identifiable data. Unaligned,
   overlapping and partial blocks should not compare equal.
 */
static inline QByteArray generateDataBlock(int blockSize, QString text, qint64 userBits = -1)
{
    QByteArray block;
    block.reserve(blockSize);

    // Use of counter and randomBits means content of block will be dependent
    // on the generation order. For (file-)systems that do not support sparse
    // files, these can be removed so the test file can be reused and doesn't
    // have to be generated for every run.

    static qint64 counter = 0;

    qint64 randomBits = ((qint64)qrand() << 32)
            | ((qint64)qrand() & 0x00000000ffffffff);

    appendRaw(block, randomBits);
    appendRaw(block, userBits);
    appendRaw(block, counter);
    appendRaw(block, (qint32)0xdeadbeef);
    appendRaw(block, blockSize);

    QByteArray userContent = text.toUtf8();
    appendRaw(block, userContent.size());
    block.append(userContent);
    appendRaw(block, (qint64)0);

    // size, so far
    appendRaw(block, block.size());

    QByteArray filler("0123456789");
    block.append(filler.right(10 - block.size() % 10));
    topUpWith(block, filler, blockSize - 2 * sizeof(qint64));

    appendRaw(block, counter);
    appendRaw(block, userBits);
    appendRaw(block, randomBits);

    Q_ASSERT( block.size() >= blockSize );
    block.resize(blockSize);

    ++counter;
    return block;
}

/*
   Generates data blocks the first time they are requested. Keeps copies for reuse.
 */
QByteArray const &tst_LargeFile::getDataBlock(int index, qint64 position)
{
    if (index >= generatedBlocks.size())
        generatedBlocks.resize(index + 1);

    if (generatedBlocks[index].isNull()) {
        QString text = QString("Current %1-byte block (index = %2) "
            "starts %3 bytes into the file '%4'.")
                .arg(blockSize)
                .arg(index)
                .arg(position)
                .arg("qt_largefile.tmp");

        generatedBlocks[index] = generateDataBlock(blockSize, text, (qint64)1 << index);
    }

    return generatedBlocks[index];
}

void tst_LargeFile::initTestCase()
{
    QFile file("qt_largefile.tmp");
    QVERIFY( !file.exists() || file.remove() );
}

void tst_LargeFile::cleanupTestCase()
{
    if (largeFile.isOpen())
        largeFile.close();

    QFile file("qt_largefile.tmp");
    QVERIFY( !file.exists() || file.remove() );
}

void tst_LargeFile::init()
{
    fd_ = -1;
    stream_ = 0;
}

void tst_LargeFile::cleanup()
{
    if (-1 != fd_)
        QT_CLOSE(fd_);
    if (stream_)
        ::fclose(stream_);
}

void tst_LargeFile::sparseFileData()
{
    QTest::addColumn<int>("index");
    QTest::addColumn<qint64>("position");
    QTest::addColumn<QByteArray>("block");

    QTest::newRow(QString("block[%1] @%2)")
            .arg(0).arg(0)
            .toLocal8Bit().constData())
        << 0 << (qint64)0 << getDataBlock(0, 0);

    // While on Linux sparse files scale well, on Windows, testing at every
    // power of 2 leads to very large files. i += 4 gives us a good coverage
    // without taxing too much on resources.
    for (int index = 12; index <= maxSizeBits; index += 4) {
        qint64 position = (qint64)1 << index;
        QByteArray block = getDataBlock(index, position);

        QTest::newRow(
            QString("block[%1] @%2)")
                .arg(index).arg(position)
                .toLocal8Bit().constData())
            << index << position << block;
    }
}

void tst_LargeFile::createSparseFile()
{
#if defined(Q_OS_WIN32)
    // On Windows platforms, we must explicitly set the file to be sparse,
    // so disk space is not allocated for the full file when writing to it.
    HANDLE handle = ::CreateFileA("qt_largefile.tmp",
        GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0);
    QVERIFY( INVALID_HANDLE_VALUE != handle );

    DWORD bytes;
    if (!::DeviceIoControl(handle, FSCTL_SET_SPARSE, NULL, 0, NULL, 0,
            &bytes, NULL)) {
        QWARN("Unable to set test file as sparse. "
            "Limiting test file to 16MiB.");
        maxSizeBits = 24;
    }

    int fd = ::_open_osfhandle((intptr_t)handle, 0);
    QVERIFY( -1 != fd );
    QVERIFY( largeFile.open(fd, QIODevice::WriteOnly | QIODevice::Unbuffered) );
#else // !Q_OS_WIN32
    largeFile.setFileName("qt_largefile.tmp");
    QVERIFY( largeFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered) );
#endif
}

void tst_LargeFile::closeSparseFile()
{
#if defined(Q_OS_WIN32)
    int fd = largeFile.handle();
#endif

    largeFile.close();

#if defined(Q_OS_WIN32)
    if (-1 != fd)
        ::_close(fd);
#endif
}

void tst_LargeFile::fillFileSparsely()
{
    QFETCH( qint64, position );
    QFETCH( QByteArray, block );
    QCOMPARE( block.size(), blockSize );

    static int lastKnownGoodIndex = 0;
    struct ScopeGuard {
        ScopeGuard(tst_LargeFile* test)
            : this_(test)
            , failed(true)
        {
            QFETCH( int, index );
            index_ = index;
        }

        ~ScopeGuard()
        {
            if (failed) {
                this_->maxSizeBits = lastKnownGoodIndex;
                QWARN( qPrintable(
                    QString("QFile::error %1: '%2'. Maximum size bits reset to %3.")
                        .arg(this_->largeFile.error())
                        .arg(this_->largeFile.errorString())
                        .arg(this_->maxSizeBits)) );
            } else
                lastKnownGoodIndex = qMax<int>(index_, lastKnownGoodIndex);
        }

    private:
        tst_LargeFile * const this_;
        int index_;

    public:
        bool failed;
    };

    ScopeGuard resetMaxSizeBitsOnFailure(this);

    QVERIFY( largeFile.seek(position) );
    QCOMPARE( largeFile.pos(), position );

    QCOMPARE( largeFile.write(block), (qint64)blockSize );
    QCOMPARE( largeFile.pos(), position + blockSize );
    QVERIFY( largeFile.flush() );

    resetMaxSizeBitsOnFailure.failed = false;
}

void tst_LargeFile::fileCreated()
{
    QFileInfo info("qt_largefile.tmp");

    QVERIFY( info.exists() );
    QVERIFY( info.isFile() );
    QVERIFY( info.size() >= ((qint64)1 << maxSizeBits) + blockSize );
}

void tst_LargeFile::filePositioning()
{
    QFETCH( qint64, position );

    QFile file("qt_largefile.tmp");
    QVERIFY( file.open(QIODevice::ReadOnly) );

    QVERIFY( file.seek(position) );
    QCOMPARE( file.pos(), position );
}

void tst_LargeFile::fdPositioning()
{
    QFETCH( qint64, position );

    fd_ = QT_OPEN("qt_largefile.tmp",
            QT_OPEN_RDONLY | QT_OPEN_LARGEFILE);
    QVERIFY( -1 != fd_ );

    QFile file;
    QVERIFY( file.open(fd_, QIODevice::ReadOnly) );
    QCOMPARE( file.pos(), (qint64)0 );
    QVERIFY( file.seek(position) );
    QCOMPARE( file.pos(), position );

    file.close();

    QCOMPARE( QT_LSEEK(fd_, QT_OFF_T(0), SEEK_SET), QT_OFF_T(0) );
    QCOMPARE( QT_LSEEK(fd_, QT_OFF_T(position), SEEK_SET), QT_OFF_T(position) );

    QVERIFY( file.open(fd_, QIODevice::ReadOnly) );
    QCOMPARE( QT_LSEEK(fd_, QT_OFF_T(0), SEEK_CUR), QT_OFF_T(position) );
    QCOMPARE( file.pos(), position );
    QVERIFY( file.seek(0) );
    QCOMPARE( file.pos(), (qint64)0 );

    file.close();

    QVERIFY( !QT_CLOSE(fd_) );
    fd_ = -1;
}

void tst_LargeFile::streamPositioning()
{
    QFETCH( qint64, position );

#if defined(QT_LARGEFILE_SUPPORT) && defined(Q_CC_MSVC) && _MSC_VER < 1400
    if (position >= (qint64)1 << 31)
        QSKIP("MSVC 2003 doesn't have 64 bit versions of fseek/ftell.", SkipSingle);
#endif

    stream_ = QT_FOPEN("qt_largefile.tmp", "rb");
    QVERIFY( 0 != stream_ );

    QFile file;
    QVERIFY( file.open(stream_, QIODevice::ReadOnly) );
    QCOMPARE( file.pos(), (qint64)0 );
    QVERIFY( file.seek(position) );
    QCOMPARE( file.pos(), position );

    file.close();

    QVERIFY( !QT_FSEEK(stream_, QT_OFF_T(0), SEEK_SET) );
    QCOMPARE( QT_FTELL(stream_), QT_OFF_T(0) );
    QVERIFY( !QT_FSEEK(stream_, QT_OFF_T(position), SEEK_SET) );
    QCOMPARE( QT_FTELL(stream_), QT_OFF_T(position) );

    QVERIFY( file.open(stream_, QIODevice::ReadOnly) );
    QCOMPARE( QT_FTELL(stream_), QT_OFF_T(position) );
    QCOMPARE( file.pos(), position );
    QVERIFY( file.seek(0) );
    QCOMPARE( file.pos(), (qint64)0 );

    file.close();

    QVERIFY( !::fclose(stream_) );
    stream_ = 0;
}

void tst_LargeFile::openFileForReading()
{
    largeFile.setFileName("qt_largefile.tmp");
    QVERIFY( largeFile.open(QIODevice::ReadOnly) );
}

void tst_LargeFile::readFile()
{
    QFETCH( qint64, position );
    QFETCH( QByteArray, block );
    QCOMPARE( block.size(), blockSize );

    QVERIFY( largeFile.size() >= position + blockSize );

    QVERIFY( largeFile.seek(position) );
    QCOMPARE( largeFile.pos(), position );

    QCOMPARE( largeFile.read(blockSize), block );
    QCOMPARE( largeFile.pos(), position + blockSize );
}

void tst_LargeFile::mapFile()
{
    QFETCH( qint64, position );
    QFETCH( QByteArray, block );
    QCOMPARE( block.size(), blockSize );

    // Keep full block mapped to facilitate OS and/or internal reuse by Qt.
    uchar *baseAddress = largeFile.map(position, blockSize);
    QVERIFY( baseAddress );
    QVERIFY( qEqual(block.begin(), block.end(), reinterpret_cast<char*>(baseAddress)) );

    for (int offset = 1; offset <  blockSize; ++offset) {
        uchar *address = largeFile.map(position + offset, blockSize - offset);

        QVERIFY( address );
        if ( !qEqual(block.begin() + offset, block.end(), reinterpret_cast<char*>(address)) ) {
            qDebug() << "Expected:" << block.toHex();
            qDebug() << "Actual  :" << QByteArray(reinterpret_cast<char*>(address), blockSize).toHex();
            QVERIFY(false);
        }

        QVERIFY( largeFile.unmap( address ) );
    }

    QVERIFY( largeFile.unmap( baseAddress ) );
}

void tst_LargeFile::mapOffsetOverflow()
{
    // Out-of-range mappings should fail, and not silently clip the offset
    for (int i = 50; i < 63; ++i) {
        uchar *address = 0;

        address = largeFile.map(((qint64)1 << i), blockSize);
        QVERIFY( !address );

        address = largeFile.map(((qint64)1 << i) + blockSize, blockSize);
        QVERIFY( !address );
    }
}

QTEST_APPLESS_MAIN(tst_LargeFile)
#include "tst_largefile.moc"

