// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QTest>
#include <QReadWriteLock>

#include <qatomic.h>
#include <qcoreapplication.h>
#include <qmutex.h>
#include <qthread.h>
#include <qwaitcondition.h>

#define COND_WAIT_TIME 1

class tst_QWaitCondition : public QObject
{
    Q_OBJECT
private slots:
    void wait_QMutex();
    void wait_QReadWriteLock();
    void wakeOne();
    void wakeAll();
    void wait_RaceCondition();
};

static const int iterations = 4;
static const int ThreadCount = 4;

// Terminate thread in destructor for threads instantiated on the stack
class TerminatingThread : public QThread
{
public:
    explicit TerminatingThread()
    {
        setTerminationEnabled(true);
    }

    ~TerminatingThread()
    {
        if (isRunning()) {
            qWarning() << "forcibly terminating " << objectName();
            terminate();
        }
    }
};

class wait_QMutex_Thread_1 : public TerminatingThread
{
public:
    QMutex mutex;
    QWaitCondition cond;

    inline wait_QMutex_Thread_1()
    { }

    void run() override
    {
        mutex.lock();
        cond.wakeOne();
        cond.wait(&mutex);
        mutex.unlock();
    }
};

class wait_QMutex_Thread_2 : public TerminatingThread
{
public:
    QWaitCondition started;

    QMutex *mutex;
    QWaitCondition *cond;

    inline wait_QMutex_Thread_2()
    : mutex(nullptr), cond(nullptr)
    { }

    void run() override
    {
        mutex->lock();
        started.wakeOne();
        cond->wait(mutex);
        mutex->unlock();
    }
};

class wait_QReadWriteLock_Thread_1 : public TerminatingThread
{
public:
    QReadWriteLock readWriteLock;
    QWaitCondition cond;

    inline wait_QReadWriteLock_Thread_1()
    { }

    void run() override
    {
        readWriteLock.lockForWrite();
        cond.wakeOne();
        cond.wait(&readWriteLock);
        readWriteLock.unlock();
    }
};

class wait_QReadWriteLock_Thread_2 : public TerminatingThread
{
public:
    QWaitCondition started;

    QReadWriteLock *readWriteLock;
    QWaitCondition *cond;

    inline wait_QReadWriteLock_Thread_2()
    : readWriteLock(nullptr), cond(nullptr)
    { }

    void run() override
    {
        readWriteLock->lockForRead();
        started.wakeOne();
        cond->wait(readWriteLock);
        readWriteLock->unlock();
    }
};

void tst_QWaitCondition::wait_QMutex()
{
    int x;
    for (int i = 0; i < iterations; ++i) {
        {
            QMutex mutex;
            QWaitCondition cond;

            mutex.lock();

            cond.wakeOne();
            QVERIFY(!cond.wait(&mutex, 1));

            cond.wakeAll();
            QVERIFY(!cond.wait(&mutex, 1));

            mutex.unlock();
        }

        {
            // test multiple threads waiting on separate wait conditions
            wait_QMutex_Thread_1 thread[ThreadCount];

            const QString prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_mutex_")
                + QString::number(i) + QLatin1Char('_');

            for (x = 0; x < ThreadCount; ++x) {
                thread[x].setObjectName(prefix + QString::number(x));
                thread[x].mutex.lock();
                thread[x].start();
                // wait for thread to start
                QVERIFY(thread[x].cond.wait(&thread[x].mutex, 1000));
                thread[x].mutex.unlock();
            }

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].isRunning());
                QVERIFY(!thread[x].isFinished());
            }

            for (x = 0; x < ThreadCount; ++x) {
                thread[x].mutex.lock();
                thread[x].cond.wakeOne();
                thread[x].mutex.unlock();
            }

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].wait(1000));
            }
        }

        {
            // test multiple threads waiting on a wait condition
            QMutex mutex;
            QWaitCondition cond1, cond2;
            wait_QMutex_Thread_2 thread[ThreadCount];

            const QString prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_mutex_")
                + QString::number(i) + QLatin1Char('_');

            mutex.lock();
            for (x = 0; x < ThreadCount; ++x) {
                thread[x].setObjectName(prefix + QString::number(x));
                thread[x].mutex = &mutex;
                thread[x].cond = (x < ThreadCount / 2) ? &cond1 : &cond2;
                thread[x].start();
                // wait for thread to start
                QVERIFY(thread[x].started.wait(&mutex, 1000));
            }
            mutex.unlock();

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].isRunning());
                QVERIFY(!thread[x].isFinished());
            }

            mutex.lock();
            cond1.wakeAll();
            cond2.wakeAll();
            mutex.unlock();

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].wait(1000));
            }
        }
    }
}

void tst_QWaitCondition::wait_QReadWriteLock()
{
    {
        QReadWriteLock readWriteLock(QReadWriteLock::Recursive);
        QWaitCondition waitCondition;

        // ensure that the lockForRead is correctly restored
        readWriteLock.lockForRead();

        QVERIFY(!waitCondition.wait(&readWriteLock, 1));

        QVERIFY(!readWriteLock.tryLockForWrite());
        QVERIFY(readWriteLock.tryLockForRead());
        readWriteLock.unlock();
        QVERIFY(!readWriteLock.tryLockForWrite());
        readWriteLock.unlock();

        QVERIFY(readWriteLock.tryLockForWrite());
        readWriteLock.unlock();
    }

    {
        QReadWriteLock readWriteLock(QReadWriteLock::Recursive);
        QWaitCondition waitCondition;

        // ensure that the lockForWrite is correctly restored
        readWriteLock.lockForWrite();

        QVERIFY(!waitCondition.wait(&readWriteLock, 1));

        QVERIFY(!readWriteLock.tryLockForRead());
        QVERIFY(readWriteLock.tryLockForWrite());
        readWriteLock.unlock();
        QVERIFY(!readWriteLock.tryLockForRead());
        readWriteLock.unlock();

        QVERIFY(readWriteLock.tryLockForRead());
        readWriteLock.unlock();
    }


    int x;
    for (int i = 0; i < iterations; ++i) {
        {
            QReadWriteLock readWriteLock;
            QWaitCondition waitCondition;

            readWriteLock.lockForRead();

            waitCondition.wakeOne();
            QVERIFY(!waitCondition.wait(&readWriteLock, 1));

            waitCondition.wakeAll();
            QVERIFY(!waitCondition.wait(&readWriteLock, 1));

            readWriteLock.unlock();
        }

        {
            QReadWriteLock readWriteLock;
            QWaitCondition waitCondition;

            readWriteLock.lockForWrite();

            waitCondition.wakeOne();
            QVERIFY(!waitCondition.wait(&readWriteLock, 1));

            waitCondition.wakeAll();
            QVERIFY(!waitCondition.wait(&readWriteLock, 1));

            readWriteLock.unlock();
        }

        {
            // test multiple threads waiting on separate wait conditions
            wait_QReadWriteLock_Thread_1 thread[ThreadCount];

            const QString prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_lockforread_");

            for (x = 0; x < ThreadCount; ++x) {
                thread[x].setObjectName(prefix + QString::number(x));
                thread[x].readWriteLock.lockForRead();
                thread[x].start();
                // wait for thread to start
                QVERIFY(thread[x].cond.wait(&thread[x].readWriteLock, 1000));
                thread[x].readWriteLock.unlock();
            }

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].isRunning());
                QVERIFY(!thread[x].isFinished());
            }

            for (x = 0; x < ThreadCount; ++x) {
                thread[x].readWriteLock.lockForRead();
                thread[x].cond.wakeOne();
                thread[x].readWriteLock.unlock();
            }

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].wait(1000));
            }
        }

        {
            // test multiple threads waiting on a wait condition
            QReadWriteLock readWriteLock;
            QWaitCondition cond1, cond2;
            wait_QReadWriteLock_Thread_2 thread[ThreadCount];

            const QString prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_lockforwrite_");

            readWriteLock.lockForWrite();
            for (x = 0; x < ThreadCount; ++x) {
                thread[x].setObjectName(prefix + QString::number(x));
                thread[x].readWriteLock = &readWriteLock;
                thread[x].cond = (x < ThreadCount / 2) ? &cond1 : &cond2;
                thread[x].start();
                // wait for thread to start
                QVERIFY(thread[x].started.wait(&readWriteLock, 1000));
            }
            readWriteLock.unlock();

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].isRunning());
                QVERIFY(!thread[x].isFinished());
            }

            readWriteLock.lockForWrite();
            cond1.wakeAll();
            cond2.wakeAll();
            readWriteLock.unlock();

            for (x = 0; x < ThreadCount; ++x) {
                QVERIFY(thread[x].wait(1000));
            }
        }
    }
}

class WakeThreadBase : public TerminatingThread
{
public:
    QAtomicInt *count;

    WakeThreadBase() : count(nullptr) {}
};

class wake_Thread : public WakeThreadBase
{
public:
    QWaitCondition started;
    QWaitCondition dummy;

    QMutex *mutex;
    QWaitCondition *cond;

    inline wake_Thread()
    : mutex(nullptr), cond(nullptr)
    { }

    static inline void sleep(ulong s)
    { QThread::sleep(std::chrono::seconds{s}); }

    void run() override
    {
        Q_ASSERT(count);
        Q_ASSERT(mutex);
        Q_ASSERT(cond);
        mutex->lock();
        ++*count;
        dummy.wakeOne(); // this wakeup should be lost
        started.wakeOne();
        dummy.wakeAll(); // this one too
        cond->wait(mutex);
        --*count;
        mutex->unlock();
    }
};

class wake_Thread_2 : public WakeThreadBase
{
public:
    QWaitCondition started;
    QWaitCondition dummy;

    QReadWriteLock *readWriteLock;
    QWaitCondition *cond;

    inline wake_Thread_2()
    : readWriteLock(nullptr), cond(nullptr)
    { }

    static inline void sleep(ulong s)
    { QThread::sleep(std::chrono::seconds{s}); }

    void run() override
    {
        Q_ASSERT(count);
        Q_ASSERT(readWriteLock);
        Q_ASSERT(cond);
        readWriteLock->lockForWrite();
        ++*count;
        dummy.wakeOne(); // this wakeup should be lost
        started.wakeOne();
        dummy.wakeAll(); // this one too
        cond->wait(readWriteLock);
        --*count;
        readWriteLock->unlock();
    }
};

void tst_QWaitCondition::wakeOne()
{
    static const int firstWaitInterval = 1000;
    static const int waitInterval = 30;

    int x;
    QAtomicInt count;
    // wake up threads, one at a time
    for (int i = 0; i < iterations; ++i) {
        QMutex mutex;
        QWaitCondition cond;

        // QMutex
        wake_Thread thread[ThreadCount];
        bool thread_exited[ThreadCount];

        QString prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_mutex_")
            + QString::number(i) + QLatin1Char('_');

        mutex.lock();
        for (x = 0; x < ThreadCount; ++x) {
            thread[x].setObjectName(prefix + QString::number(x));
            thread[x].count = &count;
            thread[x].mutex = &mutex;
            thread[x].cond = &cond;
            thread_exited[x] = false;
            thread[x].start();
            // wait for thread to start
            QVERIFY(thread[x].started.wait(&mutex, 1000));
            // make sure wakeups are not queued... if nothing is
            // waiting at the time of the wakeup, nothing happens
            QVERIFY(!thread[x].dummy.wait(&mutex, 1));
        }
        mutex.unlock();

        QCOMPARE(count.loadRelaxed(), ThreadCount);

        // wake up threads one at a time
        for (x = 0; x < ThreadCount; ++x) {
            mutex.lock();
            cond.wakeOne();
            QVERIFY(!cond.wait(&mutex, COND_WAIT_TIME));
            QVERIFY(!thread[x].dummy.wait(&mutex, 1));
            mutex.unlock();

            int exited = 0;
            for (int y = 0; y < ThreadCount; ++y) {
                if (thread_exited[y])
                    continue;
                if (thread[y].wait(exited > 0 ? waitInterval : firstWaitInterval)) {
                    thread_exited[y] = true;
                    ++exited;
                }
            }

            QCOMPARE(exited, 1);
            QCOMPARE(count.loadRelaxed(), ThreadCount - (x + 1));
        }

        QCOMPARE(count.loadRelaxed(), 0);

        // QReadWriteLock
        QReadWriteLock readWriteLock;
        wake_Thread_2 rwthread[ThreadCount];

        prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_readwritelock_")
            + QString::number(i) + QLatin1Char('_');

        readWriteLock.lockForWrite();
        for (x = 0; x < ThreadCount; ++x) {
            rwthread[x].setObjectName(prefix + QString::number(x));
            rwthread[x].count = &count;
            rwthread[x].readWriteLock = &readWriteLock;
            rwthread[x].cond = &cond;
            thread_exited[x] = false;
            rwthread[x].start();
            // wait for thread to start
            QVERIFY(rwthread[x].started.wait(&readWriteLock, 1000));
            // make sure wakeups are not queued... if nothing is
            // waiting at the time of the wakeup, nothing happens
            QVERIFY(!rwthread[x].dummy.wait(&readWriteLock, 1));
        }
        readWriteLock.unlock();

        QCOMPARE(count.loadRelaxed(), ThreadCount);

        // wake up threads one at a time
        for (x = 0; x < ThreadCount; ++x) {
            readWriteLock.lockForWrite();
            cond.wakeOne();
            QVERIFY(!cond.wait(&readWriteLock, COND_WAIT_TIME));
            QVERIFY(!rwthread[x].dummy.wait(&readWriteLock, 1));
            readWriteLock.unlock();

            int exited = 0;
            for (int y = 0; y < ThreadCount; ++y) {
                if (thread_exited[y])
                    continue;
                if (rwthread[y].wait(exited > 0 ? waitInterval : firstWaitInterval)) {
                    thread_exited[y] = true;
                    ++exited;
                }
            }

            QCOMPARE(exited, 1);
            QCOMPARE(count.loadRelaxed(), ThreadCount - (x + 1));
        }

        QCOMPARE(count.loadRelaxed(), 0);
    }

    // wake up threads, two at a time
    for (int i = 0; i < iterations; ++i) {
        QMutex mutex;
        QWaitCondition cond;

        // QMutex
        wake_Thread thread[ThreadCount];
        bool thread_exited[ThreadCount];

        QString prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_mutex2_")
            + QString::number(i) + QLatin1Char('_');

        mutex.lock();
        for (x = 0; x < ThreadCount; ++x) {
            thread[x].setObjectName(prefix + QString::number(x));
            thread[x].count = &count;
            thread[x].mutex = &mutex;
            thread[x].cond = &cond;
            thread_exited[x] = false;
            thread[x].start();
            // wait for thread to start
            QVERIFY(thread[x].started.wait(&mutex, 1000));
            // make sure wakeups are not queued... if nothing is
            // waiting at the time of the wakeup, nothing happens
            QVERIFY(!thread[x].dummy.wait(&mutex, 1));
        }
        mutex.unlock();

        QCOMPARE(count.loadRelaxed(), ThreadCount);

        // wake up threads one at a time
        for (x = 0; x < ThreadCount; x += 2) {
            mutex.lock();
            cond.wakeOne();
            cond.wakeOne();
            QVERIFY(!cond.wait(&mutex, COND_WAIT_TIME));
            QVERIFY(!thread[x].dummy.wait(&mutex, 1));
            QVERIFY(!thread[x + 1].dummy.wait(&mutex, 1));
            mutex.unlock();

            int exited = 0;
            for (int y = 0; y < ThreadCount; ++y) {
                if (thread_exited[y])
                    continue;
                if (thread[y].wait(exited > 0 ? waitInterval : firstWaitInterval)) {
                    thread_exited[y] = true;
                    ++exited;
                }
            }

            QCOMPARE(exited, 2);
            QCOMPARE(count.loadRelaxed(), ThreadCount - (x + 2));
        }

        QCOMPARE(count.loadRelaxed(), 0);

        // QReadWriteLock
        QReadWriteLock readWriteLock;
        wake_Thread_2 rwthread[ThreadCount];

        prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_readwritelock_")
            + QString::number(i) + QLatin1Char('_');

        readWriteLock.lockForWrite();
        for (x = 0; x < ThreadCount; ++x) {
            rwthread[x].setObjectName(prefix + QString::number(x));
            rwthread[x].count = &count;
            rwthread[x].readWriteLock = &readWriteLock;
            rwthread[x].cond = &cond;
            thread_exited[x] = false;
            rwthread[x].start();
            // wait for thread to start
            QVERIFY(rwthread[x].started.wait(&readWriteLock, 1000));
            // make sure wakeups are not queued... if nothing is
            // waiting at the time of the wakeup, nothing happens
            QVERIFY(!rwthread[x].dummy.wait(&readWriteLock, 1));
        }
        readWriteLock.unlock();

        QCOMPARE(count.loadRelaxed(), ThreadCount);

        // wake up threads one at a time
        for (x = 0; x < ThreadCount; x += 2) {
            readWriteLock.lockForWrite();
            cond.wakeOne();
            cond.wakeOne();
            QVERIFY(!cond.wait(&readWriteLock, COND_WAIT_TIME));
            QVERIFY(!rwthread[x].dummy.wait(&readWriteLock, 1));
            QVERIFY(!rwthread[x + 1].dummy.wait(&readWriteLock, 1));
            readWriteLock.unlock();

            int exited = 0;
            for (int y = 0; y < ThreadCount; ++y) {
                if (thread_exited[y])
                    continue;
                if (rwthread[y].wait(exited > 0 ? waitInterval : firstWaitInterval)) {
                    thread_exited[y] = true;
                    ++exited;
                }
            }

            QCOMPARE(exited, 2);
            QCOMPARE(count.loadRelaxed(), ThreadCount - (x + 2));
        }

        QCOMPARE(count.loadRelaxed(), 0);
    }
}

void tst_QWaitCondition::wakeAll()
{
    int x;
    QAtomicInt count;
    for (int i = 0; i < iterations; ++i) {
        QMutex mutex;
        QWaitCondition cond;

        // QMutex
        wake_Thread thread[ThreadCount];

        QString prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_mutex_")
            + QString::number(i) + QLatin1Char('_');

        mutex.lock();
        for (x = 0; x < ThreadCount; ++x) {
            thread[x].setObjectName(prefix + QString::number(x));
            thread[x].count = &count;
            thread[x].mutex = &mutex;
            thread[x].cond = &cond;
            thread[x].start();
            // wait for thread to start
            QVERIFY(thread[x].started.wait(&mutex, 1000));
        }
        mutex.unlock();

        QCOMPARE(count.loadRelaxed(), ThreadCount);

        // wake up all threads at once
        mutex.lock();
        cond.wakeAll();
        QVERIFY(!cond.wait(&mutex, COND_WAIT_TIME));
        mutex.unlock();

        int exited = 0;
        for (x = 0; x < ThreadCount; ++x) {
            if (thread[x].wait(1000))
            ++exited;
        }

        QCOMPARE(exited, ThreadCount);
        QCOMPARE(count.loadRelaxed(), 0);

        // QReadWriteLock
        QReadWriteLock readWriteLock;
        wake_Thread_2 rwthread[ThreadCount];

        prefix = QLatin1String(QTest::currentTestFunction()) + QLatin1String("_readwritelock_")
            + QString::number(i) + QLatin1Char('_');

        readWriteLock.lockForWrite();
        for (x = 0; x < ThreadCount; ++x) {
            rwthread[x].setObjectName(prefix + QString::number(x));
            rwthread[x].count = &count;
            rwthread[x].readWriteLock = &readWriteLock;
            rwthread[x].cond = &cond;
            rwthread[x].start();
            // wait for thread to start
            QVERIFY(rwthread[x].started.wait(&readWriteLock, 1000));
        }
        readWriteLock.unlock();

        QCOMPARE(count.loadRelaxed(), ThreadCount);

        // wake up all threads at once
        readWriteLock.lockForWrite();
        cond.wakeAll();
        QVERIFY(!cond.wait(&readWriteLock, COND_WAIT_TIME));
        readWriteLock.unlock();

        exited = 0;
        for (x = 0; x < ThreadCount; ++x) {
            if (rwthread[x].wait(1000))
            ++exited;
        }

        QCOMPARE(exited, ThreadCount);
        QCOMPARE(count.loadRelaxed(), 0);
    }
}

class wait_RaceConditionThread : public TerminatingThread
{
public:
    wait_RaceConditionThread(QMutex *mutex, QWaitCondition *startup, QWaitCondition *waitCondition,
                             ulong timeout = ULONG_MAX)
        : timeout(timeout), returnValue(false), ready(false),
          mutex(mutex), startup(startup), waitCondition(waitCondition) {}

    unsigned long timeout;
    bool returnValue;

    bool ready;

    QMutex *mutex;
    QWaitCondition *startup;
    QWaitCondition *waitCondition;

    void run() override
    {
        mutex->lock();

        ready = true;
        startup->wakeOne();

        returnValue = waitCondition->wait(mutex, timeout);

        mutex->unlock();
    }
};

class wait_RaceConditionThread_2 : public TerminatingThread
{
public:
    wait_RaceConditionThread_2(QReadWriteLock *readWriteLock,
                               QWaitCondition *startup,
                               QWaitCondition *waitCondition,
                               ulong timeout = ULONG_MAX)
        : timeout(timeout), returnValue(false), ready(false),
          readWriteLock(readWriteLock), startup(startup), waitCondition(waitCondition)
    { }

    unsigned long timeout;
    bool returnValue;

    bool ready;

    QReadWriteLock *readWriteLock;
    QWaitCondition *startup;
    QWaitCondition *waitCondition;

    void run() override
    {
        readWriteLock->lockForWrite();

        ready = true;
        startup->wakeOne();

        returnValue = waitCondition->wait(readWriteLock, timeout);

        readWriteLock->unlock();
    }
};

void tst_QWaitCondition::wait_RaceCondition()
{
    {
        QMutex mutex;
        QWaitCondition startup;
        QWaitCondition waitCondition;

        wait_RaceConditionThread timeoutThread(&mutex, &startup, &waitCondition, 1000),
            waitingThread1(&mutex, &startup, &waitCondition);

        timeoutThread.start();
        waitingThread1.start();
        mutex.lock();

        // wait for the threads to start up
        while (!timeoutThread.ready
               || !waitingThread1.ready) {
            startup.wait(&mutex);
        }

        QTest::qWait(2000);

        waitCondition.wakeOne();

        mutex.unlock();

        QVERIFY(timeoutThread.wait(5000));
        QVERIFY(!timeoutThread.returnValue);
        QVERIFY(waitingThread1.wait(5000));
        QVERIFY(waitingThread1.returnValue);
    }

    {
        QReadWriteLock readWriteLock;
        QWaitCondition startup;
        QWaitCondition waitCondition;

        wait_RaceConditionThread_2 timeoutThread(&readWriteLock, &startup, &waitCondition, 1000),
            waitingThread1(&readWriteLock, &startup, &waitCondition);

        timeoutThread.start();
        waitingThread1.start();
        readWriteLock.lockForRead();

        // wait for the threads to start up
        while (!timeoutThread.ready
               || !waitingThread1.ready) {
            startup.wait(&readWriteLock);
        }

        QTest::qWait(2000);

        waitCondition.wakeOne();

        readWriteLock.unlock();

        QVERIFY(timeoutThread.wait(5000));
        QVERIFY(!timeoutThread.returnValue);
        QVERIFY(waitingThread1.wait(5000));
        QVERIFY(waitingThread1.returnValue);
    }
}

QTEST_MAIN(tst_QWaitCondition)
#include "tst_qwaitcondition.moc"
