as-test 1.0.16 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +45 -4
- package/as-test.config.schema.json +5 -0
- package/assembly/__fuzz__/math.fuzz.ts +19 -0
- package/assembly/__fuzz__/string.fuzz.ts +31 -0
- package/assembly/index.ts +5 -5
- package/assembly/src/expectation.ts +93 -42
- package/assembly/util/format.ts +104 -0
- package/assembly/util/helpers.ts +7 -13
- package/assembly/util/json.ts +2 -2
- package/assembly/util/wipc.ts +15 -5
- package/bin/commands/clean-core.js +135 -0
- package/bin/commands/clean.js +51 -0
- package/bin/commands/init-core.js +33 -225
- package/bin/commands/run-core.js +433 -289
- package/bin/commands/web-runner-source.js +14 -700
- package/bin/commands/web-session.js +1144 -0
- package/bin/index.js +391 -78
- package/bin/types.js +1 -0
- package/bin/util.js +16 -1
- package/bin/wipc.js +7 -2
- package/lib/build/index.d.ts +1 -0
- package/lib/build/index.js +1116 -0
- package/lib/build/web-runner/client.d.ts +1 -0
- package/lib/build/web-runner/client.js +167 -0
- package/lib/build/web-runner/html.d.ts +1 -0
- package/lib/build/web-runner/html.js +201 -0
- package/lib/build/web-runner/worker.d.ts +1 -0
- package/lib/build/web-runner/worker.js +271 -0
- package/lib/src/index.ts +1266 -0
- package/package.json +14 -6
- package/transform/lib/mock.js +50 -27
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildWebRunnerClientSource(): string;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export function buildWebRunnerClientSource() {
|
|
2
|
+
return String.raw `const runnerOrigin = location.origin;
|
|
3
|
+
const workerUrl = new URL("/worker.js", runnerOrigin);
|
|
4
|
+
const wsUrl = new URL("/ws", runnerOrigin);
|
|
5
|
+
wsUrl.protocol = location.protocol == "https:" ? "wss:" : "ws:";
|
|
6
|
+
|
|
7
|
+
const status = document.getElementById("status");
|
|
8
|
+
const statusDot = document.getElementById("status-dot");
|
|
9
|
+
const summary = document.getElementById("summary");
|
|
10
|
+
const footerDetail = document.getElementById("footer-detail");
|
|
11
|
+
const output = document.getElementById("output");
|
|
12
|
+
const replyBuffer = new SharedArrayBuffer(8 + 4 * 1024 * 1024);
|
|
13
|
+
const replyState = new Int32Array(replyBuffer, 0, 2);
|
|
14
|
+
const replyBytes = new Uint8Array(replyBuffer, 8);
|
|
15
|
+
const worker = new Worker(workerUrl, { type: "module" });
|
|
16
|
+
const ws = new WebSocket(wsUrl);
|
|
17
|
+
ws.binaryType = "arraybuffer";
|
|
18
|
+
|
|
19
|
+
appendLine("launching browser terminal session", "dim prompt");
|
|
20
|
+
appendLine("waiting for worker bootstrap", "dim");
|
|
21
|
+
|
|
22
|
+
function setStatus(message, tone = "warn") {
|
|
23
|
+
status.textContent = message;
|
|
24
|
+
const tones = {
|
|
25
|
+
warn: "var(--warn)",
|
|
26
|
+
error: "var(--error)",
|
|
27
|
+
ok: "var(--success)",
|
|
28
|
+
accent: "var(--accent)",
|
|
29
|
+
};
|
|
30
|
+
statusDot.style.color = tones[tone] || tones.warn;
|
|
31
|
+
statusDot.style.background = tones[tone] || tones.warn;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setSummary(message) {
|
|
35
|
+
summary.textContent = message;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setFooter(message) {
|
|
39
|
+
footerDetail.textContent = message;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function appendLine(message, className = "") {
|
|
43
|
+
const line = document.createElement("span");
|
|
44
|
+
line.className = "line " + className;
|
|
45
|
+
line.textContent = String(message ?? "");
|
|
46
|
+
output.appendChild(line);
|
|
47
|
+
output.appendChild(document.createTextNode("\n"));
|
|
48
|
+
output.scrollTop = output.scrollHeight;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pushReply(frame) {
|
|
52
|
+
if (frame.byteLength > replyBytes.byteLength) {
|
|
53
|
+
throw new Error("WIPC reply exceeded shared browser buffer");
|
|
54
|
+
}
|
|
55
|
+
if (Atomics.load(replyState, 0) != 0) {
|
|
56
|
+
throw new Error("received concurrent WIPC replies in web runner");
|
|
57
|
+
}
|
|
58
|
+
replyBytes.set(new Uint8Array(frame), 0);
|
|
59
|
+
Atomics.store(replyState, 1, frame.byteLength);
|
|
60
|
+
Atomics.store(replyState, 0, 1);
|
|
61
|
+
Atomics.notify(replyState, 0, 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
worker.onmessage = (event) => {
|
|
65
|
+
const message = event.data ?? {};
|
|
66
|
+
if (message.kind == "wipc" && message.frame instanceof ArrayBuffer) {
|
|
67
|
+
ws.send(message.frame);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (message.kind == "terminal") {
|
|
71
|
+
appendLine(String(message.text ?? ""), String(message.level ?? ""));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (message.kind == "instantiated") {
|
|
75
|
+
setStatus("ready", "accent");
|
|
76
|
+
setSummary("worker instantiated the wasm module");
|
|
77
|
+
setFooter("Waiting for start signal from the host runner.");
|
|
78
|
+
appendLine("runtime instantiated", "success prompt");
|
|
79
|
+
ws.send(JSON.stringify({ kind: "instantiated" }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (message.kind == "done") {
|
|
83
|
+
setStatus("finished", "ok");
|
|
84
|
+
setSummary("test execution completed");
|
|
85
|
+
setFooter("Browser runtime finished cleanly.");
|
|
86
|
+
appendLine("session complete", "success prompt");
|
|
87
|
+
ws.send(JSON.stringify({ kind: "done" }));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (message.kind == "error") {
|
|
91
|
+
setStatus("failed", "error");
|
|
92
|
+
setSummary("browser runtime failed");
|
|
93
|
+
setFooter("See terminal output for the failure reason.");
|
|
94
|
+
appendLine(String(message.message ?? "unknown browser runtime error"), "error");
|
|
95
|
+
ws.send(
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
kind: "error",
|
|
98
|
+
message: String(message.message ?? "unknown browser runtime error"),
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (message.kind == "status") {
|
|
104
|
+
setStatus(String(message.text ?? "running"), String(message.level ?? "accent"));
|
|
105
|
+
if (message.summary) setSummary(String(message.summary));
|
|
106
|
+
if (message.footer) setFooter(String(message.footer));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
ws.addEventListener("open", () => {
|
|
112
|
+
setStatus("connected", "accent");
|
|
113
|
+
setSummary("transport online");
|
|
114
|
+
setFooter("Spawning browser worker.");
|
|
115
|
+
appendLine("websocket tunnel established", "accent prompt");
|
|
116
|
+
ws.send(JSON.stringify({ kind: "ready" }));
|
|
117
|
+
worker.postMessage({
|
|
118
|
+
kind: "init",
|
|
119
|
+
env: window.__AS_TEST_ENV__,
|
|
120
|
+
replyBuffer,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
ws.addEventListener("message", (event) => {
|
|
125
|
+
if (typeof event.data == "string") {
|
|
126
|
+
let message = null;
|
|
127
|
+
try {
|
|
128
|
+
message = JSON.parse(event.data);
|
|
129
|
+
} catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (message?.kind == "start") {
|
|
133
|
+
setStatus("running", "accent");
|
|
134
|
+
setSummary("host runner started execution");
|
|
135
|
+
setFooter("Streaming runtime frames to the host runner.");
|
|
136
|
+
appendLine("received start signal", "accent prompt");
|
|
137
|
+
worker.postMessage({ kind: "start" });
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
pushReply(event.data);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const message = String(error instanceof Error ? error.message : error);
|
|
145
|
+
setStatus("bridge failure", "error");
|
|
146
|
+
setSummary("shared reply buffer failure");
|
|
147
|
+
setFooter("The browser could not deliver runtime data.");
|
|
148
|
+
appendLine(message, "error");
|
|
149
|
+
ws.send(JSON.stringify({ kind: "error", message }));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
ws.addEventListener("close", () => {
|
|
154
|
+
setStatus("disconnected", "error");
|
|
155
|
+
setSummary("runner disconnected");
|
|
156
|
+
setFooter("The browser transport closed.");
|
|
157
|
+
appendLine("runner disconnected", "error prompt");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
ws.addEventListener("error", () => {
|
|
161
|
+
setStatus("connection error", "error");
|
|
162
|
+
setSummary("websocket connection failed");
|
|
163
|
+
setFooter("Unable to connect to the local web runner.");
|
|
164
|
+
appendLine("websocket connection failed", "error prompt");
|
|
165
|
+
});
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildWebRunnerHtml(): string;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export function buildWebRunnerHtml() {
|
|
2
|
+
return String.raw `<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>as-test web runner</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--bg: #030712;
|
|
12
|
+
--bg2: #07101d;
|
|
13
|
+
--line: rgba(125, 211, 252, 0.14);
|
|
14
|
+
--muted: #7f92b0;
|
|
15
|
+
--text: #dde7f7;
|
|
16
|
+
--accent: #7dd3fc;
|
|
17
|
+
--accent2: #5eead4;
|
|
18
|
+
--warn: #fbbf24;
|
|
19
|
+
--error: #f87171;
|
|
20
|
+
--success: #86efac;
|
|
21
|
+
}
|
|
22
|
+
* {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
html,
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
}
|
|
30
|
+
body {
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
background:
|
|
33
|
+
radial-gradient(circle at top left, rgba(125, 211, 252, 0.12), transparent 34%),
|
|
34
|
+
radial-gradient(circle at top right, rgba(94, 234, 212, 0.08), transparent 28%),
|
|
35
|
+
linear-gradient(180deg, var(--bg), var(--bg2));
|
|
36
|
+
color: var(--text);
|
|
37
|
+
font: 14px/1.6 "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
38
|
+
}
|
|
39
|
+
.shell {
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-rows: auto 1fr auto;
|
|
43
|
+
}
|
|
44
|
+
.chrome {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
gap: 16px;
|
|
49
|
+
padding: 18px 22px 14px;
|
|
50
|
+
border-bottom: 1px solid var(--line);
|
|
51
|
+
background: linear-gradient(180deg, rgba(8, 14, 28, 0.96), rgba(8, 14, 28, 0.78));
|
|
52
|
+
backdrop-filter: blur(18px);
|
|
53
|
+
}
|
|
54
|
+
.brand {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 14px;
|
|
58
|
+
}
|
|
59
|
+
.lights {
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 8px;
|
|
62
|
+
}
|
|
63
|
+
.light {
|
|
64
|
+
width: 11px;
|
|
65
|
+
height: 11px;
|
|
66
|
+
border-radius: 999px;
|
|
67
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
|
|
68
|
+
}
|
|
69
|
+
.light.red { background: #fb7185; }
|
|
70
|
+
.light.yellow { background: #fbbf24; }
|
|
71
|
+
.light.green { background: #4ade80; }
|
|
72
|
+
.title {
|
|
73
|
+
display: grid;
|
|
74
|
+
gap: 2px;
|
|
75
|
+
}
|
|
76
|
+
.title strong {
|
|
77
|
+
font-size: 13px;
|
|
78
|
+
letter-spacing: 0.08em;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
}
|
|
81
|
+
.title span,
|
|
82
|
+
.footer {
|
|
83
|
+
color: var(--muted);
|
|
84
|
+
}
|
|
85
|
+
.statusbar {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 10px;
|
|
89
|
+
}
|
|
90
|
+
.status-dot {
|
|
91
|
+
width: 10px;
|
|
92
|
+
height: 10px;
|
|
93
|
+
border-radius: 999px;
|
|
94
|
+
background: var(--warn);
|
|
95
|
+
box-shadow: 0 0 18px currentColor;
|
|
96
|
+
}
|
|
97
|
+
.status-text {
|
|
98
|
+
font-size: 12px;
|
|
99
|
+
letter-spacing: 0.04em;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
}
|
|
102
|
+
.terminal {
|
|
103
|
+
position: relative;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
background:
|
|
106
|
+
linear-gradient(180deg, rgba(7, 12, 24, 0.92), rgba(3, 8, 18, 0.98)),
|
|
107
|
+
linear-gradient(90deg, rgba(125, 211, 252, 0.03), rgba(94, 234, 212, 0.03));
|
|
108
|
+
}
|
|
109
|
+
.terminal::before {
|
|
110
|
+
content: "";
|
|
111
|
+
position: absolute;
|
|
112
|
+
inset: 0;
|
|
113
|
+
background:
|
|
114
|
+
linear-gradient(rgba(125, 211, 252, 0.04), transparent 2px),
|
|
115
|
+
linear-gradient(90deg, rgba(125, 211, 252, 0.03), transparent 1px);
|
|
116
|
+
background-size: 100% 4px, 24px 100%;
|
|
117
|
+
opacity: 0.22;
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
}
|
|
120
|
+
#output {
|
|
121
|
+
position: absolute;
|
|
122
|
+
inset: 0;
|
|
123
|
+
margin: 0;
|
|
124
|
+
padding: 22px 24px 28px;
|
|
125
|
+
overflow: auto;
|
|
126
|
+
white-space: pre-wrap;
|
|
127
|
+
word-break: break-word;
|
|
128
|
+
line-height: 1.65;
|
|
129
|
+
tab-size: 2;
|
|
130
|
+
}
|
|
131
|
+
.line {
|
|
132
|
+
display: block;
|
|
133
|
+
}
|
|
134
|
+
.line.dim { color: var(--muted); }
|
|
135
|
+
.line.warn { color: var(--warn); }
|
|
136
|
+
.line.error { color: var(--error); }
|
|
137
|
+
.line.success { color: var(--success); }
|
|
138
|
+
.line.accent { color: var(--accent); }
|
|
139
|
+
.prompt::before {
|
|
140
|
+
content: "› ";
|
|
141
|
+
color: var(--accent2);
|
|
142
|
+
}
|
|
143
|
+
.footer {
|
|
144
|
+
display: flex;
|
|
145
|
+
justify-content: space-between;
|
|
146
|
+
gap: 12px;
|
|
147
|
+
padding: 10px 22px 14px;
|
|
148
|
+
border-top: 1px solid var(--line);
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
background: rgba(7, 12, 24, 0.86);
|
|
151
|
+
}
|
|
152
|
+
.footer strong {
|
|
153
|
+
color: var(--text);
|
|
154
|
+
}
|
|
155
|
+
@media (max-width: 720px) {
|
|
156
|
+
.chrome {
|
|
157
|
+
padding: 14px 16px 12px;
|
|
158
|
+
align-items: flex-start;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
}
|
|
161
|
+
#output {
|
|
162
|
+
padding: 16px 16px 22px;
|
|
163
|
+
}
|
|
164
|
+
.footer {
|
|
165
|
+
padding: 10px 16px 14px;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
170
|
+
</head>
|
|
171
|
+
<body>
|
|
172
|
+
<div class="shell">
|
|
173
|
+
<header class="chrome">
|
|
174
|
+
<div class="brand">
|
|
175
|
+
<div class="lights" aria-hidden="true">
|
|
176
|
+
<span class="light red"></span>
|
|
177
|
+
<span class="light yellow"></span>
|
|
178
|
+
<span class="light green"></span>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="title">
|
|
181
|
+
<strong>as-test web runner</strong>
|
|
182
|
+
<span id="summary">waiting for browser runtime bootstrap</span>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="statusbar">
|
|
186
|
+
<span id="status-dot" class="status-dot"></span>
|
|
187
|
+
<span id="status" class="status-text">connecting</span>
|
|
188
|
+
</div>
|
|
189
|
+
</header>
|
|
190
|
+
<main class="terminal">
|
|
191
|
+
<pre id="output" aria-live="polite"></pre>
|
|
192
|
+
</main>
|
|
193
|
+
<footer class="footer">
|
|
194
|
+
<span>Mode: <strong>web</strong></span>
|
|
195
|
+
<span id="footer-detail">Waiting for transport handshake.</span>
|
|
196
|
+
</footer>
|
|
197
|
+
</div>
|
|
198
|
+
<script type="module" src="/client.js"></script>
|
|
199
|
+
</body>
|
|
200
|
+
</html>`;
|
|
201
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildWebRunnerWorkerSource(): string;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
export function buildWebRunnerWorkerSource() {
|
|
2
|
+
return String.raw `let replyState = null;
|
|
3
|
+
let replyBytes = null;
|
|
4
|
+
const WIPC_MAGIC = [0x57, 0x49, 0x50, 0x43];
|
|
5
|
+
let runtimeEnv = {};
|
|
6
|
+
let instance = null;
|
|
7
|
+
|
|
8
|
+
self.onmessage = async (event) => {
|
|
9
|
+
const message = event.data ?? {};
|
|
10
|
+
if (message.kind == "init") {
|
|
11
|
+
const shared = message.replyBuffer;
|
|
12
|
+
replyState = new Int32Array(shared, 0, 2);
|
|
13
|
+
replyBytes = new Uint8Array(shared, 8);
|
|
14
|
+
try {
|
|
15
|
+
self.postMessage({
|
|
16
|
+
kind: "status",
|
|
17
|
+
level: "accent",
|
|
18
|
+
text: "initializing",
|
|
19
|
+
summary: "worker resolving runtime imports",
|
|
20
|
+
footer: "Loading wasm assets in the browser worker.",
|
|
21
|
+
});
|
|
22
|
+
runtimeEnv = message.env ?? {};
|
|
23
|
+
applyRuntimeEnvironment(runtimeEnv);
|
|
24
|
+
instance = await instantiate({});
|
|
25
|
+
self.postMessage({ kind: "instantiated" });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
emitError(error);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (message.kind == "start") {
|
|
32
|
+
try {
|
|
33
|
+
if (!instance) {
|
|
34
|
+
throw new Error("web runtime has not been instantiated yet");
|
|
35
|
+
}
|
|
36
|
+
instance.exports.start?.();
|
|
37
|
+
self.postMessage({ kind: "done" });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
emitError(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function emitError(error) {
|
|
45
|
+
const message =
|
|
46
|
+
error && typeof error == "object" && "stack" in error
|
|
47
|
+
? String(error.stack)
|
|
48
|
+
: String(error);
|
|
49
|
+
self.postMessage({ kind: "error", message });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readReply(max) {
|
|
53
|
+
if (!replyState || !replyBytes || max <= 0) {
|
|
54
|
+
return new ArrayBuffer(0);
|
|
55
|
+
}
|
|
56
|
+
while (Atomics.load(replyState, 0) == 0) {
|
|
57
|
+
Atomics.wait(replyState, 0, 0);
|
|
58
|
+
}
|
|
59
|
+
const total = Atomics.load(replyState, 1);
|
|
60
|
+
const size = Math.min(max, total);
|
|
61
|
+
const out = new Uint8Array(size);
|
|
62
|
+
out.set(replyBytes.subarray(0, size));
|
|
63
|
+
if (size < total) {
|
|
64
|
+
replyBytes.copyWithin(0, size, total);
|
|
65
|
+
Atomics.store(replyState, 1, total - size);
|
|
66
|
+
} else {
|
|
67
|
+
Atomics.store(replyState, 1, 0);
|
|
68
|
+
Atomics.store(replyState, 0, 0);
|
|
69
|
+
Atomics.notify(replyState, 0, 1);
|
|
70
|
+
}
|
|
71
|
+
return out.buffer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function applyRuntimeEnvironment(env) {
|
|
75
|
+
self.process = {
|
|
76
|
+
env,
|
|
77
|
+
stdout: {
|
|
78
|
+
write(data) {
|
|
79
|
+
const frame = data instanceof ArrayBuffer ? data : data?.buffer;
|
|
80
|
+
if (frame) {
|
|
81
|
+
mirrorFrame(frame);
|
|
82
|
+
self.postMessage({ kind: "wipc", frame }, [frame]);
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
stdin: {
|
|
88
|
+
read(size) {
|
|
89
|
+
return readReply(Number(size ?? 0));
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function instantiate(imports) {
|
|
96
|
+
const wasmUrl = String(runtimeEnv.AS_TEST_WASM_PATH ?? "");
|
|
97
|
+
const helperUrl = String(runtimeEnv.AS_TEST_HELPER_PATH ?? "");
|
|
98
|
+
const kind = String(runtimeEnv.AS_TEST_BINDINGS_KIND ?? "raw");
|
|
99
|
+
if (!wasmUrl) {
|
|
100
|
+
throw new Error("web runtime wasm path is missing");
|
|
101
|
+
}
|
|
102
|
+
if (kind === "raw") {
|
|
103
|
+
if (!helperUrl) {
|
|
104
|
+
throw new Error("web runtime helper path is missing for raw bindings");
|
|
105
|
+
}
|
|
106
|
+
const binary = await fetchWasmBinary(wasmUrl);
|
|
107
|
+
const module = new WebAssembly.Module(binary);
|
|
108
|
+
const helper = await import(helperUrl);
|
|
109
|
+
if (typeof helper.instantiate != "function") {
|
|
110
|
+
throw new Error("bindings helper missing instantiate export");
|
|
111
|
+
}
|
|
112
|
+
const instance = await captureInstantiateInstance(async () => {
|
|
113
|
+
await helper.instantiate(module, imports);
|
|
114
|
+
});
|
|
115
|
+
return decorateInstance(instance);
|
|
116
|
+
}
|
|
117
|
+
if (kind === "esm") {
|
|
118
|
+
if (!helperUrl) {
|
|
119
|
+
throw new Error("web runtime helper path is missing for esm bindings");
|
|
120
|
+
}
|
|
121
|
+
const instance = await captureInstantiateInstance(async () => {
|
|
122
|
+
await import(helperUrl);
|
|
123
|
+
});
|
|
124
|
+
return decorateInstance(instance);
|
|
125
|
+
}
|
|
126
|
+
const binary = await fetchWasmBinary(wasmUrl);
|
|
127
|
+
const module = new WebAssembly.Module(binary);
|
|
128
|
+
const result = await WebAssembly.instantiate(module, imports);
|
|
129
|
+
const instance = result instanceof WebAssembly.Instance ? result : result.instance;
|
|
130
|
+
return decorateInstance(instance);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function fetchWasmBinary(wasmUrl) {
|
|
134
|
+
const response = await fetch(wasmUrl);
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
throw new Error("failed to fetch wasm artifact: " + response.status);
|
|
137
|
+
}
|
|
138
|
+
return response.arrayBuffer();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function captureInstantiateInstance(run) {
|
|
142
|
+
const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
|
|
143
|
+
let captured = null;
|
|
144
|
+
WebAssembly.instantiate = async (source, importObject) => {
|
|
145
|
+
const result = await originalInstantiate(source, importObject);
|
|
146
|
+
if (result instanceof WebAssembly.Instance) {
|
|
147
|
+
captured = result;
|
|
148
|
+
} else {
|
|
149
|
+
captured = result.instance;
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
await run();
|
|
155
|
+
} finally {
|
|
156
|
+
WebAssembly.instantiate = originalInstantiate;
|
|
157
|
+
}
|
|
158
|
+
if (!captured) {
|
|
159
|
+
throw new Error("failed to capture WebAssembly.Instance in web worker");
|
|
160
|
+
}
|
|
161
|
+
return captured;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function decorateInstance(instance) {
|
|
165
|
+
const exports = instance.exports ?? {};
|
|
166
|
+
if (typeof exports.start == "function") {
|
|
167
|
+
return instance;
|
|
168
|
+
}
|
|
169
|
+
const startFn = exports._start;
|
|
170
|
+
if (typeof startFn != "function") {
|
|
171
|
+
return instance;
|
|
172
|
+
}
|
|
173
|
+
const exportsProxy = new Proxy(exports, {
|
|
174
|
+
get(target, prop, receiver) {
|
|
175
|
+
if (prop == "start") {
|
|
176
|
+
return () => startFn.call(target);
|
|
177
|
+
}
|
|
178
|
+
return Reflect.get(target, prop, receiver);
|
|
179
|
+
},
|
|
180
|
+
has(target, prop) {
|
|
181
|
+
if (prop == "start") return true;
|
|
182
|
+
return Reflect.has(target, prop);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return new Proxy(instance, {
|
|
186
|
+
get(target, prop, receiver) {
|
|
187
|
+
if (prop == "exports") return exportsProxy;
|
|
188
|
+
return Reflect.get(target, prop, receiver);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function mirrorFrame(frame) {
|
|
194
|
+
if (!(frame instanceof ArrayBuffer)) return;
|
|
195
|
+
const bytes = new Uint8Array(frame);
|
|
196
|
+
if (
|
|
197
|
+
bytes.length < 9 ||
|
|
198
|
+
bytes[0] !== WIPC_MAGIC[0] ||
|
|
199
|
+
bytes[1] !== WIPC_MAGIC[1] ||
|
|
200
|
+
bytes[2] !== WIPC_MAGIC[2] ||
|
|
201
|
+
bytes[3] !== WIPC_MAGIC[3]
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const type = bytes[4];
|
|
206
|
+
const size =
|
|
207
|
+
bytes[5] |
|
|
208
|
+
(bytes[6] << 8) |
|
|
209
|
+
(bytes[7] << 16) |
|
|
210
|
+
(bytes[8] << 24);
|
|
211
|
+
const payload = bytes.subarray(9, 9 + size);
|
|
212
|
+
if (type !== 0x02) return;
|
|
213
|
+
let raw = "";
|
|
214
|
+
try {
|
|
215
|
+
raw = new TextDecoder().decode(payload);
|
|
216
|
+
} catch {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!raw.length) return;
|
|
220
|
+
let message = null;
|
|
221
|
+
try {
|
|
222
|
+
message = JSON.parse(raw);
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
renderControl(message);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderControl(message) {
|
|
230
|
+
if (!message || typeof message !== "object") return;
|
|
231
|
+
const kind = String(message.kind ?? "");
|
|
232
|
+
if (kind === "event:file-start") {
|
|
233
|
+
self.postMessage({ kind: "terminal", level: "accent prompt", text: "running " + String(message.file ?? "spec") });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (kind === "event:file-end") {
|
|
237
|
+
const verdict = String(message.verdict ?? "done").toUpperCase();
|
|
238
|
+
const time = String(message.time ?? "");
|
|
239
|
+
self.postMessage({
|
|
240
|
+
kind: "terminal",
|
|
241
|
+
level: verdict === "PASS" ? "success" : "error",
|
|
242
|
+
text: verdict + " " + String(message.file ?? "") + (time ? " " + time : ""),
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (kind === "event:suite-start") {
|
|
247
|
+
const depth = Number(message.depth ?? 0);
|
|
248
|
+
const indent = " ".repeat(Math.max(0, depth));
|
|
249
|
+
self.postMessage({ kind: "terminal", level: "dim", text: indent + String(message.description ?? "") });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (kind === "event:log") {
|
|
253
|
+
self.postMessage({ kind: "terminal", level: "", text: String(message.text ?? "") });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (kind === "event:warn") {
|
|
257
|
+
self.postMessage({ kind: "terminal", level: "warn", text: String(message.message ?? "warning") });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (kind === "event:assert-fail") {
|
|
261
|
+
const parts = [
|
|
262
|
+
"assertion failed",
|
|
263
|
+
String(message.message ?? ""),
|
|
264
|
+
"left: " + String(message.left ?? ""),
|
|
265
|
+
"right: " + String(message.right ?? ""),
|
|
266
|
+
].filter(Boolean);
|
|
267
|
+
self.postMessage({ kind: "terminal", level: "error", text: parts.join(" | ") });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
}
|