mirror of
https://github.com/crystalidea/qt6windows7.git
synced 2025-07-02 07:15:27 +08:00
qt 6.5.1 original
This commit is contained in:
75
tests/manual/wasm/qtwasmtestlib/README.md
Normal file
75
tests/manual/wasm/qtwasmtestlib/README.md
Normal file
@ -0,0 +1,75 @@
|
||||
QtWasmTestLib - async auto tests for WebAssembly
|
||||
================================================
|
||||
|
||||
QtWasmTestLib supports auto-test cases in the web browser. Like QTestLib, each
|
||||
test case is defined by a QObject subclass with one or more test functions. The
|
||||
test functions may be asynchronous, where they return early and then complete
|
||||
at some later point.
|
||||
|
||||
The test lib is implemented as a C++ and JavaScript library, where the test is written
|
||||
using C++ and a hosting html page calls JavaScript API to run the test.
|
||||
|
||||
Implementing a basic test case
|
||||
------------------------------
|
||||
|
||||
In the test cpp file, define the test functions as private slots. All test
|
||||
functions must call completeTestFunction() exactly once, or will time out
|
||||
otherwise. Subsequent calls to completeTestFunction will be disregarded.
|
||||
It is advised to use QWASMSUCCESS/QWASMFAIL for reporting the test execution
|
||||
status and QWASMCOMPARE/QWASMVERIFY to assert on test conditions. The call can
|
||||
be made after the test function itself has returned.
|
||||
|
||||
class TestTest: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void timerTest() {
|
||||
QTimer::singleShot(timeout, [](){
|
||||
completeTestFunction();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Then define a main() function which calls initTestCase(). The main()
|
||||
function is async too, as per Emscripten default. Build the .cpp file
|
||||
as a normal Qt for WebAssembly app.
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
auto testObject = std::make_shared<TestTest>();
|
||||
initTestCase<QCoreApplication>(argc, argv, testObject);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Finally provide an html file which hosts the test runner and calls runTestCase()
|
||||
|
||||
<!doctype html>
|
||||
<script type="text/javascript" src="qtwasmtestlib.js"></script>
|
||||
<script type="text/javascript" src="test_case.js"></script>
|
||||
<script>
|
||||
window.onload = async () => {
|
||||
runTestCase(document.getElementById("log"));
|
||||
};
|
||||
</script>
|
||||
<p>Running Foo auto test.</p>
|
||||
<div id="log"></div>
|
||||
|
||||
Implementing a GUI test case
|
||||
----------------------------
|
||||
|
||||
This is similar to implementing a basic test case, with the difference that the hosting
|
||||
html file provides container elements which becomes QScreens for the test code.
|
||||
|
||||
<!doctype html>
|
||||
<script type="text/javascript" src="qtwasmtestlib.js"></script>
|
||||
<script type="text/javascript" src="test_case.js"></script>
|
||||
<script>
|
||||
window.onload = async () => {
|
||||
let log = document.getElementById("log")
|
||||
let containers = [document.getElementById("container")];
|
||||
runTestCase(log, containers);
|
||||
};
|
||||
</script>
|
||||
<p>Running Foo auto test.</p>
|
||||
<div id="container"></div>
|
||||
<div id="log"></div>
|
175
tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
Normal file
175
tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.cpp
Normal file
@ -0,0 +1,175 @@
|
||||
// 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 "qtwasmtestlib.h"
|
||||
|
||||
#include <QtCore/qmetaobject.h>
|
||||
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/threading.h>
|
||||
|
||||
namespace QtWasmTest {
|
||||
namespace {
|
||||
QObject *g_testObject = nullptr;
|
||||
std::string g_currentTestName;
|
||||
std::function<void ()> g_cleanup;
|
||||
}
|
||||
|
||||
void runOnMainThread(std::function<void(void)> fn);
|
||||
static bool isValidSlot(const QMetaMethod &sl);
|
||||
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
// Initializes the test case with a test object and cleanup function. The
|
||||
// cleanup function is called when all test functions have completed.
|
||||
void initTestCase(QObject *testObject, std::function<void ()> cleanup)
|
||||
{
|
||||
g_testObject = testObject;
|
||||
g_cleanup = cleanup;
|
||||
}
|
||||
|
||||
void verify(bool condition, std::string_view conditionString, std::string_view file, int line)
|
||||
{
|
||||
if (!condition) {
|
||||
completeTestFunction(
|
||||
TestResult::Fail,
|
||||
formatMessage(file, line, "Condition failed: " + std::string(conditionString)));
|
||||
}
|
||||
}
|
||||
|
||||
// Completes the currently running test function with a result. This function is
|
||||
// thread-safe and call be called from any thread.
|
||||
void completeTestFunction(TestResult result, std::string message)
|
||||
{
|
||||
auto resultString = [](TestResult result) {
|
||||
switch (result) {
|
||||
case TestResult::Pass:
|
||||
return "PASS";
|
||||
break;
|
||||
case TestResult::Fail:
|
||||
return "FAIL";
|
||||
break;
|
||||
case TestResult::Skip:
|
||||
return "SKIP";
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Report test result to JavaScript test runner, on the main thread
|
||||
runOnMainThread([resultString = resultString(result), message](){
|
||||
EM_ASM({
|
||||
completeTestFunction(UTF8ToString($0), UTF8ToString($1), UTF8ToString($2));
|
||||
}, g_currentTestName.c_str(), resultString, message.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
// Completes the currently running test function with a Pass result.
|
||||
void completeTestFunction()
|
||||
{
|
||||
completeTestFunction(TestResult::Pass, std::string());
|
||||
}
|
||||
|
||||
//
|
||||
// Private API for the Javascript test runnner
|
||||
//
|
||||
|
||||
std::string formatMessage(std::string_view file, int line, std::string_view message)
|
||||
{
|
||||
return "[" + std::string(file) + ":" + QString::number(line).toStdString() + "] " + std::string(message);
|
||||
}
|
||||
|
||||
void cleanupTestCase()
|
||||
{
|
||||
g_testObject = nullptr;
|
||||
g_cleanup();
|
||||
}
|
||||
|
||||
std::string getTestFunctions()
|
||||
{
|
||||
std::string testFunctions;
|
||||
|
||||
// Duplicate qPrintTestSlots (private QTestLib function) logic.
|
||||
for (int i = 0; i < g_testObject->metaObject()->methodCount(); ++i) {
|
||||
QMetaMethod sl = g_testObject->metaObject()->method(i);
|
||||
if (!isValidSlot(sl))
|
||||
continue;
|
||||
QByteArray signature = sl.methodSignature();
|
||||
Q_ASSERT(signature.endsWith("()"));
|
||||
signature.chop(2);
|
||||
if (!testFunctions.empty())
|
||||
testFunctions += " ";
|
||||
testFunctions += std::string(signature.constData());
|
||||
}
|
||||
|
||||
return testFunctions;
|
||||
}
|
||||
|
||||
void runTestFunction(std::string name)
|
||||
{
|
||||
g_currentTestName = name;
|
||||
QMetaObject::invokeMethod(g_testObject, name.c_str());
|
||||
}
|
||||
|
||||
void failTest(std::string message)
|
||||
{
|
||||
completeTestFunction(QtWasmTest::Fail, std::move(message));
|
||||
}
|
||||
|
||||
void passTest()
|
||||
{
|
||||
completeTestFunction(QtWasmTest::Pass, "");
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(qtwebtestrunner) {
|
||||
emscripten::function("cleanupTestCase", &cleanupTestCase);
|
||||
emscripten::function("getTestFunctions", &getTestFunctions);
|
||||
emscripten::function("runTestFunction", &runTestFunction);
|
||||
emscripten::function("qtWasmFail", &failTest);
|
||||
emscripten::function("qtWasmPass", &passTest);
|
||||
}
|
||||
|
||||
//
|
||||
// Test lib implementation
|
||||
//
|
||||
|
||||
static bool isValidSlot(const QMetaMethod &sl)
|
||||
{
|
||||
if (sl.access() != QMetaMethod::Private || sl.parameterCount() != 0
|
||||
|| sl.returnType() != QMetaType::Void || sl.methodType() != QMetaMethod::Slot)
|
||||
return false;
|
||||
const QByteArray name = sl.name();
|
||||
return !(name.isEmpty() || name.endsWith("_data")
|
||||
|| name == "initTestCase" || name == "cleanupTestCase"
|
||||
|| name == "init" || name == "cleanup");
|
||||
}
|
||||
|
||||
void trampoline(void *context)
|
||||
{
|
||||
Q_ASSERT(emscripten_is_main_runtime_thread());
|
||||
|
||||
emscripten_async_call([](void *context) {
|
||||
std::function<void(void)> *fn = reinterpret_cast<std::function<void(void)> *>(context);
|
||||
(*fn)();
|
||||
delete fn;
|
||||
}, context, 0);
|
||||
}
|
||||
|
||||
// Runs the given function on the main thread, asynchronously
|
||||
void runOnMainThread(std::function<void(void)> fn)
|
||||
{
|
||||
void *context = new std::function<void(void)>(fn);
|
||||
if (emscripten_is_main_runtime_thread()) {
|
||||
trampoline(context);
|
||||
} else {
|
||||
#if QT_CONFIG(thread)
|
||||
emscripten_async_run_in_main_runtime_thread_(EM_FUNC_SIG_VI, reinterpret_cast<void *>(trampoline), context);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QtWasmTest
|
||||
|
73
tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.h
Normal file
73
tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.h
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2022 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#ifndef QT_WASM_TESTRUNNER_H
|
||||
#define QT_WASM_TESTRUNNER_H
|
||||
|
||||
#include <QtCore/qobject.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace QtWasmTest {
|
||||
|
||||
enum TestResult {
|
||||
Pass,
|
||||
Fail,
|
||||
Skip,
|
||||
};
|
||||
|
||||
std::string formatMessage(std::string_view file,
|
||||
int line,
|
||||
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,
|
||||
char **argv,
|
||||
std::shared_ptr<QObject> testObject)
|
||||
{
|
||||
auto app = std::make_shared<App>(argc, argv);
|
||||
auto cleanup = [testObject, app]() mutable {
|
||||
// C++ lambda capture destruction order is unspecified;
|
||||
// delete test before app by calling reset().
|
||||
testObject.reset();
|
||||
app.reset();
|
||||
};
|
||||
initTestCase(testObject.get(), cleanup);
|
||||
}
|
||||
void verify(bool condition,
|
||||
std::string_view conditionString,
|
||||
std::string_view file,
|
||||
int line);
|
||||
|
||||
template<class L, class R>
|
||||
void compare(const L& lhs,
|
||||
const R& rhs,
|
||||
std::string_view lhsString,
|
||||
std::string_view rhsString,
|
||||
std::string_view file,
|
||||
int line) {
|
||||
if (lhs != rhs) {
|
||||
completeTestFunction(
|
||||
TestResult::Fail,
|
||||
formatMessage(file, line, "Comparison failed: " + std::string(lhsString) + " == " + std::string(rhsString)));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QtWasmTest
|
||||
|
||||
#define QWASMVERIFY(condition) \
|
||||
QtWasmTest::verify((condition), #condition, __FILE__, __LINE__);
|
||||
|
||||
#define QWASMCOMPARE(left, right) \
|
||||
QtWasmTest::compare((left), (right), #left, #right, __FILE__, __LINE__);
|
||||
|
||||
#define QWASMSUCCESS() \
|
||||
QtWasmTest::completeTestFunction(QtWasmTest::Pass, "")
|
||||
|
||||
#define QWASMFAIL(message) \
|
||||
QtWasmTest::completeTestFunction(QtWasmTest::Fail, QtWasmTest::formatMessage(__FILE__, __LINE__, message))
|
||||
|
||||
#endif
|
135
tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.js
Normal file
135
tests/manual/wasm/qtwasmtestlib/qtwasmtestlib.js
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright (C) 2022 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
// A minimal async test runner for Qt async auto tests.
|
||||
//
|
||||
// Usage: Call runTest(name, testFunctionCompleted), where "name" is the name of the app
|
||||
// (the .wasm file name), and testFunctionCompleted is a test-function-complete
|
||||
// callback. The test runner will then instantiate the app and run tests.
|
||||
//
|
||||
// The test runner expects that the app instance defines the following
|
||||
// functions:
|
||||
//
|
||||
// void cleanupTestCase()
|
||||
// string getTestFunctions()
|
||||
// runTestFunction(string)
|
||||
//
|
||||
// Further, the test runner expects that the app instance calls
|
||||
// completeTestFunction() (below - note that both the instance and this
|
||||
// file have a function with that name) when a test function finishes. This
|
||||
// can be done during runTestFunction(), or after it has returned (this
|
||||
// is the part which enables async testing). Test functions which fail
|
||||
// to call completeTestFunction() will time out after 2000ms.
|
||||
//
|
||||
const g_maxTime = 2000;
|
||||
|
||||
class TestFunction {
|
||||
constructor(instance, name) {
|
||||
this.instance = instance;
|
||||
this.name = name;
|
||||
this.resolve = undefined;
|
||||
this.reject = undefined;
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
|
||||
complete(result, details) {
|
||||
// Reset timeout
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
|
||||
const callback = result.startsWith('FAIL') ? this.reject : this.resolve;
|
||||
callback(`${result}${details ? ': ' + details : ''}`);
|
||||
}
|
||||
|
||||
run() {
|
||||
// Set timer which will catch test functions
|
||||
// which fail to call completeTestFunction()
|
||||
this.timeoutId = setTimeout(() => {
|
||||
completeTestFunction(this.name, 'FAIL', `Timeout after ${g_maxTime} ms`)
|
||||
}, g_maxTime);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
|
||||
this.instance.runTestFunction(this.name);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function completeTestFunction(testFunctionName, result, details) {
|
||||
if (!window.currentTestFunction || testFunctionName !== window.currentTestFunction.name)
|
||||
return;
|
||||
|
||||
window.currentTestFunction.complete(result, details);
|
||||
}
|
||||
|
||||
async function runTestFunction(instance, name) {
|
||||
if (window.currentTestFunction) {
|
||||
throw new Error(`While trying to run ${name}: Last function hasn't yet finished`);
|
||||
}
|
||||
window.currentTestFunction = new TestFunction(instance, name);
|
||||
try {
|
||||
const result = await window.currentTestFunction.run();
|
||||
return result;
|
||||
} finally {
|
||||
delete window.currentTestFunction;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTestCaseImpl(testFunctionStarted, testFunctionCompleted, qtContainers) {
|
||||
// Create test case instance
|
||||
const config = {
|
||||
qtContainerElements: qtContainers || []
|
||||
}
|
||||
const instance = await createQtAppInstance(config);
|
||||
|
||||
// Run all test functions
|
||||
const functionsString = instance.getTestFunctions();
|
||||
const functions = functionsString.split(" ").filter(Boolean);
|
||||
for (const name of functions) {
|
||||
testFunctionStarted(name);
|
||||
try {
|
||||
const result = await runTestFunction(instance, name);
|
||||
testFunctionCompleted(result);
|
||||
} catch (err) {
|
||||
testFunctionCompleted(err.message ?? err);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
instance.cleanupTestCase();
|
||||
}
|
||||
|
||||
var g_htmlLogElement = undefined;
|
||||
|
||||
function testFunctionStarted(name) {
|
||||
let line = name + ": ";
|
||||
g_htmlLogElement.innerHTML += line;
|
||||
}
|
||||
|
||||
function testFunctionCompleted(status) {
|
||||
|
||||
const color = (status) => {
|
||||
if (status.startsWith("PASS"))
|
||||
return "green";
|
||||
if (status.startsWith("FAIL"))
|
||||
return "red";
|
||||
if (status.startsWith("SKIP"))
|
||||
return "tan";
|
||||
return "black";
|
||||
};
|
||||
|
||||
const line = `<span style='color: ${color(status)};'>${status}</text><br>`;
|
||||
g_htmlLogElement.innerHTML += line;
|
||||
}
|
||||
|
||||
async function runTestCase(htmlLogElement, qtContainers) {
|
||||
g_htmlLogElement = htmlLogElement;
|
||||
try {
|
||||
await runTestCaseImpl(testFunctionStarted, testFunctionCompleted, qtContainers);
|
||||
g_htmlLogElement.innerHTML += "<br> DONE"
|
||||
} catch (err) {
|
||||
g_htmlLogElement.innerHTML += err
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user