diff options
author | Mikolaj Boc <mikolaj.boc@qt.io> | 2022-09-02 19:23:30 +0200 |
---|---|---|
committer | Mikolaj Boc <mikolaj.boc@qt.io> | 2022-10-05 00:36:41 +0200 |
commit | b9887d51c396d126dd1dfc67a18a7de361db205b (patch) | |
tree | 58275cd8bf640a13c7e21780f890f99e033363eb /util | |
parent | 2a47bb221dacefbc606ca24ba8d926009c067fa4 (diff) | |
download | qtbase-b9887d51c396d126dd1dfc67a18a7de361db205b.tar.gz |
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>
Diffstat (limited to 'util')
-rw-r--r-- | util/wasm/batchedtestrunner/batchedtestrunner.html | 1 | ||||
-rw-r--r-- | util/wasm/batchedtestrunner/qtestoutputreporter.css | 89 | ||||
-rw-r--r-- | util/wasm/batchedtestrunner/qtestoutputreporter.js | 366 | ||||
-rw-r--r-- | util/wasm/batchedtestrunner/qwasmtestmain.js | 12 |
4 files changed, 467 insertions, 1 deletions
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); |