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.