wasm: add event loop auto test

Add basic tests for timers and event processing, for
different use cases such as on the main thread, on
a secondary thread, and with asyncify.

Pick-to: 6.4
Change-Id: Ie0f82b5de97f639867b1e65dbb0ab8b11db86f85
Reviewed-by: Lorn Potter <lorn.potter@gmail.com>
This commit is contained in:
Morten Sørvig 2022-07-06 23:01:59 +02:00
parent 2a17034ddc
commit 964765f686
7 changed files with 352 additions and 1 deletions

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
add_subdirectory(asyncify_exec)
add_subdirectory(eventloop_auto)
add_subdirectory(main_exec)
add_subdirectory(main_noexec)
add_subdirectory(thread_exec)

View File

@ -12,3 +12,4 @@ Contents
main_noexec Qt main() without QApplication::exec()
dialog_exec Shows how QDialog::exec() also does not return
thread_exec Shows how to use QThread::exec()
eventloop_auto Event loop autotest (manually run)

View File

@ -0,0 +1,21 @@
qt_internal_add_manual_test(eventloop_auto
SOURCES
main.cpp
../../qtwasmtestlib/qtwasmtestlib.cpp
PUBLIC_LIBRARIES
Qt::Core
)
include_directories(../../qtwasmtestlib/)
add_custom_command(
TARGET eventloop_auto POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/eventloop_auto.html
${CMAKE_CURRENT_BINARY_DIR}/eventloop_auto.html)
add_custom_command(
TARGET eventloop_auto POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/../../qtwasmtestlib/qtwasmtestlib.js
${CMAKE_CURRENT_BINARY_DIR}/qtwasmtestlib.js)

View File

@ -0,0 +1,10 @@
<!doctype html>
<script type="text/javascript" src="qtwasmtestlib.js"></script>
<script type="text/javascript" src="eventloop_auto.js"></script>
<script>
window.onload = () => {
runTestCase(document.getElementById("log"));
};
</script>
<p>Running event dispatcher auto test.</p>
<div id="log"></div>

View File

@ -0,0 +1,312 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/QCoreApplication>
#include <QtCore/QEvent>
#include <QtCore/QMutex>
#include <QtCore/QObject>
#include <QtCore/QThread>
#include <QtCore/QTimer>
#include <qtwasmtestlib.h>
const int timerTimeout = 10;
class WasmEventDispatcherTest: public QObject
{
Q_OBJECT
private slots:
void postEventMainThread();
void timerMainThread();
void timerMainThreadMultiple();
#if QT_CONFIG(thread)
void postEventSecondaryThread();
void postEventSecondaryThreads();
void postEventToSecondaryThread();
void timerSecondaryThread();
#endif
#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
void postEventAsyncify();
void timerAsyncify();
void postEventAsyncifyLoop();
#endif
private:
// Disabled test function: Asyncify wait on pthread_join is not supported,
// see https://github.com/emscripten-core/emscripten/issues/9910
#if QT_CONFIG(thread) && defined(QT_HAVE_EMSCRIPTEN_ASYNCIFY)
void threadAsyncifyWait();
#endif
};
class EventTarget : public QObject
{
Q_OBJECT
public:
static EventTarget *create(std::function<void()> callback)
{
return new EventTarget(callback);
}
static QEvent *createEvent()
{
return new QEvent(QEvent::User);
}
protected:
EventTarget(std::function<void()> callback)
: m_callback(callback) { }
bool event(QEvent *evt)
{
if (evt->type() == QEvent::User) {
m_callback();
deleteLater();
return true;
}
return QObject::event(evt);
}
private:
std::function<void()> m_callback;
};
class CompleteTestFunctionRefGuard {
public:
CompleteTestFunctionRefGuard(CompleteTestFunctionRefGuard const&) = delete;
CompleteTestFunctionRefGuard& operator=(CompleteTestFunctionRefGuard const&) = delete;
static CompleteTestFunctionRefGuard *create() {
return new CompleteTestFunctionRefGuard();
}
void ref() {
QMutexLocker lock(&mutex);
++m_counter;
}
void deref() {
const bool finalDeref = [this] {
QMutexLocker lock(&mutex);
return --m_counter == 0;
}();
if (finalDeref)
QtWasmTest::completeTestFunction();
}
private:
CompleteTestFunctionRefGuard() { };
QMutex mutex;
int m_counter = 0;
};
#if QT_CONFIG(thread)
class TestThread : public QThread
{
public:
static QThread *create(std::function<void()> started, std::function<void()> finished)
{
TestThread *thread = new TestThread();
connect(thread, &QThread::started, [started]() {
started();
});
connect(thread, &QThread::finished, [thread, finished]() {
finished();
thread->deleteLater();
});
thread->start();
return thread;
}
};
#endif
// Post event to the main thread and verify that it is processed.
void WasmEventDispatcherTest::postEventMainThread()
{
QCoreApplication::postEvent(EventTarget::create([](){
QtWasmTest::completeTestFunction();
}), EventTarget::createEvent());
}
// Create a timer on the main thread and verify that it fires
void WasmEventDispatcherTest::timerMainThread()
{
QTimer::singleShot(timerTimeout, [](){
QtWasmTest::completeTestFunction();
});
}
void WasmEventDispatcherTest::timerMainThreadMultiple()
{
CompleteTestFunctionRefGuard *completeGuard = CompleteTestFunctionRefGuard::create();
int timers = 10;
for (int i = 0; i < timers; ++i) {
completeGuard->ref();
QTimer::singleShot(timerTimeout * i, [completeGuard](){
completeGuard->deref();
});
}
}
#if QT_CONFIG(thread)
// Post event on a secondary thread and verify that it is processed.
void WasmEventDispatcherTest::postEventSecondaryThread()
{
auto started = [](){
QCoreApplication::postEvent(EventTarget::create([](){
QThread::currentThread()->quit();
}), EventTarget::createEvent());
};
auto finished = [](){
QtWasmTest::completeTestFunction();
};
TestThread::create(started, finished);
}
// Post event _to_ a secondary thread and verify that it is processed.
void WasmEventDispatcherTest::postEventToSecondaryThread()
{
auto started = [](){};
auto finished = [](){
QtWasmTest::completeTestFunction();
};
QThread *t = TestThread::create(started, finished);
EventTarget *target = EventTarget::create([](){
QThread::currentThread()->quit();
});
target->moveToThread(t);
QCoreApplication::postEvent(target, EventTarget::createEvent());
}
// Post events to many secondary threads and verify that they are processed.
void WasmEventDispatcherTest::postEventSecondaryThreads()
{
// This test completes afer all threads has finished
CompleteTestFunctionRefGuard *completeGuard = CompleteTestFunctionRefGuard::create();
completeGuard->ref(); // including this thread
auto started = [](){
QCoreApplication::postEvent(EventTarget::create([](){
QThread::currentThread()->quit();
}), EventTarget::createEvent());
};
auto finished = [completeGuard](){
completeGuard->deref();
};
// Start a nymber of threads in parallel, keeping in mind that the browser
// has some max number of concurrent web workers (maybe 20), and that starting
// a new web worker requires completing a GET network request for the worker's JS.
const int numThreads = 10;
for (int i = 0; i < numThreads; ++i) {
completeGuard->ref();
TestThread::create(started, finished);
}
completeGuard->deref();
}
// Create a timer a secondary thread and verify that it fires
void WasmEventDispatcherTest::timerSecondaryThread()
{
auto started = [](){
QTimer::singleShot(timerTimeout, [](){
QThread::currentThread()->quit();
});
};
auto finished = [](){
QtWasmTest::completeTestFunction();
};
TestThread::create(started, finished);
}
#endif
#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY
// Post an event to the main thread and asyncify wait for it
void WasmEventDispatcherTest::postEventAsyncify()
{
QEventLoop loop;
QCoreApplication::postEvent(EventTarget::create([&loop](){
loop.quit();
}), EventTarget::createEvent());
loop.exec();
QtWasmTest::completeTestFunction();
}
// Create a timer on the main thread and asyncify wait for it
void WasmEventDispatcherTest::timerAsyncify()
{
QEventLoop loop;
QTimer::singleShot(timerTimeout, [&loop](){
loop.quit();
});
loop.exec();
QtWasmTest::completeTestFunction();
}
// Asyncify wait in a loop
void WasmEventDispatcherTest::postEventAsyncifyLoop()
{
for (int i = 0; i < 10; ++i) {
QEventLoop loop;
QCoreApplication::postEvent(EventTarget::create([&loop]() {
loop.quit();
}), EventTarget::createEvent());
loop.exec();
}
QtWasmTest::completeTestFunction();
}
#if QT_CONFIG(thread)
// Asyncify wait for QThread::wait() / pthread_join()
void WasmEventDispatcherTest::threadAsyncifyWait()
{
const int threadCount = 15;
QVector<QThread *> threads;
threads.reserve(threadCount);
for (int i = 0; i < threadCount; ++i) {
QThread *thread = new QThread();
threads.push_back(thread);
thread->start();
}
for (int i = 0; i < threadCount; ++i) {
QThread *thread = threads[i];
thread->wait();
delete thread;
}
QtWasmTest::completeTestFunction();
}
#endif
#endif // QT_HAVE_EMSCRIPTEN_ASYNCIFY
int main(int argc, char **argv)
{
auto testObject = std::make_shared<WasmEventDispatcherTest>();
QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
return 0;
}
#include "main.moc"

View File

@ -45,7 +45,6 @@ void verify(bool condition, std::string_view conditionString, std::string_view f
// thread-safe and call be called from any thread.
void completeTestFunction(TestResult result, std::string message)
{
// Report test result to JavaScript test runner, on the main thread
runOnMainThread([resultString = result == TestResult::Pass ? "PASS" : "FAIL", message](){
EM_ASM({
@ -54,6 +53,12 @@ void completeTestFunction(TestResult result, std::string message)
});
}
// Completes the currently running test function with a Pass result.
void completeTestFunction()
{
completeTestFunction(TestResult::Pass, std::string());
}
//
// Private API for the Javascript test runnner
//

View File

@ -20,6 +20,7 @@ std::string formatMessage(std::string_view file,
std::string_view message);
void completeTestFunction(TestResult result, std::string message);
void completeTestFunction();
void initTestCase(QObject *testObject, std::function<void ()> cleanup);
template <typename App>
void initTestCase(int argc,