summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/corelib/Qt6WasmMacros.cmake4
-rw-r--r--src/testlib/CMakeLists.txt4
-rw-r--r--util/wasm/batchedtestrunner/batchedtestrunner.html1
-rw-r--r--util/wasm/batchedtestrunner/qtestoutputreporter.css89
-rw-r--r--util/wasm/batchedtestrunner/qtestoutputreporter.js366
-rw-r--r--util/wasm/batchedtestrunner/qwasmtestmain.js12
6 files changed, 475 insertions, 1 deletions
diff --git a/src/corelib/Qt6WasmMacros.cmake b/src/corelib/Qt6WasmMacros.cmake
index d86c7202db..c248331e61 100644
--- a/src/corelib/Qt6WasmMacros.cmake
+++ b/src/corelib/Qt6WasmMacros.cmake
@@ -29,6 +29,8 @@ function(_qt_internal_wasm_add_target_helpers target)
if(is_test)
configure_file("${WASM_BUILD_DIR}/libexec/batchedtestrunner.html"
"${target_output_directory}/batchedtestrunner.html" COPYONLY)
+ configure_file("${WASM_BUILD_DIR}/libexec/qtestoutputreporter.css"
+ "${target_output_directory}/qtestoutputreporter.css" COPYONLY)
configure_file("${WASM_BUILD_DIR}/libexec/batchedtestrunner.js"
"${target_output_directory}/batchedtestrunner.js" COPYONLY)
configure_file("${WASM_BUILD_DIR}/libexec/emrunadapter.js"
@@ -37,6 +39,8 @@ function(_qt_internal_wasm_add_target_helpers target)
"${target_output_directory}/qwasmjsruntime.js" COPYONLY)
configure_file("${WASM_BUILD_DIR}/libexec/qwasmtestmain.js"
"${target_output_directory}/qwasmtestmain.js" COPYONLY)
+ configure_file("${WASM_BUILD_DIR}/libexec/qtestoutputreporter.js"
+ "${target_output_directory}/qtestoutputreporter.js" COPYONLY)
configure_file("${WASM_BUILD_DIR}/libexec/util.js"
"${target_output_directory}/util.js" COPYONLY)
else()
diff --git a/src/testlib/CMakeLists.txt b/src/testlib/CMakeLists.txt
index 4b6e2921e7..37f0188006 100644
--- a/src/testlib/CMakeLists.txt
+++ b/src/testlib/CMakeLists.txt
@@ -178,6 +178,10 @@ if(QT_FEATURE_batch_test_support AND WASM)
list(APPEND wasm_support_libexec_files
${QtBase_SOURCE_DIR}/util/wasm/batchedtestrunner/qwasmtestmain.js)
list(APPEND wasm_support_libexec_files
+ ${QtBase_SOURCE_DIR}/util/wasm/batchedtestrunner/qtestoutputreporter.js)
+ list(APPEND wasm_support_libexec_files
+ ${QtBase_SOURCE_DIR}/util/wasm/batchedtestrunner/qtestoutputreporter.css)
+ list(APPEND wasm_support_libexec_files
${QtBase_SOURCE_DIR}/util/wasm/batchedtestrunner/util.js)
qt_path_join(libexec_dir ${QT_INSTALL_DIR} ${INSTALL_LIBEXECDIR})
diff --git a/util/wasm/batchedtestrunner/batchedtestrunner.html b/util/wasm/batchedtestrunner/batchedtestrunner.html
index 14a9fa1807..0b85e48691 100644
--- a/util/wasm/batchedtestrunner/batchedtestrunner.html
+++ b/util/wasm/batchedtestrunner/batchedtestrunner.html
@@ -8,6 +8,7 @@ SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
<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>
diff --git a/util/wasm/batchedtestrunner/qtestoutputreporter.css b/util/wasm/batchedtestrunner/qtestoutputreporter.css
new file mode 100644
index 0000000000..3cf312b11a
--- /dev/null
+++ b/util/wasm/batchedtestrunner/qtestoutputreporter.css
@@ -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);
+}
diff --git a/util/wasm/batchedtestrunner/qtestoutputreporter.js b/util/wasm/batchedtestrunner/qtestoutputreporter.js
new file mode 100644
index 0000000000..ad8a373540
--- /dev/null
+++ b/util/wasm/batchedtestrunner/qtestoutputreporter.js
@@ -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));
+ }
+}
diff --git a/util/wasm/batchedtestrunner/qwasmtestmain.js b/util/wasm/batchedtestrunner/qwasmtestmain.js
index e91ff6799d..644455907f 100644
--- a/util/wasm/batchedtestrunner/qwasmtestmain.js
+++ b/util/wasm/batchedtestrunner/qwasmtestmain.js
@@ -9,6 +9,7 @@ import {
ResourceLocator,
} from './qwasmjsruntime.js';
import { parseQuery } from './util.js';
+import { VisualOutputProducer, UI, ScannerFactory } from './qtestoutputreporter.js'
(() => {
const setPageTitle = (useEmrun, testName, isBatch) => {
@@ -23,6 +24,7 @@ import { parseQuery } from './util.js';
}
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;
@@ -49,10 +51,18 @@ import { parseQuery } from './util.js';
if (useEmrun) {
const adapter = new EmrunAdapter(new EmrunCommunication(), testRunner, () => {
- window.close();
+ 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);