Search code examples
multithreadingqteventsqt6

How to execute a QtConcurrent::run call on QtTest


I'm building the tests for my application. Currently, the code delegates all database calls to a separate worker thread. When trying to test the functions, I see no call for the thread executes.

I saw that I can call QCoreApplication::exec() for executing the Event Loop calls, but this executes some and then hangs forever.

Here, anda_skoa suggests "using a test-local QEventLoop instance that quits on the result signal", but I have no idea how to do this.

Below is a sample of current code:

tests.cpp

#include <QtTest>
#include <QCoreApplication>
#include <QDebug>

#include "dbmanager.h"
#include "exception.h"

class Test : public QObject
{
    Q_OBJECT

    const QString dbPath = "test_DB.db";

private slots:
    void initTestCase()
    {
        DbManager* db = new DbManager();
        QFuture<void> connection = db->connect(dbPath);

        connection
            .then([](){
                qInfo() << "Database Connected!";
            }).onFailed([](Exception e){
                qDebug() << "Error initializing database" << e.what();
            }); // None of this ever runs.
    }

    void isTrueTrue()
    {
        QCOMPARE(true, true);
    }
};

QTEST_MAIN(Test)

#include "tests.moc"

dbmanager.h

#include <QSqlQuery>
#include <QFuture>
#include <QtConcurrent/QtConcurrent>

#include "dbthread.h"
#include "exception.h"

class DbManager : public QObject
{
    Q_OBJECT
public:
    QFuture<void> connect(const QString &path)
    {
        return QtConcurrent::run(DbThread::instance()->pool(), [path]() {
            QSqlDatabase m_db = QSqlDatabase::addDatabase("QSQLITE", "main");
            m_db.setDatabaseName(path);

            if (!m_db.open())
            {
                throw Exception("Error connecting to the database");
            }
        });
    }
};

dbthread.h

#ifndef DBTHREAD_H
#define DBTHREAD_H

#include <QThreadPool>

class DbThread
{
    DbThread() {
        m_pool = new QThreadPool();
        m_pool->setMaxThreadCount(1);
        m_pool->setExpiryTimeout(-1);
        m_pool->setObjectName("Database ThreadPool");
    };

    virtual ~DbThread() {
        m_pool->deleteLater();
    };

public:
    DbThread( const DbThread& ) = delete;               // singletons should not be copy-constructed
    DbThread& operator=( const DbThread& ) = delete;    // singletons should not be assignable

    static DbThread* instance() {

        if ( !m_instance )
            m_instance = new DbThread();

        return m_instance;
    }

    QThreadPool* pool() const { return m_pool; };

private:
    inline static DbThread* m_instance = nullptr; // https://stackoverflow.com/a/61519399/12172630
    QThreadPool* m_pool;

};

#endif // DBTHREAD_H

exception.h

#ifndef EXCEPTION_H
#define EXCEPTION_H

#include <QException>

class Exception : public QException
{

public:
    Exception(QString text)
    {
        m_text = text.toLocal8Bit();
    }

    const char* what() const noexcept
    {
        return m_text.data(); // https://wiki.qt.io/Technical_FAQ#How_can_I_convert_a_QString_to_char.2A_and_vice_versa.3F
    }

    void raise() const
    {
        Exception e = *this;
        throw e;
    }

    Exception *clone() const
    {
        return new Exception(*this);
    }


private:
    QByteArray m_text;

};

#endif // EXCEPTION_H

Many of the subsequent tests in this test are also methods wrapped on a Concurrent run that return a QFuture. However, I can see that none of the lambdas are executed as I can see that no Debug log is produced, and all the tests fail, as the values are not changed because their logic was not executed.

If I add a QCoreApplication::exec() at the end of initTestCase(), the lambdas are executed, but it hangs there forever, and nothing more is executed.

Everything works fine on my application, so I'm sure something is missing for testing only.

What to do?


Solution

  • I figured it out! If you have the future variable you can call [varaible with the future].waitForFinished(); and it will have it executed. With this I was able to get the database up on the initTestCase()!

    I'm still having issues when I don't have the future variable, e.g. when the concurrent is being called inside a method that do not return the future, but I will open a new stack for it. Thanks!