tags:

views:

87

answers:

1

I'm having a problem with QEventLoop. I want to create a "TimeBoundExerciser" for my unit test so that my SUT, which blocks on a QEventLoop, won't block the rest of the test cases. Specifically, my test case is to make sure the SUT terminates after a timeout.

The TimeBoundExerciser basically spawns a thread, executes the SUT on that thread, waits for the thread to terminate, and if it doesn't terminate after a specific amount of time, invokes the quit() method on the thread through QMetaObject::invokeMethod() and a QueuedConnection. I would expect that executing quit() will cause my nested QEventLoop to exit, terminating my thread. However, what I've found is that the quit() method is never invoked, and the thread never terminates. The code for my TimeBoundExerciser is below:

class IExerciseTheSystem
{
    void operator()() = 0;
};

class TimeBoundExerciser : private QThread
{
Q_OBJECT
public:
    enum CompletionType
    {
        TERMINATED,
        FORCE_QUIT,
        QUIT
    };
    TimeBoundExerciser(const IExerciseTheSystem& exerciser);
    CompletionType exercise(unsigned long timeoutMillis);   
protected:
    void run();

protected slots:
    void exerciseTheSystem();
private:
    const IExerciseTheSystem& exerciser;
};

TimeBoundExerciser::TimeBoundExerciser(const IExerciseTheSystem& exerciser) : exerciser(exerciser)
{

}

TimeBoundExerciser::CompletionType TimeBoundExerciser::exercise(unsigned long timeoutMillis)
{
    start();
    while (!isRunning()) 
    {
        msleep(10);
    }

    moveToThread(this);

    wait(timeoutMillis);
    if (!isFinished()) 
    {
        bool quitResult;
        QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection, Q_RETURN_ARG(bool, quitResult));
        wait();
        return FORCE_QUIT;
    }

    return QUIT;
}

void TimeBoundExerciser::run()
{
    setTerminationEnabled(true);
    QMetaObject::invokeMethod(this, "exerciseTheSystem", Qt::QueuedConnection);
    exec();
}

void TimeBoundExerciser::exerciseTheSystem()
{
    cout << "Starting exerciser" << endl;
    exerciser();
    cout << "Exerciser ended" << endl;
}

The exercise() method is executed on the main thread to kick off the whole process.

A: 

If the test runs too long, it's probably in some sort of loop processing data.

Naturally, your quit request won't be delivered because the test thread is busy running the test. Messages don't interrupt threads, they are processed when the thread finishes processing the previous message and resumes the event loop.

Ben Voigt
Except, since I control the code, I know that the test thread is busy running a local QEventLoop, not on some long-running processing. The SUT is basically waiting for its QEventLoop::exec() call to exit.
gregsymons
Thought you said it was a unit test. Entering an infinite loop is an entirely possible way of failing testing, and something that your unit test suite should detect.
Ben Voigt
It is. Basically, the unit under test creates a local QEventLoop and executes it until it receives a specific signal, at which point the local QEventLoop will exit and return to the caller. Essentially, I'm creating a modal object similar to a modal dialog box, except it will have a timeout. The unit test I'm writing that uses the TimeBoundedExerciser is intended to test that the object exits its event loop properly when the timeout passes.
gregsymons
Maybe the other thread does get the "quit" message, but the "quit" message handler doesn't exit the thread? I don't see the "quit" handler implementation in any of your posted code.
Ben Voigt
But, it really doesn't matter. Your test framework needs to handle the possibility that the code under test isn't in the event loop but stuck elsewhere.
Ben Voigt
That's what the TimeBoundedExerciser is supposed to do. It originally would terminate the thread using terminate(), but that's dangerous (and didn't work anyway). The only real safe way to do it would be to execute the test in a separate process and terminate the process if it doesn't complete in time. But that's more work than I have time for right now.
gregsymons
Unfortunately, only a separate process provides the isolation necessary for recovery. But you don't necessarily need to isolate each test independently -- most testing frameworks provide a test runner which spawns the tests themselves and will kill the test process if it goes too long without generating a test result. The advantage is the simplicity from running all tests in a single process, the downside is that a timeout is a fatal failure that stops the rest of the testing series.
Ben Voigt
The framework I'm using (Google Test) actually allows spawning an individual test case as a "Death Test" but in my case, the overhead of making sure QT is up and running in the spawned process is more trouble than it's worth, especially since I figured out how to do what I needed to do without spawning a thread.
gregsymons
That's interesting. I'm using Google Test for unit testing as well and I've been wondering how I was going to tackle Qt classes with slots and signals.
Arnold Spence