qt 6.5.1 original

This commit is contained in:
kleuter
2023-10-29 23:33:08 +01:00
parent 71d22ab6b0
commit 85d238dfda
21202 changed files with 5499099 additions and 0 deletions

View File

@ -0,0 +1,60 @@
This package contains sources for a webpage whose scripts run batched WASM tests - a single
executable with a number of linked test classes.
The webpage operates on an assumption that the test program, when run without arguments,
prints out a list of test classes inside its module. Then, when run with the first argument
equal to the name of one of the test classes, the test program will execute all tests within
that single class.
The following query parameters are recognized by the webpage:
qtestname=testname - the test case to run. When batched test module is used, the test is assumed to
be a part of the batch. If a standalone test module is used, this is assumed to be the name of
the wasm module.
quseemrun - if specified, the test communicates with the emrun instance via the protocol expected
by emrun.
qtestoutputformat=txt|xml|lightxml|junitxml|tap - specifies the output format for the test case.
qbatchedtest - if specified, the script will load the test_batch.wasm module and either run all
testcases in it or a specific test case, depending on the existence of the qtestname parameter.
Otherwise, the test is assumed to be a standalone binary whose name is determined by the
qtestname parameter.
The scripts in the page will load the wasm file as specified by a combination of qbatchedtest and
qtestname.
Public interface for querying the test execution status is accessible via the global object
'qtTestRunner':
qtTestRunner.status - this contains the status of the test runner itself, of the enumeration type
RunnerStatus.
qtTestRunner.results - a map of test class name to test result. The result contains a test status
(status, of the enumeration TestStatus), text output chunks (output), and in case of a terminal
status, also the test's exit code (exitCode)
qtTestRunner.onStatusChanged - an event for changes in state of the runner itself. The possible
values are those of the enumeration RunnerStatus.
qtTestRunner.onTestStatusChanged - an event for changes in state of a single tests class. The
possible values are those of the enumeration TestStatus. When a terminal state is reached
(Completed, Error, Crashed), the text results and exit code are filled in, if available, and
will not change.
Typical usage:
Run all tests in a batch:
- load the webpage batchedtestrunner.html
Run a single test in a batch:
- load the webpage batchedtestrunner.html?qtestname=tst_mytest
Query for test execution state:
- qtTestRunner.onStatusChanged.addEventListener((runnerStatus) => (...)))
- qtTestRunner.onTestStatusChanged.addEventListener((testName, status) => (...))
- qtTestRunner.status === (...)
- qtTestRunner.results['tst_mytest'].status === (...)
- qtTestRunner.results['tst_mytest'].textOutput
When queseemrun is specified, the built-in emrun support module will POST the test output to the
emrun instance and will report ^exit^ with a suitable exit code to it when testing is finished.

View File

@ -0,0 +1,15 @@
<!--
Copyright (C) 2022 The Qt Company Ltd.
SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
-->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WASM batched test runner (emrun-enabled)</title>
<link rel="stylesheet" href="qtestoutputreporter.css"></link>
<script type="module" defer="defer" src="qwasmtestmain.js"></script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,177 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import {
AbortedError,
} from './qwasmjsruntime.js';
import { EventSource } from './util.js';
class ProgramError extends Error {
constructor(exitCode) {
super(`The program reported an exit code of ${exitCode}`)
}
}
export class RunnerStatus {
static Running = 'Running';
static Passed = 'Passed';
static Error = 'Error';
static TestCrashed = 'TestCrashed';
static TestsFailed = 'TestsFailed';
}
export class TestStatus {
static Pending = 'Pending';
static Running = 'Running';
static Completed = 'Completed';
static Error = 'Error';
static Failed = 'Failed';
static Crashed = 'Crashed';
}
export class BatchedTestRunner {
static #TestBatchModuleName = 'test_batch';
#loader;
#results = new Map();
#status = RunnerStatus.Running;
#numberOfFailed = 0;
#statusChangedEventPrivate;
#testStatusChangedEventPrivate;
#testOutputChangedEventPrivate;
#errorDetails;
onStatusChanged =
new EventSource((privateInterface) => this.#statusChangedEventPrivate = privateInterface);
onTestStatusChanged =
new EventSource((privateInterface) =>
this.#testStatusChangedEventPrivate = privateInterface);
onTestOutputChanged =
new EventSource(
(privateInterface) => this.#testOutputChangedEventPrivate = privateInterface);
constructor(loader) {
this.#loader = loader;
}
get results() { return this.#results; }
get status() { return this.#status; }
get numberOfFailed() {
if (this.#status !== RunnerStatus.TestsFailed)
throw new Error(`numberOfFailed called with status=${this.#status}`);
return this.#numberOfFailed;
}
get errorDetails() { return this.#errorDetails; }
async run(targetIsBatch, testName, testOutputFormat) {
try {
await this.#doRun(targetIsBatch, testName, testOutputFormat);
} catch (e) {
this.#setTestRunnerError(e.message);
return;
}
const status = (() => {
const hasAnyCrashedTest =
!![...window.qtTestRunner.results.values()].find(
result => result.status === TestStatus.Crashed);
if (hasAnyCrashedTest)
return { code: RunnerStatus.TestCrashed };
const numberOfFailed = [...window.qtTestRunner.results.values()].reduce(
(previous, current) => previous + current.exitCode, 0);
return {
code: (numberOfFailed ? RunnerStatus.TestsFailed : RunnerStatus.Passed),
numberOfFailed
};
})();
this.#setTestRunnerStatus(status.code, status.numberOfFailed);
}
async #doRun(targetIsBatch, testName, testOutputFormat) {
const module = await this.#loader.loadEmscriptenModule(
targetIsBatch ? BatchedTestRunner.#TestBatchModuleName : testName,
() => { }
);
const testsToExecute = (testName || !targetIsBatch)
? [testName] : await this.#getTestClassNames(module);
testsToExecute.forEach(testClassName => this.#registerTest(testClassName));
for (const testClassName of testsToExecute) {
let result = {};
this.#setTestStatus(testClassName, TestStatus.Running);
try {
const LogToStdoutSpecialFilename = '-';
result = await module.exec({
args: [...(targetIsBatch ? [testClassName] : []),
'-o', `${LogToStdoutSpecialFilename},${testOutputFormat}`],
onStdout: (output) => {
this.#addTestOutput(testClassName, output);
}
});
if (result.exitCode < 0)
throw new ProgramError(result.exitCode);
result.status = result.exitCode > 0 ? TestStatus.Failed : TestStatus.Completed;
// Yield to other tasks on the main thread.
await new Promise(resolve => window.setTimeout(resolve, 0));
} catch (e) {
result.status = e instanceof ProgramError ? TestStatus.Error : TestStatus.Crashed;
result.stdout = e instanceof AbortedError ? e.stdout : result.stdout;
}
this.#setTestResultData(testClassName, result.status, result.exitCode);
}
}
async #getTestClassNames(module) {
return (await module.exec()).stdout.trim().split(' ');
}
#registerTest(testName) {
this.#results.set(testName, { status: TestStatus.Pending, output: [] });
}
#setTestStatus(testName, status) {
const testData = this.#results.get(testName);
if (testData.status === status)
return;
this.#results.get(testName).status = status;
this.#testStatusChangedEventPrivate.fireEvent(testName, status);
}
#setTestResultData(testName, testStatus, exitCode) {
const testData = this.#results.get(testName);
const statusChanged = testStatus !== testData.status;
testData.status = testStatus;
testData.exitCode = exitCode;
if (statusChanged)
this.#testStatusChangedEventPrivate.fireEvent(testName, testStatus);
}
#setTestRunnerStatus(status, numberOfFailed) {
if (status === this.#status)
return;
this.#status = status;
this.#numberOfFailed = numberOfFailed;
this.#statusChangedEventPrivate.fireEvent(status);
}
#setTestRunnerError(details) {
this.#status = RunnerStatus.Error;
this.#errorDetails = details;
this.#statusChangedEventPrivate.fireEvent(this.#status);
}
#addTestOutput(testName, output) {
const testData = this.#results.get(testName);
testData.output.push(output);
this.#testOutputChangedEventPrivate.fireEvent(testName, testData.output);
}
}

View File

@ -0,0 +1,119 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import { RunnerStatus, TestStatus } from './batchedtestrunner.js';
// Sends messages to the running emrun instance via POST requests.
export class EmrunCommunication {
#indexOfMessage = 0;
#postOutputPromises = [];
#post(body) {
return fetch('stdio.html', {
method: 'POST',
body
});
}
// Returns a promise whose resolution signals that all outstanding traffic to the emrun instance
// has been completed.
waitUntilAllSent() {
return Promise.all(this.#postOutputPromises);
}
// Posts the exit status to the running emrun instance. Emrun will drop connection unless it is
// run with --serve_after_exit, therefore this method will throw most of the times.
postExit(status) {
return this.#post(`^exit^${status}`);
}
// Posts an indexed output chunk to the running emrun instance. Each consecutive call to this
// method increments the output index by 1.
postOutput(output) {
const newPromise = this.#post(`^out^${this.#indexOfMessage++}^${output}`);
this.#postOutputPromises.push(newPromise);
newPromise.finally(() => {
this.#postOutputPromises.splice(this.#postOutputPromises.indexOf(newPromise), 1);
});
return newPromise;
}
}
// Wraps a test module runner; forwards its output and resolution state to the running emrun
// instance.
export class EmrunAdapter {
#communication;
#batchedTestRunner;
#sentLines = 0;
#onExitSent;
constructor(communication, batchedTestRunner, onExitSent) {
this.#communication = communication;
this.#batchedTestRunner = batchedTestRunner;
this.#onExitSent = onExitSent;
}
// Starts listening to test module runner's state changes. When the test module runner finishes
// or reports output, sends suitable messages to the emrun instance.
run() {
this.#batchedTestRunner.onStatusChanged.addEventListener(
status => this.#onRunnerStatusChanged(status));
this.#batchedTestRunner.onTestStatusChanged.addEventListener(
(test, status) => this.#onTestStatusChanged(test, status));
this.#batchedTestRunner.onTestOutputChanged.addEventListener(
(test, output) => this.#onTestOutputChanged(test, output));
const currentTest = [...this.#batchedTestRunner.results.entries()].find(
entry => entry[1].status === TestStatus.Running)?.[0];
const output = this.#batchedTestRunner.results.get(currentTest)?.output;
if (output)
this.#onTestOutputChanged(testName, output);
this.#onRunnerStatusChanged(this.#batchedTestRunner.status);
}
#toExitCode(status) {
switch (status) {
case RunnerStatus.Error:
return -1;
case RunnerStatus.Passed:
return 0;
case RunnerStatus.Running:
throw new Error('No exit code when still running');
case RunnerStatus.TestCrashed:
return -2;
case RunnerStatus.TestsFailed:
return this.#batchedTestRunner.numberOfFailed;
}
}
async #onRunnerStatusChanged(status) {
if (RunnerStatus.Running === status)
return;
const exit = this.#toExitCode(status);
if (RunnerStatus.Error === status)
this.#communication.postOutput(this.#batchedTestRunner.errorDetails);
await this.#communication.waitUntilAllSent();
try {
await this.#communication.postExit(exit);
} catch {
// no-op: The remote end will drop connection on exit.
} finally {
this.#onExitSent?.();
}
}
async #onTestOutputChanged(_, output) {
const notSent = output.slice(this.#sentLines);
for (const out of notSent)
this.#communication.postOutput(out);
this.#sentLines = output.length;
}
async #onTestStatusChanged(_, status) {
if (status === TestStatus.Running)
this.#sentLines = 0;
}
}

View File

@ -0,0 +1,89 @@
/*
Copyright (C) 2022 The Qt Company Ltd.
SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
*/
:root {
--good-color-light: chartreuse;
--bad-color-light: lightcoral;
--warning-color-light: orange;
--info-color-light: cornflowerblue;
--ignore-color-light: gray;
--good-color-dark: green;
--bad-color-dark: red;
--warning-color-dark: darkorange;
--info-color-dark: blue;
--ignore-color-dark: lightgray;
}
.zero {
display: none;
}
.light-background .good {
color: var(--good-color-dark);
}
.light-background .bad {
color: var(--bad-color-dark);
}
.light-background .warning {
color: var(--warning-color-dark);
}
.light-background .info {
color: var(--info-color-dark);
}
.light-background .ignore {
color: var(--ignore-color-dark);
}
.output-area {
font-family: monospace;
}
.output-line {
display: block;
white-space: pre-wrap;
}
.counter-box {
position: fixed;
width: 100%;
display: flex;
justify-content: center;
}
.counter-box span {
padding-right: 10px;
}
.counter-box .pass {
background-color: var(--good-color-light);
}
.counter-box .fail {
background-color: var(--bad-color-light);
}
.counter-box .skip {
background-color: var(--info-color-light);
}
.counter-box .xfail {
background-color: var(--warning-color-light);
}
.counter-box .xpass {
background-color: var(--bad-color-light);
}
.counter-box .bpass,
.counter-box .bfail,
.counter-box .bxpass,
.counter-box .bxfail,
.counter-box .other {
background-color: var(--ignore-color-light);
}

View File

@ -0,0 +1,366 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import { RunnerStatus, TestStatus } from './batchedtestrunner.js'
class AttentionType
{
static None = 1;
static Bad = 2;
static Good = 3;
static Warning = 4;
static Info = 5;
static Ignore = 6;
};
export class IncidentType
{
// See QAbstractTestLogger::IncidentTypes (and keep in sync with it):
static Pass = 'pass';
static Fail = 'fail';
static Skip = 'skip';
static XFail = 'xfail';
static XPass = 'xpass';
static BlacklistedPass = 'bpass';
static BlacklistedFail = 'bfail';
static BlacklistedXPass = 'bxpass';
static BlacklistedXFail = 'bxfail';
// The following is not mapped from QAbstractTestLogger::IncidentTypes and is used internally:
static None = 'none';
static values()
{
return Object.getOwnPropertyNames(IncidentType)
.filter(
propertyName =>
['length', 'prototype', 'values', 'name'].indexOf(propertyName) === -1)
.map(propertyName => IncidentType[propertyName]);
}
}
class OutputArea
{
#outputDiv;
constructor()
{
this.#outputDiv = document.createElement('div');
this.#outputDiv.classList.add('output-area');
this.#outputDiv.classList.add('light-background');
document.querySelector('body').appendChild(this.#outputDiv);
}
addOutput(text, attentionType)
{
const newContentWrapper = document.createElement('span');
newContentWrapper.className = 'output-line';
newContentWrapper.innerText = text;
switch (attentionType) {
case AttentionType.Bad:
newContentWrapper.classList.add('bad');
break;
case AttentionType.Good:
newContentWrapper.classList.add('good');
break;
case AttentionType.Warning:
newContentWrapper.classList.add('warning');
break
case AttentionType.Info:
newContentWrapper.classList.add('info');
break;
case AttentionType.Ignore:
newContentWrapper.classList.add('ignore');
break;
default:
break;
}
this.#outputDiv.appendChild(newContentWrapper);
}
}
class Counter
{
#count = 0;
#decriptionElement;
#counterElement;
constructor(parentElement, incidentType)
{
this.#decriptionElement = document.createElement('span');
this.#decriptionElement.classList.add(incidentType);
this.#decriptionElement.classList.add('zero');
this.#decriptionElement.innerText = Counter.#humanReadableIncidentName(incidentType);
parentElement.appendChild(this.#decriptionElement);
this.#counterElement = document.createElement('span');
this.#counterElement.classList.add(incidentType);
this.#counterElement.classList.add('zero');
parentElement.appendChild(this.#counterElement);
}
increment()
{
if (!this.#count++) {
this.#decriptionElement.classList.remove('zero');
this.#counterElement.classList.remove('zero');
}
this.#counterElement.innerText = this.#count;
}
static #humanReadableIncidentName(incidentName)
{
switch (incidentName) {
case IncidentType.Pass:
return 'Passed';
case IncidentType.Fail:
return 'Failed';
case IncidentType.Skip:
return 'Skipped';
case IncidentType.XFail:
return 'Known failure';
case IncidentType.XPass:
return 'Unexpectedly passed';
case IncidentType.BlacklistedPass:
return 'Blacklisted passed';
case IncidentType.BlacklistedFail:
return 'Blacklisted failed';
case IncidentType.BlacklistedXPass:
return 'Blacklisted unexpectedly passed';
case IncidentType.BlacklistedXFail:
return 'Blacklisted unexpectedly failed';
case IncidentType.None:
throw new Error('Incident of the None type cannot be displayed');
}
}
}
class Counters
{
#contentsDiv;
#counters;
constructor(parentElement)
{
this.#contentsDiv = document.createElement('div');
this.#contentsDiv.className = 'counter-box';
parentElement.appendChild(this.#contentsDiv);
const centerDiv = document.createElement('div');
this.#contentsDiv.appendChild(centerDiv);
this.#counters = new Map(IncidentType.values()
.filter(incidentType => incidentType !== IncidentType.None)
.map(incidentType => [incidentType, new Counter(centerDiv, incidentType)]));
}
incrementIncidentCounter(incidentType)
{
this.#counters.get(incidentType).increment();
}
}
export class UI
{
#contentsDiv;
#counters;
#outputArea;
constructor(parentElement, hasCounters)
{
this.#contentsDiv = document.createElement('div');
parentElement.appendChild(this.#contentsDiv);
if (hasCounters)
this.#counters = new Counters(this.#contentsDiv);
this.#outputArea = new OutputArea(this.#contentsDiv);
}
get counters()
{
return this.#counters;
}
get outputArea()
{
return this.#outputArea;
}
htmlElement()
{
return this.#contentsDiv;
}
}
class OutputScanner
{
static #supportedIncidentTypes = IncidentType.values().filter(
incidentType => incidentType !== IncidentType.None);
static get supportedIncidentTypes()
{
return this.#supportedIncidentTypes;
}
#regex;
constructor(regex)
{
this.#regex = regex;
}
classifyOutputLine(line)
{
const match = this.#regex.exec(line);
if (!match)
return IncidentType.None;
match.splice(0, 1);
// Find the index of the first non-empty matching group and recover an incident type for it.
return OutputScanner.supportedIncidentTypes[match.findIndex(element => !!element)];
}
}
class XmlOutputScanner extends OutputScanner
{
constructor()
{
// Scan for any line with an incident of type from supportedIncidentTypes. The matching
// group at offset n will contain the type. The match type can be preceded by any number of
// whitespace characters to factor in the indentation.
super(new RegExp(`^\\s*<Incident type="${OutputScanner.supportedIncidentTypes
.map(incidentType => `(${incidentType})`).join('|')}"`));
}
}
class TextOutputScanner extends OutputScanner
{
static #incidentNameMap = new Map([
[IncidentType.Pass, 'PASS'],
[IncidentType.Fail, 'FAIL!'],
[IncidentType.Skip, 'SKIP'],
[IncidentType.XFail, 'XFAIL'],
[IncidentType.XPass, 'XPASS'],
[IncidentType.BlacklistedPass, 'BPASS'],
[IncidentType.BlacklistedFail, 'BFAIL'],
[IncidentType.BlacklistedXPass, 'BXPASS'],
[IncidentType.BlacklistedXFail, 'BXFAIL']
]);
constructor()
{
// Scan for any line with an incident of type from incidentNameMap. The matching group
// at offset n will contain the type. The type can be preceded by any number of whitespace
// characters to factor in the indentation.
super(new RegExp(`^\\s*${OutputScanner.supportedIncidentTypes
.map(incidentType =>
`(${TextOutputScanner.#incidentNameMap.get(incidentType)})`).join('|')}\\s`));
}
}
export class ScannerFactory
{
static createScannerForFormat(format)
{
switch (format) {
case 'txt':
return new TextOutputScanner();
case 'xml':
return new XmlOutputScanner();
default:
return null;
}
}
}
export class VisualOutputProducer
{
#batchedTestRunner;
#outputArea;
#counters;
#outputScanner;
#processedLines;
constructor(outputArea, counters, outputScanner, batchedTestRunner)
{
this.#outputArea = outputArea;
this.#counters = counters;
this.#outputScanner = outputScanner;
this.#batchedTestRunner = batchedTestRunner;
this.#processedLines = 0;
}
run()
{
this.#batchedTestRunner.onStatusChanged.addEventListener(
status => this.#onRunnerStatusChanged(status));
this.#batchedTestRunner.onTestStatusChanged.addEventListener(
(test, status) => this.#onTestStatusChanged(test, status));
this.#batchedTestRunner.onTestOutputChanged.addEventListener(
(test, output) => this.#onTestOutputChanged(test, output));
const currentTest = [...this.#batchedTestRunner.results.entries()].find(
entry => entry[1].status === TestStatus.Running)?.[0];
const output = this.#batchedTestRunner.results.get(currentTest)?.output;
if (output)
this.#onTestOutputChanged(testName, output);
this.#onRunnerStatusChanged(this.#batchedTestRunner.status);
}
async #onRunnerStatusChanged(status)
{
if (RunnerStatus.Running === status)
return;
this.#outputArea.addOutput(
`Runner exited with status: ${status}`,
status === RunnerStatus.Passed ? AttentionType.Good : AttentionType.Bad);
if (RunnerStatus.Error === status)
this.#outputArea.addOutput(`The error was: ${this.#batchedTestRunner.errorDetails}`);
}
async #onTestOutputChanged(_, output)
{
const notSent = output.slice(this.#processedLines);
for (const out of notSent) {
const incidentType = this.#outputScanner?.classifyOutputLine(out);
if (incidentType !== IncidentType.None)
this.#counters.incrementIncidentCounter(incidentType);
this.#outputArea.addOutput(
out,
(() =>
{
switch (incidentType) {
case IncidentType.Fail:
case IncidentType.XPass:
return AttentionType.Bad;
case IncidentType.Pass:
return AttentionType.Good;
case IncidentType.XFail:
return AttentionType.Warning;
case IncidentType.Skip:
return AttentionType.Info;
case IncidentType.BlacklistedFail:
case IncidentType.BlacklistedPass:
case IncidentType.BlacklistedXFail:
case IncidentType.BlacklistedXPass:
return AttentionType.Ignore;
case IncidentType.None:
return AttentionType.None;
}
})());
}
this.#processedLines = output.length;
}
async #onTestStatusChanged(_, status)
{
if (status === TestStatus.Running)
this.#processedLines = 0;
await new Promise(resolve => window.setTimeout(resolve, 500));
}
}

View File

@ -0,0 +1,229 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Exposes platform capabilities as static properties
export class AbortedError extends Error {
constructor(stdout) {
super(`The program has been aborted`)
this.stdout = stdout;
}
}
export class Platform {
static #webAssemblySupported = typeof WebAssembly !== 'undefined';
static #canCompileStreaming = WebAssembly.compileStreaming !== 'undefined';
static #webGLSupported = (() => {
// We expect that WebGL is supported if WebAssembly is; however
// the GPU may be blacklisted.
try {
const canvas = document.createElement('canvas');
return !!(
window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
);
} catch (e) {
return false;
}
})();
static #canLoadQt = Platform.#webAssemblySupported && Platform.#webGLSupported;
static get webAssemblySupported() {
return this.#webAssemblySupported;
}
static get canCompileStreaming() {
return this.#canCompileStreaming;
}
static get webGLSupported() {
return this.#webGLSupported;
}
static get canLoadQt() {
return this.#canLoadQt;
}
}
// Locates a resource, based on its relative path
export class ResourceLocator {
#rootPath;
constructor(rootPath) {
this.#rootPath = rootPath;
if (rootPath.length > 0 && !rootPath.endsWith('/')) rootPath += '/';
}
locate(relativePath) {
return this.#rootPath + relativePath;
}
}
// Allows fetching of resources, such as text resources or wasm modules.
export class ResourceFetcher {
#locator;
constructor(locator) {
this.#locator = locator;
}
async fetchText(filePath) {
return (await this.#fetchRawResource(filePath)).text();
}
async fetchCompileWasm(filePath, onFetched) {
const fetchResponse = await this.#fetchRawResource(filePath);
onFetched?.();
if (Platform.canCompileStreaming) {
try {
return await WebAssembly.compileStreaming(fetchResponse);
} catch {
// NOOP - fallback to sequential fetching below
}
}
return WebAssembly.compile(await fetchResponse.arrayBuffer());
}
async #fetchRawResource(filePath) {
const response = await fetch(this.#locator.locate(filePath));
if (!response.ok)
throw new Error(
`${response.status} ${response.statusText} ${response.url}`
);
return response;
}
}
// Represents a WASM module, wrapping the instantiation and execution thereof.
export class CompiledModule {
#createQtAppInstanceFn;
#js;
#wasm;
#resourceLocator;
constructor(createQtAppInstanceFn, js, wasm, resourceLocator) {
this.#createQtAppInstanceFn = createQtAppInstanceFn;
this.#js = js;
this.#wasm = wasm;
this.#resourceLocator = resourceLocator;
}
static make(js, wasm, resourceLocator
) {
const exports = {};
eval(js);
if (!exports.createQtAppInstance) {
throw new Error(
'createQtAppInstance has not been exported by the main script'
);
}
return new CompiledModule(
exports.createQtAppInstance, js, wasm, resourceLocator
);
}
async exec(parameters) {
return await new Promise(async (resolve, reject) => {
let instance = undefined;
let result = undefined;
const continuation = () => {
if (!(instance && result))
return;
resolve({
stdout: result.stdout,
exitCode: result.exitCode,
instance,
});
};
instance = await this.#createQtAppInstanceFn((() => {
const params = this.#makeDefaultExecParams({
onInstantiationError: (error) => { reject(error); },
});
params.arguments = parameters?.args;
let data = '';
params.print = (out) => {
parameters?.onStdout?.(out);
data += `${out}\n`;
};
params.printErr = () => { };
params.onAbort = () => reject(new AbortedError(data));
params.quit = (code, exception) => {
if (exception && exception.name !== 'ExitStatus')
reject(exception);
result = { stdout: data, exitCode: code };
continuation();
};
return params;
})());
continuation();
});
}
#makeDefaultExecParams(params) {
const instanceParams = {};
instanceParams.instantiateWasm = async (imports, onDone) => {
try {
onDone(await WebAssembly.instantiate(this.#wasm, imports), this.#wasm);
} catch (e) {
params?.onInstantiationError?.(e);
}
};
instanceParams.locateFile = (filename) =>
this.#resourceLocator.locate(filename);
instanceParams.monitorRunDependencies = (name) => { };
instanceParams.print = (text) => true && console.log(text);
instanceParams.printErr = (text) => true && console.warn(text);
instanceParams.preRun = [
(instance) => {
const env = {};
instance.ENV = env;
},
];
instanceParams.mainScriptUrlOrBlob = new Blob([this.#js], {
type: 'text/javascript',
});
return instanceParams;
}
}
// Streamlines loading of WASM modules.
export class ModuleLoader {
#fetcher;
#resourceLocator;
constructor(
fetcher,
resourceLocator
) {
this.#fetcher = fetcher;
this.#resourceLocator = resourceLocator;
}
// Loads an emscripten module named |moduleName| from the main resource path. Provides
// progress of 'downloading' and 'compiling' to the caller using the |onProgress| callback.
async loadEmscriptenModule(
moduleName, onProgress
) {
if (!Platform.webAssemblySupported)
throw new Error('Web assembly not supported');
if (!Platform.webGLSupported)
throw new Error('WebGL is not supported');
onProgress('downloading');
const jsLoadPromise = this.#fetcher.fetchText(`${moduleName}.js`);
const wasmLoadPromise = this.#fetcher.fetchCompileWasm(
`${moduleName}.wasm`,
() => {
onProgress('compiling');
}
);
const [js, wasm] = await Promise.all([jsLoadPromise, wasmLoadPromise]);
return CompiledModule.make(js, wasm, this.#resourceLocator);
}
}

View File

@ -0,0 +1,69 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import { BatchedTestRunner } from './batchedtestrunner.js'
import { EmrunAdapter, EmrunCommunication } from './emrunadapter.js'
import {
ModuleLoader,
ResourceFetcher,
ResourceLocator,
} from './qwasmjsruntime.js';
import { parseQuery } from './util.js';
import { VisualOutputProducer, UI, ScannerFactory } from './qtestoutputreporter.js'
(() => {
const setPageTitle = (useEmrun, testName, isBatch) => {
document.title = 'Qt WASM test runner';
if (useEmrun || testName || isBatch) {
document.title += `(${[
...[useEmrun ? ['emrun'] : []],
...[testName ? ['test=' + testName] : []],
...[isBatch ? ['batch'] : []]
].flat().join(", ")})`;
}
}
const parsed = parseQuery(location.search);
const outputInPage = parsed.get('qvisualoutput') !== undefined;
const testName = parsed.get('qtestname');
const isBatch = parsed.get('qbatchedtest') !== undefined;
const useEmrun = parsed.get('quseemrun') !== undefined;
if (testName === undefined) {
if (!isBatch)
throw new Error('The qtestname parameter is required if not running a batch');
} else if (testName === '') {
throw new Error(`The qtestname=${testName} parameter is incorrect`);
}
const testOutputFormat = (() => {
const format = parsed.get('qtestoutputformat') ?? 'txt';
if (-1 === ['txt', 'xml', 'lightxml', 'junitxml', 'tap'].indexOf(format))
throw new Error(`Bad file format: ${format}`);
return format;
})();
const resourceLocator = new ResourceLocator('');
const testRunner = new BatchedTestRunner(
new ModuleLoader(new ResourceFetcher(resourceLocator), resourceLocator),
);
window.qtTestRunner = testRunner;
if (useEmrun) {
const adapter = new EmrunAdapter(new EmrunCommunication(), testRunner, () => {
if (!outputInPage)
window.close();
});
adapter.run();
}
if (outputInPage) {
const scanner = ScannerFactory.createScannerForFormat(testOutputFormat);
const ui = new UI(document.querySelector('body'), !!scanner);
const adapter =
new VisualOutputProducer(ui.outputArea, ui.counters, scanner, testRunner);
adapter.run();
}
setPageTitle(useEmrun, testName, isBatch);
testRunner.run(isBatch, testName, testOutputFormat);
})();

View File

@ -0,0 +1,31 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
export function parseQuery() {
const trimmed = window.location.search.substring(1);
return new Map(
trimmed.length === 0 ?
[] :
trimmed.split('&').map(paramNameAndValue => {
const [name, value] = paramNameAndValue.split('=');
return [decodeURIComponent(name), value ? decodeURIComponent(value) : ''];
}));
}
export class EventSource {
#listeners = [];
constructor(receivePrivateInterface) {
receivePrivateInterface({
fireEvent: (arg0, arg1) => this.#fireEvent(arg0, arg1)
});
}
addEventListener(listener) {
this.#listeners.push(listener);
}
#fireEvent(arg0, arg1) {
this.#listeners.forEach(listener => listener(arg0, arg1));
}
}