diff options
Diffstat (limited to 'util/wasm/batchedtestrunner/emrunadapter.js')
-rw-r--r-- | util/wasm/batchedtestrunner/emrunadapter.js | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/util/wasm/batchedtestrunner/emrunadapter.js b/util/wasm/batchedtestrunner/emrunadapter.js new file mode 100644 index 0000000000..5b4284e18f --- /dev/null +++ b/util/wasm/batchedtestrunner/emrunadapter.js @@ -0,0 +1,137 @@ +// 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'; + +// Sends messages to the running emrun instance via POST requests. +export class EmrunCommunication { + static #BATCHING_DELAY = 300; + + #indexOfMessage = 0; + #postOutputPromise; + // Accumulate output in a batch that gets sent with a delay so that the emrun http server + // does not get pounded with requests. + #nextOutputBatch = null; + + #post(body) { + return fetch('stdio.html', { + method: 'POST', + body + }); + } + + // Waits for the output sending to finish, if any output transfer is still in progress. + async waitUntilAllSent() + { + if (this.#postOutputPromise) + await this.#postOutputPromise; + } + + // Posts the exit status to the running emrun instance. Emrun will drop connection unless it is + // run with --serve_after_exit, therefore this method will throw most of the times. + postExit(status) { + return this.#post(`^exit^${status}`); + } + + // Posts an indexed output chunk to the running emrun instance. Each consecutive call to this + // method increments the output index by 1. + postOutput(output) + { + if (this.#nextOutputBatch) { + this.#nextOutputBatch += output; + } else { + this.#nextOutputBatch = output; + this.#postOutputPromise = new Promise(resolve => + { + window.setTimeout(() => + { + const toSend = this.#nextOutputBatch; + this.#nextOutputBatch = null; + this.#post(`^out^${this.#indexOfMessage++}^${toSend}$`) + .finally(resolve); + }, EmrunCommunication.#BATCHING_DELAY); + }); + } + + return this.#postOutputPromise; + } +} + +// Wraps a test module runner; forwards its output and resolution state to the running emrun +// instance. +export class EmrunAdapter { + #communication; + #batchedTestRunner; + #sentLines = 0; + #onExitSent; + + constructor(communication, batchedTestRunner, onExitSent) { + this.#communication = communication; + this.#batchedTestRunner = batchedTestRunner; + this.#onExitSent = onExitSent; + } + + // Starts listening to test module runner's state changes. When the test module runner finishes + // or reports output, sends suitable messages to the emrun instance. + 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); + } + + #toExitCode(status) { + switch (status) { + case RunnerStatus.Error: + return -1; + case RunnerStatus.Passed: + return 0; + case RunnerStatus.Running: + throw new Error('No exit code when still running'); + case RunnerStatus.TestCrashed: + return -2; + case RunnerStatus.TestsFailed: + return this.#batchedTestRunner.numberOfFailed; + } + } + + async #onRunnerStatusChanged(status) { + if (RunnerStatus.Running === status) + return; + + const exit = this.#toExitCode(status); + if (RunnerStatus.Error === status) + this.#communication.postOutput(this.#batchedTestRunner.errorDetails); + + await this.#communication.waitUntilAllSent(); + try { + await this.#communication.postExit(exit); + } catch { + // no-op: The remote end will drop connection on exit. + } finally { + this.#onExitSent?.(); + } + } + + async #onTestOutputChanged(_, output) { + const notSent = output.slice(this.#sentLines); + for (const out of notSent) + this.#communication.postOutput(out); + this.#sentLines = output.length; + } + + async #onTestStatusChanged(_, status) { + if (status === TestStatus.Running) + this.#sentLines = 0; + } +} |