2

I want to test a custom QMessageBox that I created. In particular, my API will execute automatically the exec() loop of the QMessageBox, and I want to check the result when the user clicks a button or closes the widget.

The problem is that I am blocked in the exec() loop.

The only solution I found is creating a QTimer that will do what I want just before the loop is executed. References to this solution are this and this.

The tests are working correctly, but really there isn't a more native way in Qt to test the exec() loop of the QDialogs? I mean, there are ways to activate/simulate signals in QTest, there is nothing that can simulate the exec() loop?

n3mo
  • 663
  • 8
  • 23

1 Answers1

5

Why would you want to "simulate" the event loop? You want to run the real event loop! You just want to inject your code into it, and that's what timers are for (or events in general).

Qt Test has all sorts of convenience wrappers around the event loop, e.g. keyClick delivers the event and drains the event queue, etc., but you don't need to simulate anything. If you don't find something that you need, you can make your own.

The API you're using is of course broken: it pretends that an asynchronous action is synchronous. It should invert the control into a continuation passing style, i.e.:

// horrible
int Api::askForValue();
// better
Q_SIGNAL void askForValueResult(int);
void Api::askForValue(); // emits the signal
template <typename F>
void Api::askForValue(F &&fun) { // convenience wrapper
  auto *conn = new QMetaObject::Connection;
  *conn = connect(this, &Class::askForValueResult, 
    [fun = std::forward<F>(fun), 
     c = std::unique_ptr<QMetaObject::Connection>(conn)]
    (int val){
      fun(val);
      QObject::disconnect(*c);
    });
  askForValue();
}

The synchronous APIs like make a lot of application code have to face reentrancy, and this is not a hypothetical problem. Questions about it are not uncommon. Few people realize how bad of a problem it is.

But assuming that you are forced to stick with the horrible API: what you really want is to react to the message window being shown. You can do this by adding a global, application-wide event filter on the QEvent::WindowActivate. Simply install your event filter object on the application instance. This could be wrapped in some helper code:

/// Invokes setup, waits for a new window of a type T to be activated,
/// then invokes fun on it. Returns true when fun was invoked before the timeout elapsed.
template <typename T, typename F1, typename F2>
bool waitForWindowAnd(F1 &&setup, F2 &&fun, int timeout = -1) {
  QEventLoop loop;
  QWidgetList const exclude = QApplication::topLevelWidgets();
  struct Filter : QObject {
    QEventLoop &loop;
    F2 &fun;
    bool eventFilter(QObject *obj, QEvent *ev) override {
      if (ev.type() == QEvent::WindowActivate)
        if (auto *w = qobject_cast<T*>(obj))
          if (w->isWindow() && !exclude.contains(w)) {
            fun(w);
            loop.exit(0);
          }
      return false;
    }
    Filter(QEventLoop &loop, F2 &fun) : loop(loop), fun(fun) {}
  } filter{loop, fun};
  qApp->installEventFilter(&filter);

  QTimer::singleShot(0, std::forward<F1>(setup));
  if (timeout > -1)
    QTimer::singleShot(timeout, &loop, [&]{ loop.exit(1); });
  return loop.exec() == 0;
}

Further wrappers could be made that factor out common requirements:

/// Invokes setup, waits for new widget of type T to be shown,
/// then clicks the standard OK button on it. Returns true if the button
/// was clicked before the timeout.
template <typename T, typename F>
bool waitForStandardOK(F &&setup, int timeout = -1) {
  return waitForWindowAnd<T>(setup,
    [](QWidget *w){
        if (auto *box = w->findChild<QDialogButtonBox*>())
          if (auto *ok = box->standardButton(QDialogButtonBox::Ok))
            ok->click();
    }, timeout);
}

Then, supposed you wanted to test the blocking API QDialogInput::getInt:

waitForStandardOK<QDialog>([]{ QInputDialog::getInt(nullptr, "Hello", "Gimme int!"); }, 500);

You could also make a wrapper that builds the bound call without needing to use a lambda:

/// Binds and invokes the setup call, waits for new widget of type T to be shown,
/// then clicks the standard OK button on it. Returns true if the button
/// was clicked before the timeout.
template<typename T, class ...Args>
void waitForStandardOKCall(int timeout, Args&&... setup) {
  auto bound = std::bind(&invoke<Args&...>, std::forward<Args>(args)...);
  return waitForStandardOK<T>(bound, timeout);
}

And thus:

waitForStandardOKCall<QDialog>(500, &QInputDialog::getInt, nullptr, "Hello", "Gimme int!");

For more about std::bind and perfect forwarding, see this question.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Hi Kuba thanks for the answer, Why you say "it pretends that an asynchronous action is synchronous"? I just reuse the event loop of `QMessageBox::exec()` that the doc say "Shows the message box as a modal dialog, blocking until the user closes it.". My API (maybe the API is the wrong terminology?) is just a shortcut to have the creation of `QMessageBox` and the execution of `exec()` in one call. basically something like `int execMessageBox(params){return QMessageBox(params).exec()}`. what is wrong with that? – n3mo Jun 22 '18 at 15:48
  • At should have never provided those `exec` methods. They are horrible hacks with roots in the times when writing async code was considered a black art and the macabre of C APIs was overshadowing everything. The event loop concept is designed to always be in control, and re-entering the event loop by calling `processEvents` or `exec` is absolutely unnecessary. When the dialog has some data to share, run a continuation or emit a signal. That’s howodern async APIs should be made. It’s not Qt specific at all — all modern .Net APIs are that way too and use events. – Kuba hasn't forgotten Monica Jun 25 '18 at 08:00
  • All synchronous wrappers around async APIs are essentially broken by design. They are only usable from the most basic linear-flow applications of the old “query and step” design, where you are issued a series of prompts in sequence with no way to do anything else. E.g. `QInputDialog::get` methods are of “great use” when you port a command line application that essentially does a series of `printf`/`sscanf` queries. Awesome for trivial classroom code, useless for anything else. No modern UI should work that way, never mind no code should be written with those APIs. – Kuba hasn't forgotten Monica Jun 25 '18 at 08:04
  • Qt does everyone a big disservice by not deprecating those methods and not explaining how bad of a user experience they provide, and how they are a subtle source of bugs that are very hard to test for. A recursive `exec` forces all of your code to be reentrant. Good luck with that. – Kuba hasn't forgotten Monica Jun 25 '18 at 08:05
  • Hi Kuba, I have some case when I need to block my code waiting for some user decision. At the moment it does not come to my mind another solution. You are talking about "modern" api. Can you give me some reference, or some "name" or "keyword" to look for? – n3mo Jun 26 '18 at 07:47
  • Such code should be driven by a state machine or a coroutine. You're not stopping the world for the user decision. You're just pausing some sequence of actions. Your program, including its UI thread, must still be responsive to other events. So in truth you're not stopping, and writing code that look as if things were exclusively stopped is unhelpful and a source of bugs.It also makes testing a pain - there's a reason why it's a pain: pseudo-synchronous code is very hard to use for anything, including testing. – Kuba hasn't forgotten Monica Jun 27 '18 at 19:22