Provide visual output in page in WASM test runner
There will now be a visual output in page if the qvisualoutput query parameter is supplied. This simplifies debugging. The main html resource has been renamed test_batch.html to reflect the name of the actual test unit, not functionality. Change-Id: Ib6cd4712de9c47cfcc5f670e7b34f998858f99b7 Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
This commit is contained in:
parent
2a47bb221d
commit
b9887d51c3
@ -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()
|
||||
|
@ -177,6 +177,10 @@ if(QT_FEATURE_batch_test_support AND WASM)
|
||||
${QtBase_SOURCE_DIR}/util/wasm/batchedtestrunner/qwasmjsruntime.js)
|
||||
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)
|
||||
|
||||
|
@ -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>
|
||||
|
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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user