Слот Qt не вызывается СИГНАЛОМ, испускаемым из std :: asyn c в среде QTest - PullRequest
1 голос
/ 26 марта 2020

Я исследовал топи c программирования asyn c с использованием Qt и пришел к выводу, что безопасно излучать сигналы из потоков любого типа (хотя в документах QT упоминается только QThread), так как больше или менее описанный здесь . Теперь я столкнулся с проблемой тестирования моего приложения. Чтобы максимально упростить: у меня есть операция asyn c, которая может уведомить MainWindow, испуская СИГНАЛ. Он отлично работает на производстве, но не работает в среде модульного тестирования с QTest. Полный пример (структура проекта плоская, без подкаталогов):

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

Результат моего теста

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

, что означает, что слот был активирован только один раз, по сигналу, исходящему из основного потока.

Выход из основного потока:

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

, что является ожидаемым поведением. Почему существует разница между средой QTest и обычным QApplication в main? Что я должен сделать, чтобы исправить поведение тестов?

Я должен использовать стандартные потоки C ++, потому что мой GUI - это просто фасад реальной системы, не связанной с Qt, которая имеет различные типы асин c оперов, callbacks et c.

Извините за количество кода, но с QT я не могу сжать его в несколько строк.

=== РЕДАКТИРОВАТЬ ===

Указание Qt::DirectConnection атрибута соединения «исправит» проблему в среде QTest. Но это то, что я не могу сделать в большинстве случаев, потому что GUI действия должны выполняться в главном потоке (например, scyn c oper посылает сигнал для запуска QPixmap refre sh);

1 Ответ

1 голос
/ 26 марта 2020

Код передачи сигнала правильный. Для соединения с помещенным в очередь сигнальным слотом требуется событие l oop для выполнения в потоке приемника для доставки сигнала:

События для этого объекта отправляются событие этого (получающего) потока l oop.

Событие l oop потока получателя не работает, потому что вы блокируете его в строке

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

That есть, событие l oop потока получателя блокируется во всем теле example_test() метода. Нет ни одной строки, которая запускает событие l oop внутри себя. Это не проблема Qt Test или QTEST_MAIN ().

Вот как это можно исправить:

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 () :

Запускает событие l oop, которое продолжается до получения данного сигнала. Опционально, событие l oop может возвращаться раньше по тайм-ауту (в миллисекундах).

Возвращает истину, если сигнал излучался хотя бы один раз в течение тайм-аута, в противном случае возвращает ложь.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...