summaryrefslogtreecommitdiffstats
path: root/util/wasm/batchedtestrunner/emrunadapter.js
blob: cd793a38f29759ecb1ebc9b5fa40760534ff2eba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

import { RunnerStatus, TestStatus } from './batchedtestrunner.js';

// Sends messages to the running emrun instance via POST requests.
export class EmrunCommunication {
    #indexOfMessage = 0;
    #postOutputPromises = [];

    #post(body) {
        return fetch('stdio.html', {
            method: 'POST',
            body
        });
    }

    // Returns a promise whose resolution signals that all outstanding traffic to the emrun instance
    // has been completed.
    waitUntilAllSent() {
        return Promise.all(this.#postOutputPromises);
    }

    // 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) {
        const newPromise = this.#post(`^out^${this.#indexOfMessage++}^${output}`);
        this.#postOutputPromises.push(newPromise);
        newPromise.finally(() => {
            this.#postOutputPromises.splice(this.#postOutputPromises.indexOf(newPromise), 1);
        });
        return newPromise;
    }
}

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