Reputation: 8312
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
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();
}
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