diff options
Diffstat (limited to 'util/wasm/batchedtestrunner/qtestoutputreporter.js')
-rw-r--r-- | util/wasm/batchedtestrunner/qtestoutputreporter.js | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/util/wasm/batchedtestrunner/qtestoutputreporter.js b/util/wasm/batchedtestrunner/qtestoutputreporter.js new file mode 100644 index 0000000000..7af288b8f0 --- /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 WITH Qt-GPL-exception-1.0 + +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)); + } +} |