pptaszni
pptaszni

Reputation: 8312

Qt SLOT not triggered by the SIGNAL emitted from std::async in QTest environment

I was investigating the topic of async programming with Qt and I reached the conclusion that it is safe to emit signals from whatever kind of threads (although QT docs only mention QThread), as more or less described here. Now I faced the problem testing my application. To simplify as much as possible: I have the async operation, which might notify MainWindow by emitting the SIGNAL. It works fine in the production, but it doesn't work in unit-test environment with QTest. Complete example (project structure flat, no subdirs):

CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)
project(QtFailure)

enable_testing()
set(CMAKE_CXX_STANDARD 14)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
if(CMAKE_VERSION VERSION_LESS "3.7.0")
    set(CMAKE_INCLUDE_CURRENT_DIR ON)
endif()
find_package(Qt5 COMPONENTS Core Widgets Test REQUIRED)

add_library(QtFailure source.cpp header.hpp)
target_include_directories(QtFailure PUBLIC .)
target_link_libraries(QtFailure
  pthread
  Qt5::Core
  Qt5::Widgets
)

add_executable(main main.cpp)
target_link_libraries(main QtFailure)

add_executable(QtFailureTest test.cpp)
target_link_libraries(QtFailureTest
  QtFailure
  Qt5::Test
)

header.hpp

#pragma once

#include <QMainWindow>
#include <QWidget>

#include <future>

class MainWindow : public QMainWindow
{
  Q_OBJECT
public:
  explicit MainWindow(QWidget* parent = nullptr);
  ~MainWindow();
  void start();
  int counter_value();

signals:
  void sendSignal();

private slots:
  bool triggerSlot();

private:
  bool stop_;
  std::future<void> async_oper_;
};

source.cpp


#include "header.hpp"

#include <QMainWindow>
#include <QWidget>
#include <QObject>
#include <QDebug>

#include <future>
#include <chrono>
#include <thread>

static int counter = 0;

MainWindow::MainWindow(QWidget* parent):
  QMainWindow(parent),
  stop_(false),
  async_oper_()
{
  QObject::connect(this, SIGNAL(sendSignal()), this, SLOT(triggerSlot()));
}

MainWindow::~MainWindow()
{
  stop_ = true;
}

int MainWindow::counter_value()
{
  return counter;
}

void MainWindow::start()
{
  if (async_oper_.valid()) return;
  emit sendSignal();  // this one works
  async_oper_ = std::async(std::launch::async, [this]()
  {
    while (!stop_)
    {
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
      emit sendSignal();  // this one doesn't work in tests
    }
  });
}

bool MainWindow::triggerSlot()
{
  qDebug() << "triggerSlot: " << counter;
  counter++;
}

test.cpp

#include "header.hpp"

#include <QSignalSpy>
#include <QDebug>
#include <QtTest/QtTest>

#include <memory>
#include <chrono>
#include <thread>

class MyFixture: public QObject
{
  Q_OBJECT
private:
  std::unique_ptr<MainWindow> sut_;

private slots:
  void init()
  {
    qDebug("MyFixture init");
    sut_.reset(new MainWindow);
  }
  void cleanup()
  {
    qDebug("MyFixture cleanup");
    sut_.reset();
  }
  void example_test()
  {
    QSignalSpy spy(sut_.get(), SIGNAL(sendSignal()));
    sut_->start();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    qDebug() << "num signals: " << spy.count();
    qDebug() << "counter value: " << sut_->counter_value();
  }
};

QTEST_MAIN(MyFixture)
#include "test.moc"

main.cpp


#include <QApplication>

#include "header.hpp"

int main(int argc, char** argv)
{
  QApplication a(argc, argv);
  MainWindow w;
  w.start();
  w.show();
  return a.exec();
}

The output from my test is

PASS   : MyFixture::initTestCase()
QDEBUG : MyFixture::example_test() MyFixture init
QDEBUG : MyFixture::example_test() triggerSlot:  0
QDEBUG : MyFixture::example_test() num signals:  10
QDEBUG : MyFixture::example_test() counter value:  1
QDEBUG : MyFixture::example_test() MyFixture cleanup
PASS   : MyFixture::example_test()
PASS   : MyFixture::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 1003ms

which means that the slot was triggered only once, by the signal emitted from the main thread.

The output from main is:

...
triggerSlot:  25
triggerSlot:  26
triggerSlot:  27
etc ...

which is the expected behavior. Why is there a difference between QTest environment and normal QApplication in main? What should I do to correct tests behavior?

I must use standard C++ threads, because my GUI is just a facade to real non-Qt related system, which has different kinds of async opers, callbacks etc.

Sorry for the amount of code, but with QT I cannot really squeeze it in a few lines.

=== EDIT ===

Specifying Qt::DirectConnection connection attribute will "fix" the issue in the QTest environment. But this is something I cannot do in most cases, because GUI actions must take place in the main thread (e.g. scync oper emits signal to trigger QPixmap refresh);

Upvotes: 1

Views: 961

Answers (1)

Vladimir Bershov
Vladimir Bershov

Reputation: 2832

The signal emit code is correct. Queued Signal-Slot connection requires event loop to be running in the receiver thread for signal delivery:

Events to that object are dispatched by that (receiver) thread's event loop.

Event loop of the receiver thread aren't working because you block it in the line

// just suspends current thread without running any event loop
std::this_thread::sleep_for(std::chrono::seconds(1));

That is, event loop of the receiver thread are blocking throughout the whole example_test() method body. There is no one line which runs an event loop within itself. That is not Qt Test or QTEST_MAIN() issue.

Here is how you can fix that:

void example_test()
  {
    QSignalSpy spy(sut_.get(), SIGNAL(sendSignal()));
    sut_->start(); 

    QElapsedTimer timer;
    timer.start();

    while(timer.elapsed() < 1000)
    {
        spy.wait(10); // event loop runs here
    }

    qDebug() << "num signals: " << spy.count();
    qDebug() << "counter value: " << sut_->counter_value();
  }

QSignalSpy::wait():

Starts an event loop that runs until the given signal is received. Optionally the event loop can return earlier on a timeout (in milliseconds).

Returns true if the signal was emitted at least once in timeout milliseconds, otherwise returns false.

Upvotes: 1

Related Questions