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));
}
}

View File

@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
netifaces = "*"
[dev-packages]
[requires]
python_version = "3.9"

View File

@ -0,0 +1,124 @@
#!/usr/bin/env python3
# Copyright (C) 2021 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import os
import socket
import ssl
import sys
import threading
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from subprocess import run
import netifaces as ni
import argparse
# This script implements a web server which serves the content of the current
# working directory using the http and secure https protocols. The server is
# intented to be used as a development server.
#
# Https certificates are generated using the 'mkcert' utility. You should generate
# a certificate authority first, see the mkcert documentation at
# https://github.com/FiloSottile/mkcert
#
# The server sets the COOP and COEP headers, which are required to enable multithreading.
def main():
parser = argparse.ArgumentParser(
description="Run a minimal HTTP(S) server to test Qt for WebAssembly applications.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--port",
"-p",
help="Port on which to listen for HTTP and HTTPS (PORT + 1)",
type=int,
default=8000,
)
parser.add_argument(
"--address",
"-a",
help="Address on which to listen for HTTP and HTTPS, in addition to localhost",
action="append",
)
parser.add_argument(
"--all",
help="Start web server which binds to all local interfaces, instead of locahost only",
action="store_true",
)
parser.add_argument(
"path", help="The directory to serve", nargs="?", default=os.getcwd()
)
args = parser.parse_args()
http_port = args.port
https_port = http_port + 1
all_addresses = args.all
cmd_addresses = args.address or []
serve_path = args.path
addresses = ["127.0.0.1"] + cmd_addresses
if all_addresses:
addresses += [
addr[ni.AF_INET][0]["addr"]
for addr in map(ni.ifaddresses, ni.interfaces())
if ni.AF_INET in addr
]
addresses = sorted(set(addresses)) # deduplicate
# Generate a https certificate for "localhost" and selected addresses. This
# requires that the mkcert utility is installed, and that a certificate
# authority key pair (rootCA-key.pem and rootCA.pem) has been generated. The
# certificates are written to /tmp, where the https server can find them
# later on.
cert_base_path = "/tmp/qtwasmserver-certificate"
cert_file = f"{cert_base_path}.pem"
cert_key_file = f"{cert_base_path}-key.pem"
addresses_string = f"localhost {' '.join(addresses)}"
ret = run(
f"mkcert -cert-file {cert_file} -key-file {cert_key_file} {addresses_string}",
shell=True,
)
has_certificate = ret.returncode == 0
if not has_certificate:
print(
"Warning: mkcert is not installed or was unable to create a certificate. Will not start HTTPS server."
)
# Http request handler which sends headers required to enable multithreading using SharedArrayBuffer.
class MyHTTPRequestHandler(SimpleHTTPRequestHandler):
def __init__(self, request, client_address, server):
super().__init__(request, client_address, server, directory=serve_path)
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
self.send_header("Cross-Origin-Resource-Policy", "cross-origin")
SimpleHTTPRequestHandler.end_headers(self)
# Serve cwd from http(s)://address:port, with certificates from certdir if set
def serve_on_thread(address, port, secure):
httpd = ThreadingHTTPServer((address, port), MyHTTPRequestHandler)
if secure:
httpd.socket = ssl.wrap_socket(
httpd.socket,
certfile=cert_file,
keyfile=cert_key_file,
server_side=True,
)
thread = threading.Thread(target=httpd.serve_forever)
thread.start()
# Start servers
print(f"Serving at:")
for address in addresses:
print(f" http://{address}:{http_port}")
serve_on_thread(address, http_port, False)
if has_certificate:
for address in addresses:
print(f" https://{address}:{https_port}")
serve_on_thread(address, https_port, True)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,13 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
selenium = "*"
argparse = "*"
[dev-packages]
[requires]
python_version = "3.9"

View File

@ -0,0 +1,10 @@
# qtwasmtestrunner
This is a utility that launches a small webserver and\
either a browser or a webdriver (only chrome/chromedriver at the time of writing)\
This allows running wasm tests and printing the output to stdout like a normal test.
chromedriver must be installed: https://chromedriver.chromium.org/ \
to use it with chromedriver (default operation), and it must be in PATH\
unless --chromedriver_path is passed with full path to chromedriver
Run the script with --help for more info.

View File

@ -0,0 +1,331 @@
#!/usr/bin/env python3
# Copyright (C) 2021 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import argparse
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait
from selenium import webdriver
from pathlib import Path
import typing
import http.server
import subprocess
import threading
import psutil
import re
import os
from signal import SIGINT
import sys
class StdoutOutputSink(object):
def __init__(self):
pass
def write(self, data: str):
print(data)
def __enter__(self):
pass
def __exit__(self, _, __, ___):
pass
class FileOutputSink(object):
def __init__(self, filename: str):
self.__filename = filename
self.__file = None
def write(self, data: str):
self.__file.write(data)
def __enter__(self):
self.__file = open(self.__filename, 'w')
def __exit__(self, _, __, ___):
self.__file.close()
class OutputMulticast(object):
def __init__(self, destinations: typing.List[str]):
self.__sinks: typing.List[typing.Union[StdoutOutputSink, FileOutputSink]] = [
]
self.__destinations = [
'stdout'] if destinations is None else destinations
number_of_stdout_sinks = sum(
[1 if destination == 'stdout' else 0 for destination in self.__destinations])
if number_of_stdout_sinks > 1:
raise Exception('Maximum allowed number of stdout sinks is 1')
def write(self, data: str):
for sink in self.__sinks:
sink.write(data)
def _makeSink(self, destination: str):
return StdoutOutputSink() if 'stdout' == destination else FileOutputSink(destination)
def __enter__(self):
for destination in self.__destinations:
sink = self._makeSink(destination)
sink.__enter__()
self.__sinks.append(sink)
return self
def __exit__(self, _, __, ___):
for sink in reversed(self.__sinks):
sink.__exit__(_, __, ___)
class WasmTestRunner:
def __init__(self, args: dict):
self.server_process = None
self.browser_process = None
self.python_path = Path(sys.executable)
self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
self.host = 'localhost'
self.webserver = None
self.webthread = None
paths = ['html_path', 'browser_path', 'chromedriver_path', 'tmp_dir']
for key, value in args.items():
if value is None:
continue
if key in paths:
value = Path(value)
value.resolve()
setattr(self, key, value)
if not self.html_path.exists():
raise FileNotFoundError(self.html_path)
self.webroot = self.html_path.parent
if hasattr(self, 'browser_path') and not self.browser_path.exists():
raise FileNotFoundError(self.browser_path)
def run(self):
self.run_threaded_webserver()
with OutputMulticast(
self.output if hasattr(self, 'output') else ['stdout']) as output_multicast:
try:
if self.use_browser:
return self.run_wasm_browser()
else:
return self.run_wasm_webdriver(output_multicast)
finally:
self.cleanup()
def run_webserver(self):
webroot = self.html_path.parent.resolve()
self.server_process =\
subprocess.Popen([
str(self.python_path),
'-m', 'http.server',
'--directory', str(webroot),
self.port
])
def run_threaded_webserver(self):
self.webserver = http.server.ThreadingHTTPServer(
(self.host, int(self.port)), self.get_http_handler_class())
self.webthread = threading.Thread(target=self.webserver.serve_forever)
self.webthread.start()
def shutdown_threaded_webserver(self):
if self.webserver is not None:
self.webserver.shutdown()
if self.webthread is not None:
self.webthread.join()
def run_wasm_webdriver(self, output_multicast: OutputMulticast):
url = f'http://localhost:{self.port}/{self.html_path.name}'
if (self.batched_test is not None):
url = f'{url}?qtestname={self.batched_test}&qtestoutputformat={self.format}'
d = DesiredCapabilities.CHROME
d['goog:loggingPrefs'] = {'browser': 'ALL'}
ser = Service(executable_path=self.chromedriver_path)
driver = webdriver.Chrome(desired_capabilities=d, service=ser)
driver.get(url)
driver.execute_script(
""" const status = qtTestRunner.status;
const onFinished = status => {
if (status === 'Completed' || status === 'Error')
document.title = 'qtFinished';
};
onFinished(status);
qtTestRunner.onStatusChanged.addEventListener(onFinished);
""")
WebDriverWait(driver, self.timeout).until(
expected_conditions.title_is('qtFinished'))
runner_status = driver.execute_script(f"return qtTestRunner.status")
if runner_status == 'Error':
output_multicast.write(driver.execute_script(
"return qtTestRunner.errorDetails"))
return -1
else:
assert runner_status == 'Completed'
output_multicast.write(driver.execute_script(
f"return qtTestRunner.results.get('{self.batched_test}').textOutput"))
return driver.execute_script(
f"return qtTestRunner.results.get('{self.batched_test}').exitCode")
def run_wasm_browser(self):
if not hasattr(self, 'browser_path'):
print('Error: browser path must be set to run with browser')
return
if not hasattr(self, 'tmp_dir'):
print('Error: tmp_dir must be set to run with browser')
return
self.create_tmp_dir()
self.browser_process =\
subprocess.Popen([
str(self.browser_path),
'--user-data-dir=' + str(self.tmp_dir),
'--enable-logging=stderr',
f'http://localhost:{self.port}/{self.html_path.name}'
],
stderr=subprocess.PIPE
)
# Only capture the console content
regex = re.compile(r'[^"]*CONSOLE[^"]*"(.*)"[.\w]*')
for line in self.browser_process.stderr:
str_line = line.decode('utf-8')
match = regex.match(str_line)
# Error condition, this should have matched
if 'CONSOLE' in str_line and match is None:
print('Error: did not match console line:')
print(str_line)
if match is not None:
console_line = match.group(1)
print(console_line)
if 'Finished testing' in str_line:
self.browser_process.kill()
break
@staticmethod
def get_loader_variable(driver, varname: str):
return driver.execute_script('return qtLoader.' + varname)
def create_tmp_dir(self):
if not self.tmp_dir.exists():
self.tmp_dir.mkdir()
if not self.tmp_dir.is_dir():
raise NotADirectoryError(self.tmp_dir)
# Needed to bypass the "Welcome to Chrome" prompt
first_run = Path(self.tmp_dir, 'First Run')
first_run.touch()
def get_http_handler_class(self):
wtr = self
class OriginIsolationHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, request, client_address, server):
super().__init__(request, client_address, server, directory=wtr.webroot)
# Headers required to enable SharedArrayBuffer
# See https://web.dev/cross-origin-isolation-guide/
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header(
"Cross-Origin-Embedder-Policy", "require-corp")
self.send_header(
"Cross-Origin-Resource-Policy", "cross-origin")
http.server.SimpleHTTPRequestHandler.end_headers(self)
# We usually don't care that much about what the webserver is logging
def log_message(self, format_, *args):
return
return OriginIsolationHTTPRequestHandler
def cleanup(self):
if self.browser_process is not None:
self.browser_process.kill()
if self.server_process is not None:
self.server_process.kill()
self.shutdown_threaded_webserver()
class BackendProcess:
def __init__(self) -> None:
self.__process = subprocess.Popen(
[sys.executable, *sys.argv, '--backend'], shell=False, stdout=subprocess.PIPE)
def abort(self):
current_process = psutil.Process(self.__process.pid)
children = current_process.children(recursive=True)
for child in [*children, current_process]:
os.kill(child.pid, SIGINT)
def communicate(self, timeout):
return self.__process.communicate(timeout)[0].decode('utf-8')
def returncode(self):
return self.__process.returncode
def main():
parser = argparse.ArgumentParser(description='WASM testrunner')
parser.add_argument('html_path', help='Path to the HTML file to request')
parser.add_argument(
'--batched_test', help='Specifies a batched test to run')
parser.add_argument('--timeout', help='Test timeout',
type=int, default=120)
parser.add_argument(
'--port', help='Port to run the webserver on', default='8000')
parser.add_argument('--use_browser', action='store_true')
parser.add_argument('--browser_path', help='Path to the browser to use')
parser.add_argument('--chromedriver_path', help='Absolute path to chromedriver',
default='chromedriver')
parser.add_argument('--tmp_dir', help='Path to the tmpdir to use when using a browser',
default='/tmp/wasm-testrunner')
parser.add_argument(
'-o', help='filename. Filename may be "stdout" to write to stdout.',
action='append', dest='output')
parser.add_argument(
'--format', help='Output format', choices=['txt', 'xml', 'lightxml', 'junitxml', 'tap'],
default='txt')
parser.add_argument(
'--backend', help='Run as a backend process. There are two types of test runner processes - '
'the main monitoring process and the backend processes launched by it. The tests are '
'run on the backend to avoid any undesired behavior, like deadlocks in browser main process, '
'spilling over across test cases.',
action='store_true')
args = parser.parse_args()
if not args.backend:
backend_process = BackendProcess()
try:
stdout = backend_process.communicate(args.timeout)
print(stdout)
return backend_process.returncode()
except Exception as e:
print(f"Exception while executing test {e}")
backend_process.abort()
return -1
return WasmTestRunner(vars(args)).run()
if __name__ == '__main__':
sys.exit(main())