简体   繁体   English

Qt SLOT 不是由 QTest 环境中的 std::async 发出的信号触发的

[英]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 .我正在研究使用 Qt 进行异步编程的主题,我得出的结论是,从任何类型的线程发出信号都是安全的(尽管 QT 文档只提到了QThread ),正如此处或多或少所述。 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.尽可能简化:我有异步操作,它可能会通过发出信号来通知MainWindow It works fine in the production, but it doesn't work in unit-test environment with QTest .它在生产中运行良好,但在带有QTest单元测试环境中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: main 的输出是:

...
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?为什么QTest环境和main中的普通QApplication有区别? 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.我必须使用标准的 C++ 线程,因为我的 GUI 只是真正的非 Qt 相关系统的一个外观,它具有不同类型的异步操作、回调等。

Sorry for the amount of code, but with QT I cannot really squeeze it in a few lines.抱歉代码量太大,但使用 QT 我无法真正将其压缩为几行。

=== EDIT === === 编辑 ===

Specifying Qt::DirectConnection connection attribute will "fix" the issue in the QTest environment.指定Qt::DirectConnection连接属性将“修复”QTest 环境中的问题。 But this is something I cannot do in most cases, because GUI actions must take place in the main thread (eg scync oper emits signal to trigger QPixmap refresh);但这在大多数情况下我无法做到,因为 GUI 操作必须在主线程中进行(例如 scync oper 发出信号以触发 QPixmap 刷新);

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.也就是说,接收器线程的事件循环在整个example_test()方法体中都是阻塞的。 There is no one line which runs an event loop within itself.没有一行在其内部运行事件循环。 That is not Qt Test or QTEST_MAIN() issue.这不是 Qt 测试或 QTEST_MAIN() 问题。

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() : 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.如果信号在超时毫秒内至少发出一次,则返回 true,否则返回 false。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM