tests/auto/qeventloop/tst_qeventloop.cpp
author Alex Gilkes <alex.gilkes@nokia.com>
Mon, 11 Jan 2010 14:00:40 +0000
changeset 0 1918ee327afb
child 4 3b1da2848fc7
permissions -rw-r--r--
Revision: 200952

/****************************************************************************
**
** 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 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 <QtTest/QtTest>


#include <qabstracteventdispatcher.h>
#include <qcoreapplication.h>
#include <qcoreevent.h>
#include <qeventloop.h>
#include <qmutex.h>
#include <qthread.h>
#include <qtimer.h>
#include <qwaitcondition.h>
#include <QTcpServer>
#include <QTcpSocket>

#ifdef Q_OS_SYMBIAN
#include <e32base.h>
#include <unistd.h>
#endif

//TESTED_CLASS=
//TESTED_FILES=

class EventLoopExiter : public QObject
{
    Q_OBJECT
    QEventLoop *eventLoop;
public:
    inline EventLoopExiter(QEventLoop *el)
        : eventLoop(el)
    { }
public slots:
    void exit();
    void exit1();
    void exit2();
};

void EventLoopExiter::exit()
{ eventLoop->exit(); }

void EventLoopExiter::exit1()
{ eventLoop->exit(1); }

void EventLoopExiter::exit2()
{ eventLoop->exit(2); }

class EventLoopThread : public QThread
{
    Q_OBJECT
signals:
    void checkPoint();
public:
    QEventLoop *eventLoop;
    void run();
};

void EventLoopThread::run()
{
    eventLoop = new QEventLoop;
    emit checkPoint();
    (void) eventLoop->exec();
    delete eventLoop;
    eventLoop = 0;
}

class MultipleExecThread : public QThread
{
    Q_OBJECT
signals:
    void checkPoint();
public:
    QMutex mutex;
    QWaitCondition cond;
    void run()
    {
        QMutexLocker locker(&mutex);
        // this exec should work

        cond.wakeOne();
        cond.wait(&mutex);

        QTimer timer;
        connect(&timer, SIGNAL(timeout()), SLOT(quit()), Qt::DirectConnection);
        timer.setInterval(1000);
        timer.start();
        (void) exec();

        // this should return immediately, since exit() has been called
        cond.wakeOne();
        cond.wait(&mutex);
        QEventLoop eventLoop;
        (void) eventLoop.exec();
    }
};

class StartStopEvent: public QEvent
{
public:
    StartStopEvent(int type, QEventLoop *loop = 0)
        : QEvent(Type(type)), el(loop)
    { }

    QEventLoop *el;
};

class EventLoopExecutor : public QObject
{
    Q_OBJECT
    QEventLoop *eventLoop;
public:
    int returnCode;
    EventLoopExecutor(QEventLoop *eventLoop)
        : QObject(), eventLoop(eventLoop), returnCode(-42)
    {
    }
public slots:
    void exec()
    {
        QTimer::singleShot(100, eventLoop, SLOT(quit()));
        // this should return immediately, and the timer event should be delivered to
        // tst_QEventLoop::exec() test, letting the test complete
        returnCode = eventLoop->exec();
    }
};

#ifndef QT_NO_EXCEPTIONS
class QEventLoopTestException { };

class ExceptionThrower : public QObject
{
    Q_OBJECT
public:
    ExceptionThrower() : QObject() { }
public slots:
    void throwException()
    {
        QEventLoopTestException e;
        throw e;
    }
};
#endif

class tst_QEventLoop : public QObject
{
    Q_OBJECT
public:
    tst_QEventLoop();
    ~tst_QEventLoop();
public slots:
    void init();
    void cleanup();
private slots:
    // This test *must* run first. See the definition for why.
    void onlySymbianActiveScheduler();
    void symbianNestedActiveSchedulerLoop_data();
    void symbianNestedActiveSchedulerLoop();
    void processEvents();
    void exec();
    void exit();
    void wakeUp();
    void quit();
    void processEventsExcludeSocket();
    void processEventsExcludeTimers();

    // keep this test last:
    void nestedLoops();

protected:
    void customEvent(QEvent *e);
};

tst_QEventLoop::tst_QEventLoop()
{ }

tst_QEventLoop::~tst_QEventLoop()
{ }

void tst_QEventLoop::init()
{ }

void tst_QEventLoop::cleanup()
{ }

#ifdef Q_OS_SYMBIAN
class OnlySymbianActiveScheduler_helper : public QObject
{
    Q_OBJECT

public:
    OnlySymbianActiveScheduler_helper(int fd, QTimer *zeroTimer)
        : fd(fd),
          timerCount(0),
          zeroTimer(zeroTimer),
          zeroTimerCount(0),
          notifierCount(0)
    {
    }
    ~OnlySymbianActiveScheduler_helper() {}

public slots:
    void timerSlot()
    {
        // Let all the events occur twice so we know they reactivated after
        // each occurrence.
        if (++timerCount >= 2) {
            // This will hopefully run last, so stop the active scheduler.
            CActiveScheduler::Stop();
        }
    }
    void zeroTimerSlot()
    {
        if (++zeroTimerCount >= 2) {
            zeroTimer->stop();
        }
    }
    void notifierSlot()
    {
        if (++notifierCount >= 2) {
            char dummy;
            ::read(fd, &dummy, 1);
        }
    }

private:
    int fd;
    int timerCount;
    QTimer *zeroTimer;
    int zeroTimerCount;
    int notifierCount;
};
#endif

void tst_QEventLoop::onlySymbianActiveScheduler() {
#ifndef Q_OS_SYMBIAN
    QSKIP("This is a Symbian-only test.", SkipAll);
#else
    // In here we try to use timers and sockets exclusively using the Symbian
    // active scheduler and no processEvents().
    // This test should therefore be run first, so that we can verify that
    // the first occurrence of processEvents does not do any initalization that
    // we depend on.

    // Open up a pipe so we can test socket notifiers.
    int pipeEnds[2];
    if (::pipe(pipeEnds) != 0) {
        QFAIL("Could not open pipe");
    }
    QSocketNotifier notifier(pipeEnds[0], QSocketNotifier::Read);
    QSignalSpy notifierSpy(&notifier, SIGNAL(activated(int)));
    char dummy = 1;
    ::write(pipeEnds[1], &dummy, 1);

    QTimer zeroTimer;
    QSignalSpy zeroTimerSpy(&zeroTimer, SIGNAL(timeout()));
    zeroTimer.setInterval(0);
    zeroTimer.start();

    QTimer timer;
    QSignalSpy timerSpy(&timer, SIGNAL(timeout()));
    timer.setInterval(2000); // Generous timeout or this test will fail if there is high load
    timer.start();

    OnlySymbianActiveScheduler_helper helper(pipeEnds[0], &zeroTimer);
    connect(&notifier, SIGNAL(activated(int)), &helper, SLOT(notifierSlot()));
    connect(&zeroTimer, SIGNAL(timeout()), &helper, SLOT(zeroTimerSlot()));
    connect(&timer, SIGNAL(timeout()), &helper, SLOT(timerSlot()));

    CActiveScheduler::Start();

    ::close(pipeEnds[1]);
    ::close(pipeEnds[0]);

    QCOMPARE(notifierSpy.count(), 2);
    QCOMPARE(zeroTimerSpy.count(), 2);
    QCOMPARE(timerSpy.count(), 2);
#endif
}

void tst_QEventLoop::processEvents()
{
    QSignalSpy spy1(QAbstractEventDispatcher::instance(), SIGNAL(aboutToBlock()));
    QSignalSpy spy2(QAbstractEventDispatcher::instance(), SIGNAL(awake()));

    QEventLoop eventLoop;

    QCoreApplication::postEvent(&eventLoop, new QEvent(QEvent::User));

    // process posted events, QEventLoop::processEvents() should return
    // true
    QVERIFY(eventLoop.processEvents());
    QCOMPARE(spy1.count(), 0);
    QCOMPARE(spy2.count(), 1);

    // allow any session manager to complete its handshake, so that
    // there are no pending events left.
    while (eventLoop.processEvents())
        ;

    // On mac we get application started events at this point,
    // so process events one more time just to be sure.
    eventLoop.processEvents();

    // no events to process, QEventLoop::processEvents() should return
    // false
    spy1.clear();
    spy2.clear();
    QVERIFY(!eventLoop.processEvents());
    QCOMPARE(spy1.count(), 0);
    QCOMPARE(spy2.count(), 1);

    // make sure the test doesn't block forever
    int timerId = startTimer(100);

    // wait for more events to process, QEventLoop::processEvents()
    // should return true
    spy1.clear();
    spy2.clear();
    QVERIFY(eventLoop.processEvents(QEventLoop::WaitForMoreEvents));

    // Verify that the eventloop has blocked and woken up. Some eventloops
    // may block and wake up multiple times.
    QVERIFY(spy1.count() > 0);
    QVERIFY(spy2.count() > 0);
    // We should get one awake for each aboutToBlock, plus one awake when
    // processEvents is entered.
    QVERIFY(spy2.count() >= spy1.count());

    killTimer(timerId);
}

#if defined(Q_OS_SYMBIAN) && defined(Q_CC_NOKIAX86)
// Symbian needs bit longer timeout for emulator, as emulator startup causes additional delay
#  define EXEC_TIMEOUT 1000
#else
#  define EXEC_TIMEOUT 100
#endif


void tst_QEventLoop::exec()
{
    {
        QEventLoop eventLoop;
        EventLoopExiter exiter(&eventLoop);
        int returnCode;

        QTimer::singleShot(EXEC_TIMEOUT, &exiter, SLOT(exit()));
        returnCode = eventLoop.exec();
        QCOMPARE(returnCode, 0);

        QTimer::singleShot(EXEC_TIMEOUT, &exiter, SLOT(exit1()));
        returnCode = eventLoop.exec();
        QCOMPARE(returnCode, 1);

        QTimer::singleShot(EXEC_TIMEOUT, &exiter, SLOT(exit2()));
        returnCode = eventLoop.exec();
        QCOMPARE(returnCode, 2);
    }

    {
        // calling exec() after exit()/quit() should return immediately
        MultipleExecThread thread;

        // start thread and wait for checkpoint
        thread.mutex.lock();
        thread.start();
        thread.cond.wait(&thread.mutex);

        // make sure the eventloop runs
        QSignalSpy spy(QAbstractEventDispatcher::instance(&thread), SIGNAL(awake()));
        thread.cond.wakeOne();
        thread.cond.wait(&thread.mutex);
        QVERIFY(spy.count() > 0);

        // exec should return immediately
        spy.clear();
        thread.cond.wakeOne();
        thread.mutex.unlock();
        thread.wait();
        QCOMPARE(spy.count(), 0);
    }

    {
        // a single instance of QEventLoop should not be allowed to recurse into exec()
        QEventLoop eventLoop;
        EventLoopExecutor executor(&eventLoop);

        QTimer::singleShot(EXEC_TIMEOUT, &executor, SLOT(exec()));
        int returnCode = eventLoop.exec();
        QCOMPARE(returnCode, 0);
        QCOMPARE(executor.returnCode, -1);
    }

#if !defined(QT_NO_EXCEPTIONS) && !defined(Q_OS_WINCE_WM) && !defined(Q_OS_SYMBIAN)
    // Windows Mobile cannot handle cross library exceptions
    // qobject.cpp will try to rethrow the exception after handling
    // which causes gwes.exe to crash

    // Symbian doesn't propagate exceptions from eventloop, but converts them to
    // CActiveScheduler errors instead -> this test will hang.
    {
        // QEventLoop::exec() is exception safe
        QEventLoop eventLoop;
        int caughtExceptions = 0;

        try {
            ExceptionThrower exceptionThrower;
            QTimer::singleShot(EXEC_TIMEOUT, &exceptionThrower, SLOT(throwException()));
            (void) eventLoop.exec();
        } catch (...) {
            ++caughtExceptions;
        }
        try {
            ExceptionThrower exceptionThrower;
            QTimer::singleShot(EXEC_TIMEOUT, &exceptionThrower, SLOT(throwException()));
            (void) eventLoop.exec();
        } catch (...) {
            ++caughtExceptions;
        }
        QCOMPARE(caughtExceptions, 2);
    }
#endif
}

void tst_QEventLoop::exit()
{ DEPENDS_ON(exec()); }

void tst_QEventLoop::wakeUp()
{
    EventLoopThread thread;
    QEventLoop eventLoop;
    connect(&thread, SIGNAL(checkPoint()), &eventLoop, SLOT(quit()));
    connect(&thread, SIGNAL(finished()), &eventLoop, SLOT(quit()));

    thread.start();
    (void) eventLoop.exec();

    QSignalSpy spy(QAbstractEventDispatcher::instance(&thread), SIGNAL(awake()));
    thread.eventLoop->wakeUp();

    // give the thread time to wake up
    QTimer::singleShot(1000, &eventLoop, SLOT(quit()));
    (void) eventLoop.exec();

    QVERIFY(spy.count() > 0);

    thread.quit();
    (void) eventLoop.exec();
}

void tst_QEventLoop::quit()
{
    QEventLoop eventLoop;
    int returnCode;

    QTimer::singleShot(100, &eventLoop, SLOT(quit()));
    returnCode = eventLoop.exec();
    QCOMPARE(returnCode, 0);
}


void tst_QEventLoop::nestedLoops()
{
    QCoreApplication::postEvent(this, new StartStopEvent(QEvent::User));
    QCoreApplication::postEvent(this, new StartStopEvent(QEvent::User));
    QCoreApplication::postEvent(this, new StartStopEvent(QEvent::User));

    // without the fix, this will *wedge* and never return
    QTest::qWait(1000);
}

void tst_QEventLoop::customEvent(QEvent *e)
{
    if (e->type() == QEvent::User) {
        QEventLoop loop;
        QCoreApplication::postEvent(this, new StartStopEvent(int(QEvent::User) + 1, &loop));
        loop.exec();
    } else {
        static_cast<StartStopEvent *>(e)->el->exit();
    }
}

class SocketEventsTester: public QObject
{
    Q_OBJECT
public:
    SocketEventsTester()
    {
        socket = 0;
        server = 0;
        dataArrived = false;
        testResult = false;
    }
    ~SocketEventsTester()
    {
        delete socket;
        delete server;
    }
    bool init()
    {
        bool ret = false;
        server = new QTcpServer();
        socket = new QTcpSocket();
        connect(server, SIGNAL(newConnection()), this, SLOT(sendHello()));
        connect(socket, SIGNAL(readyRead()), this, SLOT(sendAck()), Qt::DirectConnection);
        if((ret = server->listen(QHostAddress::LocalHost, 0))) {
            socket->connectToHost(server->serverAddress(), server->serverPort());
            socket->waitForConnected();
        }
        return ret;
    }

    QTcpSocket *socket;
    QTcpServer *server;
    bool dataArrived;
    bool testResult;
public slots:
    void sendAck()
    {
        dataArrived = true;
    }
    void sendHello()
    {
        char data[10] ="HELLO";
        qint64 size = sizeof(data);

        QTcpSocket *serverSocket = server->nextPendingConnection();
        serverSocket->write(data, size);
        serverSocket->flush();
        QCoreApplication::processEvents(QEventLoop::ExcludeSocketNotifiers);
        testResult = dataArrived;
        serverSocket->close();
        QThread::currentThread()->exit(0);
    }
};

class SocketTestThread : public QThread
{
    Q_OBJECT
public:
    SocketTestThread():QThread(0),testResult(false){};
    void run()
    {
        SocketEventsTester *tester = new SocketEventsTester();
        if (tester->init())
            exec();
        testResult = tester->testResult;
        delete tester;
    }
     bool testResult;
};

void tst_QEventLoop::processEventsExcludeSocket()
{
    SocketTestThread thread;
    thread.start();
    QVERIFY(thread.wait());
    QVERIFY(!thread.testResult);
}

class TimerReceiver : public QObject
{
public:
    int gotTimerEvent;

    TimerReceiver()
        : QObject(), gotTimerEvent(-1)
    { }

    void timerEvent(QTimerEvent *event)
    {
        gotTimerEvent = event->timerId();
    }
};

void tst_QEventLoop::processEventsExcludeTimers()
{
    TimerReceiver timerReceiver;
    int timerId = timerReceiver.startTimer(0);

    QEventLoop eventLoop;

    // normal process events will send timers
    eventLoop.processEvents();
    QCOMPARE(timerReceiver.gotTimerEvent, timerId);
    timerReceiver.gotTimerEvent = -1;

    // normal process events will send timers
    eventLoop.processEvents(QEventLoop::X11ExcludeTimers);
#if !defined(Q_OS_UNIX) || defined(Q_OS_SYMBIAN)
    QEXPECT_FAIL("", "X11ExcludeTimers only works on UN*X", Continue);
#endif
    QCOMPARE(timerReceiver.gotTimerEvent, -1);
    timerReceiver.gotTimerEvent = -1;

    // resume timer processing
    eventLoop.processEvents();
    QCOMPARE(timerReceiver.gotTimerEvent, timerId);
    timerReceiver.gotTimerEvent = -1;
}

#ifdef Q_OS_SYMBIAN
class DummyActiveObject : public CActive
{
public:
    DummyActiveObject(int levels);
    ~DummyActiveObject();

    void Start();

protected:
    void DoCancel();
    void RunL();

public:
    bool succeeded;

private:
    RTimer m_rTimer;
    int remainingLevels;
};

class ActiveSchedulerLoop : public QObject
{
public:
    ActiveSchedulerLoop(int levels) : succeeded(false), timerId(-1), remainingLevels(levels) {}
    ~ActiveSchedulerLoop() {}

    void timerEvent(QTimerEvent *e);

public:
    bool succeeded;
    int timerId;
    int remainingLevels;
};

DummyActiveObject::DummyActiveObject(int levels)
    : CActive(CActive::EPriorityStandard),
      succeeded(false),
      remainingLevels(levels)
{
    m_rTimer.CreateLocal();
}

DummyActiveObject::~DummyActiveObject()
{
    Cancel();
    m_rTimer.Close();
}

void DummyActiveObject::DoCancel()
{
    m_rTimer.Cancel();
}

void DummyActiveObject::RunL()
{
    if (remainingLevels - 1 <= 0) {
        ActiveSchedulerLoop loop(remainingLevels - 1);
        loop.timerId = loop.startTimer(0);
        QCoreApplication::processEvents();

        succeeded = loop.succeeded;
    } else {
        succeeded = true;
    }
    CActiveScheduler::Stop();
}

void DummyActiveObject::Start()
{
    m_rTimer.After(iStatus, 100000); // 100 ms
    SetActive();
}

void ActiveSchedulerLoop::timerEvent(QTimerEvent *e)
{
    Q_UNUSED(e);
    DummyActiveObject *dummy = new(ELeave) DummyActiveObject(remainingLevels);
    CActiveScheduler::Add(dummy);

    dummy->Start();

    CActiveScheduler::Start();

    succeeded = dummy->succeeded;

    delete dummy;

    killTimer(timerId);
}

// We cannot trap panics when the test case fails, so run it in a different thread instead.
class ActiveSchedulerThread : public QThread
{
public:
    ActiveSchedulerThread(QEventLoop::ProcessEventsFlag flags);
    ~ActiveSchedulerThread();

protected:
    void run();

public:
    volatile bool succeeded;

private:
    QEventLoop::ProcessEventsFlag m_flags;
};

ActiveSchedulerThread::ActiveSchedulerThread(QEventLoop::ProcessEventsFlag flags)
    : succeeded(false),
      m_flags(flags)
{
}

ActiveSchedulerThread::~ActiveSchedulerThread()
{
}

void ActiveSchedulerThread::run()
{
    ActiveSchedulerLoop loop(2);
    loop.timerId = loop.startTimer(0);
    // It may panic in here if the active scheduler and the Qt loop don't go together.
    QCoreApplication::processEvents(m_flags);

    succeeded = loop.succeeded;
}
#endif // ifdef Q_OS_SYMBIAN

void tst_QEventLoop::symbianNestedActiveSchedulerLoop_data()
{
    QTest::addColumn<int>("processEventFlags");

    QTest::newRow("AllEvents") << (int)QEventLoop::AllEvents;
    QTest::newRow("WaitForMoreEvents") << (int)QEventLoop::WaitForMoreEvents;
}

/*
  Before you start fiddling with this test, you should have a good understanding of how
  Symbian active objects work. What the test does is to try to screw up the semaphore count
  in the active scheduler to cause stray signals, by running the Qt event loop and the
  active scheduler inside each other. Naturally, its attempts to do this should be futile!
*/
void tst_QEventLoop::symbianNestedActiveSchedulerLoop()
{
#ifndef Q_OS_SYMBIAN
    QSKIP("This is a Symbian only test.", SkipAll);
#else
    QFETCH(int, processEventFlags);

    ActiveSchedulerThread thread((QEventLoop::ProcessEventsFlag)processEventFlags);
    thread.start();
    thread.wait(2000);

    QVERIFY(thread.succeeded);
#endif
}

QTEST_MAIN(tst_QEventLoop)
#include "tst_qeventloop.moc"