mirror of
https://github.com/crystalidea/qt6windows7.git
synced 2025-07-03 15:55:27 +08:00
qt 6.5.1 original
This commit is contained in:
60
util/wasm/batchedtestrunner/README.md
Normal file
60
util/wasm/batchedtestrunner/README.md
Normal 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.
|
15
util/wasm/batchedtestrunner/batchedtestrunner.html
Normal file
15
util/wasm/batchedtestrunner/batchedtestrunner.html
Normal 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>
|
177
util/wasm/batchedtestrunner/batchedtestrunner.js
Normal file
177
util/wasm/batchedtestrunner/batchedtestrunner.js
Normal 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);
|
||||
}
|
||||
}
|
119
util/wasm/batchedtestrunner/emrunadapter.js
Normal file
119
util/wasm/batchedtestrunner/emrunadapter.js
Normal 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;
|
||||
}
|
||||
}
|
89
util/wasm/batchedtestrunner/qtestoutputreporter.css
Normal file
89
util/wasm/batchedtestrunner/qtestoutputreporter.css
Normal 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);
|
||||
}
|
366
util/wasm/batchedtestrunner/qtestoutputreporter.js
Normal file
366
util/wasm/batchedtestrunner/qtestoutputreporter.js
Normal 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));
|
||||
}
|
||||
}
|
229
util/wasm/batchedtestrunner/qwasmjsruntime.js
Normal file
229
util/wasm/batchedtestrunner/qwasmjsruntime.js
Normal 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);
|
||||
}
|
||||
}
|
69
util/wasm/batchedtestrunner/qwasmtestmain.js
Normal file
69
util/wasm/batchedtestrunner/qwasmtestmain.js
Normal 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);
|
||||
})();
|
31
util/wasm/batchedtestrunner/util.js
Normal file
31
util/wasm/batchedtestrunner/util.js
Normal 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));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user