qt5base-lts/util/wasm/batchedtestrunner/qtestoutputreporter.js
Mikolaj Boc b9887d51c3 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>
2022-10-05 00:36:41 +02:00

367 lines
11 KiB
JavaScript

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