as-test 1.4.1 → 1.5.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 +64 -0
- package/assembly/src/expectation.ts +2 -2
- package/assembly/src/stringify.ts +29 -4
- package/bin/commands/build-core.js +62 -6
- package/bin/commands/fuzz-core.js +22 -1
- package/bin/commands/init-core.js +371 -36
- package/bin/commands/run-core.js +130 -21
- package/bin/commands/web-session.js +50 -3
- package/bin/index.js +6 -2
- package/bin/wipc.js +46 -7
- package/lib/build/index.js +45 -15
- package/lib/build/web-runner/worker.js +27 -0
- package/lib/src/index.ts +66 -15
- package/package.json +18 -17
- package/transform/lib/equals.js +3 -3
- package/transform/lib/index.js +10 -1
- package/transform/lib/mock.js +63 -20
package/bin/commands/run-core.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { glob } from "glob";
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
4
5
|
import { Channel, MessageType } from "../wipc.js";
|
|
5
6
|
import {
|
|
6
7
|
applyMode,
|
|
@@ -686,10 +687,60 @@ export async function run(
|
|
|
686
687
|
const loadedConfig = loadConfig(resolvedConfigPath);
|
|
687
688
|
const mode = applyMode(loadedConfig, options.modeName);
|
|
688
689
|
const config = mode.config;
|
|
690
|
+
// Mode-level exclusion: when a mode override adds `!`-prefixed negations
|
|
691
|
+
// beyond the top-level config, honor them even when the orchestrator passes
|
|
692
|
+
// a single-file selector (`resolveInputPatterns` drops the configured input
|
|
693
|
+
// in that case). Top-level negations are intentionally not enforced here so
|
|
694
|
+
// explicit selectors can still override them (e.g. picking a __tmp_* spec).
|
|
695
|
+
if (options.modeName && selectors.length > 0) {
|
|
696
|
+
const topInput = Array.isArray(loadedConfig.input)
|
|
697
|
+
? loadedConfig.input
|
|
698
|
+
: [loadedConfig.input];
|
|
699
|
+
const modeInput = Array.isArray(config.input)
|
|
700
|
+
? config.input
|
|
701
|
+
: [config.input];
|
|
702
|
+
const topNegations = new Set(topInput.filter((p) => p.startsWith("!")));
|
|
703
|
+
const modeOnlyNegations = modeInput
|
|
704
|
+
.filter((p) => p.startsWith("!") && !topNegations.has(p))
|
|
705
|
+
.map((p) => p.slice(1));
|
|
706
|
+
if (modeOnlyNegations.length > 0) {
|
|
707
|
+
const cwd = process.cwd();
|
|
708
|
+
const allExcluded = selectors.every((sel) => {
|
|
709
|
+
const abs = path.isAbsolute(sel) ? sel : path.resolve(cwd, sel);
|
|
710
|
+
const rel = path.relative(cwd, abs).split(path.sep).join("/");
|
|
711
|
+
return modeOnlyNegations.some((pat) =>
|
|
712
|
+
minimatch(rel, pat, { dot: true, matchBase: true }),
|
|
713
|
+
);
|
|
714
|
+
});
|
|
715
|
+
if (allExcluded) {
|
|
716
|
+
return {
|
|
717
|
+
failed: false,
|
|
718
|
+
stats: collectRunStats([]),
|
|
719
|
+
buildTime: 0,
|
|
720
|
+
snapshotSummary: { matched: 0, created: 0, updated: 0, failed: 0 },
|
|
721
|
+
coverageSummary: {
|
|
722
|
+
enabled: false,
|
|
723
|
+
showPoints: false,
|
|
724
|
+
total: 0,
|
|
725
|
+
covered: 0,
|
|
726
|
+
uncovered: 0,
|
|
727
|
+
percent: 100,
|
|
728
|
+
files: [],
|
|
729
|
+
},
|
|
730
|
+
reports: [],
|
|
731
|
+
logSummary: { count: 0, file: null, groups: [], text: "" },
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
689
736
|
const inputPatterns = resolveInputPatterns(config.input, selectors);
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
737
|
+
const includePatterns = inputPatterns.filter((p) => !p.startsWith("!"));
|
|
738
|
+
const ignorePatterns = inputPatterns
|
|
739
|
+
.filter((p) => p.startsWith("!"))
|
|
740
|
+
.map((p) => p.slice(1));
|
|
741
|
+
const inputFiles = (
|
|
742
|
+
await glob(includePatterns, { ignore: ignorePatterns })
|
|
743
|
+
).sort((a, b) => a.localeCompare(b));
|
|
693
744
|
const snapshotEnabled = flags.snapshot !== false;
|
|
694
745
|
const createSnapshots = Boolean(flags.createSnapshots);
|
|
695
746
|
const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
|
|
@@ -798,7 +849,20 @@ export async function run(
|
|
|
798
849
|
token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken),
|
|
799
850
|
),
|
|
800
851
|
};
|
|
801
|
-
|
|
852
|
+
// A managed web-session run never spawns the standalone runner, so its
|
|
853
|
+
// repro must re-run the managed CLI command (which renders the same panel
|
|
854
|
+
// UI) rather than `default.web.js <wasm>`, a different web stack.
|
|
855
|
+
const runCommandForLog = webSession
|
|
856
|
+
? formatInvocation({
|
|
857
|
+
command: execPath,
|
|
858
|
+
args: [
|
|
859
|
+
process.argv[1] ?? "",
|
|
860
|
+
"run",
|
|
861
|
+
resolveSpecRelativePath(file, config.input),
|
|
862
|
+
...(options.modeName ? ["--mode", options.modeName] : []),
|
|
863
|
+
],
|
|
864
|
+
})
|
|
865
|
+
: formatInvocation(invocation);
|
|
802
866
|
const snapshotStore = new SnapshotStore(
|
|
803
867
|
file,
|
|
804
868
|
config.snapshotDir,
|
|
@@ -1991,7 +2055,7 @@ async function runProcess(
|
|
|
1991
2055
|
if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
|
|
1992
2056
|
reportStream.chunkFramesReceived++;
|
|
1993
2057
|
reportStream.chunkBytesReceived += data.length;
|
|
1994
|
-
reportStream.chunks.push(
|
|
2058
|
+
reportStream.chunks.push(Buffer.from(data));
|
|
1995
2059
|
return;
|
|
1996
2060
|
}
|
|
1997
2061
|
try {
|
|
@@ -2041,7 +2105,9 @@ async function runProcess(
|
|
|
2041
2105
|
parseError =
|
|
2042
2106
|
parseError ?? "missing report:end marker for chunked report payload";
|
|
2043
2107
|
} else {
|
|
2044
|
-
const chunkedPayload = reportStream.chunks.
|
|
2108
|
+
const chunkedPayload = Buffer.concat(reportStream.chunks).toString(
|
|
2109
|
+
"utf8",
|
|
2110
|
+
);
|
|
2045
2111
|
try {
|
|
2046
2112
|
report = JSON.parse(chunkedPayload);
|
|
2047
2113
|
parseError = null;
|
|
@@ -2099,14 +2165,19 @@ async function runProcess(
|
|
|
2099
2165
|
runtimeEvents,
|
|
2100
2166
|
);
|
|
2101
2167
|
if (synthesized) {
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2168
|
+
const exitedEarly = !runtimeEvents.sawFileEnd;
|
|
2169
|
+
if (
|
|
2170
|
+
exitedEarly ||
|
|
2171
|
+
code !== 0 ||
|
|
2172
|
+
hasMeaningfulRuntimeOutput(stderrBuffer)
|
|
2173
|
+
) {
|
|
2107
2174
|
const errorParts = [];
|
|
2108
2175
|
if (code !== 0) {
|
|
2109
2176
|
errorParts.push(`child process exited with code ${code}`);
|
|
2177
|
+
} else if (exitedEarly) {
|
|
2178
|
+
errorParts.push(
|
|
2179
|
+
"test runtime exited before reporting file completion",
|
|
2180
|
+
);
|
|
2110
2181
|
}
|
|
2111
2182
|
const stderrText = normalizeRuntimeOutput(stderrBuffer);
|
|
2112
2183
|
if (stderrText.length) {
|
|
@@ -2135,12 +2206,18 @@ async function runProcess(
|
|
|
2135
2206
|
modeName,
|
|
2136
2207
|
code !== 0
|
|
2137
2208
|
? `test runtime failed with exit code ${code}`
|
|
2138
|
-
:
|
|
2209
|
+
: exitedEarly
|
|
2210
|
+
? "test runtime exited before completing the test file"
|
|
2211
|
+
: "test runtime wrote to stderr",
|
|
2139
2212
|
errorText,
|
|
2140
2213
|
stdoutBuffer,
|
|
2141
2214
|
stderrBuffer,
|
|
2142
2215
|
);
|
|
2143
2216
|
}
|
|
2217
|
+
reporter.onWarning?.({
|
|
2218
|
+
message:
|
|
2219
|
+
"runtime report payload missing; reconstructed result from streamed lifecycle events",
|
|
2220
|
+
});
|
|
2144
2221
|
return synthesized;
|
|
2145
2222
|
}
|
|
2146
2223
|
const errorText = "missing report payload from test runtime";
|
|
@@ -2375,7 +2452,7 @@ async function runWebSessionProcess(
|
|
|
2375
2452
|
if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
|
|
2376
2453
|
reportStream.chunkFramesReceived++;
|
|
2377
2454
|
reportStream.chunkBytesReceived += data.length;
|
|
2378
|
-
reportStream.chunks.push(
|
|
2455
|
+
reportStream.chunks.push(Buffer.from(data));
|
|
2379
2456
|
return;
|
|
2380
2457
|
}
|
|
2381
2458
|
try {
|
|
@@ -2422,7 +2499,9 @@ async function runWebSessionProcess(
|
|
|
2422
2499
|
parseError =
|
|
2423
2500
|
parseError ?? "missing report:end marker for chunked report payload";
|
|
2424
2501
|
} else {
|
|
2425
|
-
const chunkedPayload = reportStream.chunks.
|
|
2502
|
+
const chunkedPayload = Buffer.concat(reportStream.chunks).toString(
|
|
2503
|
+
"utf8",
|
|
2504
|
+
);
|
|
2426
2505
|
try {
|
|
2427
2506
|
report = JSON.parse(chunkedPayload);
|
|
2428
2507
|
parseError = null;
|
|
@@ -2480,14 +2559,19 @@ async function runWebSessionProcess(
|
|
|
2480
2559
|
runtimeEvents,
|
|
2481
2560
|
);
|
|
2482
2561
|
if (synthesized) {
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2562
|
+
const exitedEarly = !runtimeEvents.sawFileEnd;
|
|
2563
|
+
if (
|
|
2564
|
+
exitedEarly ||
|
|
2565
|
+
code !== 0 ||
|
|
2566
|
+
hasMeaningfulRuntimeOutput(stderrBuffer)
|
|
2567
|
+
) {
|
|
2488
2568
|
const errorParts = [];
|
|
2489
2569
|
if (code !== 0) {
|
|
2490
2570
|
errorParts.push(`child process exited with code ${code}`);
|
|
2571
|
+
} else if (exitedEarly) {
|
|
2572
|
+
errorParts.push(
|
|
2573
|
+
"test runtime exited before reporting file completion",
|
|
2574
|
+
);
|
|
2491
2575
|
}
|
|
2492
2576
|
const stderrText = normalizeRuntimeOutput(stderrBuffer);
|
|
2493
2577
|
if (stderrText.length) {
|
|
@@ -2499,10 +2583,35 @@ async function runWebSessionProcess(
|
|
|
2499
2583
|
reportStream,
|
|
2500
2584
|
runtimeEvents,
|
|
2501
2585
|
);
|
|
2502
|
-
|
|
2503
|
-
|
|
2586
|
+
errorParts.push(diagnostics);
|
|
2587
|
+
const errorText = errorParts.join("\n\n");
|
|
2588
|
+
persistCrashRecord(crashDir, {
|
|
2589
|
+
kind: "test",
|
|
2590
|
+
file: specFile,
|
|
2591
|
+
entryKey: crashEntryKey,
|
|
2592
|
+
mode: modeName ?? "default",
|
|
2593
|
+
error: errorText || "runtime reported an unknown error",
|
|
2594
|
+
stdout: stdoutBuffer,
|
|
2595
|
+
stderr: stderrBuffer,
|
|
2504
2596
|
});
|
|
2597
|
+
return appendRuntimeFailureReport(
|
|
2598
|
+
synthesized,
|
|
2599
|
+
specFile,
|
|
2600
|
+
modeName,
|
|
2601
|
+
code !== 0
|
|
2602
|
+
? `test runtime failed with exit code ${code}`
|
|
2603
|
+
: exitedEarly
|
|
2604
|
+
? "test runtime exited before completing the test file"
|
|
2605
|
+
: "test runtime wrote to stderr",
|
|
2606
|
+
errorText,
|
|
2607
|
+
stdoutBuffer,
|
|
2608
|
+
stderrBuffer,
|
|
2609
|
+
);
|
|
2505
2610
|
}
|
|
2611
|
+
reporter.onWarning?.({
|
|
2612
|
+
message:
|
|
2613
|
+
"runtime report payload missing; reconstructed result from streamed lifecycle events",
|
|
2614
|
+
});
|
|
2506
2615
|
return synthesized;
|
|
2507
2616
|
}
|
|
2508
2617
|
const diagnostics = buildRuntimeReportDiagnostics(
|
|
@@ -16,6 +16,8 @@ export class PersistentWebSessionHost {
|
|
|
16
16
|
this.serverSockets = new Set();
|
|
17
17
|
this.wsSocket = null;
|
|
18
18
|
this.wsBuffer = Buffer.alloc(0);
|
|
19
|
+
this.wsFragmentOpcode = 0;
|
|
20
|
+
this.wsFragments = [];
|
|
19
21
|
this.ready = false;
|
|
20
22
|
this.closed = false;
|
|
21
23
|
this.currentJob = null;
|
|
@@ -334,14 +336,32 @@ export class PersistentWebSessionHost {
|
|
|
334
336
|
payload = Buffer.from(payload);
|
|
335
337
|
}
|
|
336
338
|
this.wsBuffer = this.wsBuffer.subarray(offset + maskLength + length);
|
|
337
|
-
|
|
339
|
+
// Reassemble fragmented messages: a non-final data frame (0x1/0x2 with
|
|
340
|
+
// FIN=0) starts a sequence continued by 0x0 frames until FIN=1. Chromium
|
|
341
|
+
// fragments large (64KB) frames, so without this the wipc stream desyncs
|
|
342
|
+
// and the report payload is corrupted.
|
|
343
|
+
const fin = (first & 0x80) !== 0;
|
|
344
|
+
let effectiveOpcode = opcode;
|
|
345
|
+
if (opcode == 0x0) {
|
|
346
|
+
this.wsFragments.push(payload);
|
|
347
|
+
if (!fin) continue;
|
|
348
|
+
payload = Buffer.concat(this.wsFragments);
|
|
349
|
+
this.wsFragments = [];
|
|
350
|
+
effectiveOpcode = this.wsFragmentOpcode;
|
|
351
|
+
this.wsFragmentOpcode = 0;
|
|
352
|
+
} else if ((opcode == 0x1 || opcode == 0x2) && !fin) {
|
|
353
|
+
this.wsFragmentOpcode = opcode;
|
|
354
|
+
this.wsFragments = [payload];
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (effectiveOpcode == 0x8) {
|
|
338
358
|
return;
|
|
339
359
|
}
|
|
340
|
-
if (
|
|
360
|
+
if (effectiveOpcode == 0x1) {
|
|
341
361
|
this.onControl(payload.toString("utf8"));
|
|
342
362
|
continue;
|
|
343
363
|
}
|
|
344
|
-
if (
|
|
364
|
+
if (effectiveOpcode == 0x2 && this.currentJob) {
|
|
345
365
|
this.currentJob.onBinary(Buffer.from(payload));
|
|
346
366
|
}
|
|
347
367
|
}
|
|
@@ -1012,6 +1032,10 @@ async function instantiate(imports) {
|
|
|
1012
1032
|
if (typeof helper.instantiate != "function") {
|
|
1013
1033
|
throw new Error("bindings helper missing instantiate export");
|
|
1014
1034
|
}
|
|
1035
|
+
// Stub before the helper runs: it reads imports.<module> while building its
|
|
1036
|
+
// import object, so a missing import must be filled here, not just in the
|
|
1037
|
+
// WebAssembly.instantiate wrapper (which runs too late for raw bindings).
|
|
1038
|
+
stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
|
|
1015
1039
|
const instance = await captureInstantiateInstance(async () => {
|
|
1016
1040
|
await helper.instantiate(module, imports);
|
|
1017
1041
|
});
|
|
@@ -1026,11 +1050,31 @@ async function instantiate(imports) {
|
|
|
1026
1050
|
}
|
|
1027
1051
|
const binary = await fetchWasmBinary(wasmUrl);
|
|
1028
1052
|
const module = new WebAssembly.Module(binary);
|
|
1053
|
+
stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
|
|
1029
1054
|
const result = await WebAssembly.instantiate(module, imports);
|
|
1030
1055
|
const wasmInstance = result instanceof WebAssembly.Instance ? result : result.instance;
|
|
1031
1056
|
return decorateInstance(wasmInstance);
|
|
1032
1057
|
}
|
|
1033
1058
|
|
|
1059
|
+
// Fills function imports the module declares but nothing provided with a no-op
|
|
1060
|
+
// stub, so instantiation never fails on a missing import (e.g. an unmocked
|
|
1061
|
+
// mockImport target with no host implementation). Mirrors the node runner.
|
|
1062
|
+
function stubMissingImports(module, importObject, skipModules) {
|
|
1063
|
+
const skip = new Set(skipModules || []);
|
|
1064
|
+
for (const entry of WebAssembly.Module.imports(module)) {
|
|
1065
|
+
if (entry.kind !== "function" || skip.has(entry.module)) continue;
|
|
1066
|
+
let mod = importObject[entry.module];
|
|
1067
|
+
if (!mod || typeof mod !== "object") {
|
|
1068
|
+
mod = {};
|
|
1069
|
+
importObject[entry.module] = mod;
|
|
1070
|
+
}
|
|
1071
|
+
if (!(entry.name in mod)) {
|
|
1072
|
+
mod[entry.name] = () => 0;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return importObject;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1034
1078
|
async function fetchWasmBinary(wasmUrl) {
|
|
1035
1079
|
const response = await fetch(wasmUrl);
|
|
1036
1080
|
if (!response.ok) throw new Error("failed to fetch wasm artifact: " + response.status);
|
|
@@ -1041,6 +1085,9 @@ async function captureInstantiateInstance(run) {
|
|
|
1041
1085
|
const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
|
|
1042
1086
|
let captured = null;
|
|
1043
1087
|
WebAssembly.instantiate = async (source, importObject) => {
|
|
1088
|
+
if (source instanceof WebAssembly.Module && importObject) {
|
|
1089
|
+
stubMissingImports(source, importObject, ["wasi_snapshot_preview1"]);
|
|
1090
|
+
}
|
|
1044
1091
|
const result = await originalInstantiate(source, importObject);
|
|
1045
1092
|
captured = result instanceof WebAssembly.Instance ? result : result.instance;
|
|
1046
1093
|
return result;
|
package/bin/index.js
CHANGED
|
@@ -2344,7 +2344,10 @@ async function runWatchLoop(
|
|
|
2344
2344
|
if (byte === 0x03) {
|
|
2345
2345
|
if (rawModeEnabled) stdin.setRawMode(false);
|
|
2346
2346
|
closeAllWatchers();
|
|
2347
|
-
|
|
2347
|
+
// Exit non-zero if the last run left anything failing, or if a run
|
|
2348
|
+
// is still in flight (an interrupted run counts as a failure), so
|
|
2349
|
+
// quitting a red watch session still fails CI / shell pipelines.
|
|
2350
|
+
process.exit(isRunning || failingSpecs.size ? 1 : 0);
|
|
2348
2351
|
}
|
|
2349
2352
|
if (isRunning) break;
|
|
2350
2353
|
if (byte === 0x77 || byte === 0x57) {
|
|
@@ -2394,7 +2397,8 @@ async function runWatchLoop(
|
|
|
2394
2397
|
process.on("SIGINT", () => {
|
|
2395
2398
|
if (rawModeEnabled) stdin.setRawMode(false);
|
|
2396
2399
|
closeAllWatchers();
|
|
2397
|
-
|
|
2400
|
+
// Mirror the raw-mode ctrl+c path: a failing or in-flight run exits 1.
|
|
2401
|
+
process.exit(isRunning || failingSpecs.size ? 1 : 0);
|
|
2398
2402
|
});
|
|
2399
2403
|
// Keep the process alive
|
|
2400
2404
|
await new Promise(() => {});
|
package/bin/wipc.js
CHANGED
|
@@ -45,14 +45,52 @@ export class Channel {
|
|
|
45
45
|
if (this.buffer.length < Channel.HEADER_SIZE) return;
|
|
46
46
|
const type = this.buffer.readUInt8(4);
|
|
47
47
|
const length = this.buffer.readUInt32LE(5);
|
|
48
|
+
// The magic can occur by chance inside ordinary stdout output (e.g. a
|
|
49
|
+
// test printing binary data or the literal string "WIPC"). A genuine
|
|
50
|
+
// frame always carries a known type and a bounded length; if either
|
|
51
|
+
// check fails the match is coincidental, so surface the magic bytes as
|
|
52
|
+
// passthrough and resume scanning past them rather than crash or stall.
|
|
53
|
+
if (!Channel.isKnownType(type) || length > Channel.MAX_FRAME_SIZE) {
|
|
54
|
+
this.resyncPastMagic();
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
48
57
|
const frameSize = Channel.HEADER_SIZE + length;
|
|
49
58
|
if (this.buffer.length < frameSize) return;
|
|
50
59
|
const payload = this.buffer.subarray(Channel.HEADER_SIZE, frameSize);
|
|
60
|
+
if (type === MessageType.CALL) {
|
|
61
|
+
// CALL payloads are always JSON. If the bytes do not parse the magic
|
|
62
|
+
// was coincidental — do NOT consume the frame; treat the magic as
|
|
63
|
+
// output and resync so the rest flushes as passthrough.
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(payload.toString("utf8"));
|
|
67
|
+
} catch {
|
|
68
|
+
this.resyncPastMagic();
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
this.buffer = this.buffer.subarray(frameSize);
|
|
72
|
+
this.onCall(parsed);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
51
75
|
this.buffer = this.buffer.subarray(frameSize);
|
|
52
|
-
this.
|
|
76
|
+
this.dispatchFrame(type, payload);
|
|
53
77
|
}
|
|
54
78
|
}
|
|
55
|
-
|
|
79
|
+
static isKnownType(type) {
|
|
80
|
+
return (
|
|
81
|
+
type === MessageType.OPEN ||
|
|
82
|
+
type === MessageType.CLOSE ||
|
|
83
|
+
type === MessageType.CALL ||
|
|
84
|
+
type === MessageType.DATA
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
// A coincidental magic match: emit the magic bytes as ordinary output and
|
|
88
|
+
// advance past them so scanning can continue from the next byte.
|
|
89
|
+
resyncPastMagic() {
|
|
90
|
+
this.onPassthrough(this.buffer.subarray(0, Channel.MAGIC.length));
|
|
91
|
+
this.buffer = this.buffer.subarray(Channel.MAGIC.length);
|
|
92
|
+
}
|
|
93
|
+
dispatchFrame(type, payload) {
|
|
56
94
|
switch (type) {
|
|
57
95
|
case MessageType.OPEN:
|
|
58
96
|
this.onOpen();
|
|
@@ -60,14 +98,9 @@ export class Channel {
|
|
|
60
98
|
case MessageType.CLOSE:
|
|
61
99
|
this.onClose();
|
|
62
100
|
break;
|
|
63
|
-
case MessageType.CALL:
|
|
64
|
-
this.onCall(JSON.parse(payload.toString("utf8")));
|
|
65
|
-
break;
|
|
66
101
|
case MessageType.DATA:
|
|
67
102
|
this.onDataMessage(payload);
|
|
68
103
|
break;
|
|
69
|
-
default:
|
|
70
|
-
throw new Error(`Unknown frame type: ${type}`);
|
|
71
104
|
}
|
|
72
105
|
}
|
|
73
106
|
onPassthrough(_data) {}
|
|
@@ -79,3 +112,9 @@ export class Channel {
|
|
|
79
112
|
Channel.MAGIC = Buffer.from("WIPC");
|
|
80
113
|
Channel.HEADER_SIZE = 9;
|
|
81
114
|
Channel.MAGIC_PREFIX_MAX = Channel.MAGIC.length - 1;
|
|
115
|
+
// Upper bound on a single frame's declared payload length. Real frames are
|
|
116
|
+
// small JSON events or report chunks (<= 64 KiB); anything larger means the
|
|
117
|
+
// 4 magic bytes appeared by coincidence inside passthrough output. Without
|
|
118
|
+
// this bound a forged length would make us buffer (and swallow real frames)
|
|
119
|
+
// indefinitely. The margin over 64 KiB future-proofs larger report chunks.
|
|
120
|
+
Channel.MAX_FRAME_SIZE = 16 * 1024 * 1024;
|
package/lib/build/index.js
CHANGED
|
@@ -200,7 +200,11 @@ async function instantiateRawInstance(wasmPath, helperPath, imports) {
|
|
|
200
200
|
if (typeof helper.instantiate != "function") {
|
|
201
201
|
throw new Error("bindings helper missing instantiate export");
|
|
202
202
|
}
|
|
203
|
-
const mergedImports =
|
|
203
|
+
const mergedImports = stubMissingImports(
|
|
204
|
+
module,
|
|
205
|
+
mergeImports(withNodeIo({}), imports),
|
|
206
|
+
["wasi_snapshot_preview1"],
|
|
207
|
+
);
|
|
204
208
|
const instance = await captureHelperInstance(async () => {
|
|
205
209
|
await helper.instantiate(module, mergedImports);
|
|
206
210
|
});
|
|
@@ -216,6 +220,7 @@ async function instantiateEsmInstance(wasmPath, helperPath, imports) {
|
|
|
216
220
|
"esm bindings do not support custom imports in as-test/lib; pass {} or switch to raw bindings",
|
|
217
221
|
);
|
|
218
222
|
}
|
|
223
|
+
patchNodeIo();
|
|
219
224
|
const instance = await captureHelperInstance(async () => {
|
|
220
225
|
await import(`${pathToFileURL(helperPath).href}?t=${Date.now()}`);
|
|
221
226
|
});
|
|
@@ -279,6 +284,8 @@ async function instantiateWebInstance(wasmPath, imports) {
|
|
|
279
284
|
let ready = false;
|
|
280
285
|
let wsSocket = null;
|
|
281
286
|
let wsBuffer = Buffer.alloc(0);
|
|
287
|
+
let wsFragmentOpcode = 0;
|
|
288
|
+
let wsFragments = [];
|
|
282
289
|
let stdinBuffer = Buffer.alloc(0);
|
|
283
290
|
let browserProcess = null;
|
|
284
291
|
let browserStderr = "";
|
|
@@ -420,15 +427,29 @@ async function instantiateWebInstance(wasmPath, imports) {
|
|
|
420
427
|
payload = Buffer.from(payload);
|
|
421
428
|
}
|
|
422
429
|
wsBuffer = wsBuffer.subarray(offset + maskLength + length);
|
|
423
|
-
|
|
430
|
+
const fin = (first & 0x80) !== 0;
|
|
431
|
+
let effectiveOpcode = opcode;
|
|
432
|
+
if (opcode == 0x0) {
|
|
433
|
+
wsFragments.push(payload);
|
|
434
|
+
if (!fin) continue;
|
|
435
|
+
payload = Buffer.concat(wsFragments);
|
|
436
|
+
wsFragments = [];
|
|
437
|
+
effectiveOpcode = wsFragmentOpcode;
|
|
438
|
+
wsFragmentOpcode = 0;
|
|
439
|
+
} else if ((opcode == 0x1 || opcode == 0x2) && !fin) {
|
|
440
|
+
wsFragmentOpcode = opcode;
|
|
441
|
+
wsFragments = [payload];
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (effectiveOpcode == 0x8) {
|
|
424
445
|
finish(0);
|
|
425
446
|
return;
|
|
426
447
|
}
|
|
427
|
-
if (
|
|
448
|
+
if (effectiveOpcode == 0x1) {
|
|
428
449
|
onControl(payload.toString("utf8"));
|
|
429
450
|
continue;
|
|
430
451
|
}
|
|
431
|
-
if (
|
|
452
|
+
if (effectiveOpcode == 0x2) {
|
|
432
453
|
process.stdout.write(payload);
|
|
433
454
|
}
|
|
434
455
|
}
|
|
@@ -626,20 +647,23 @@ function hasUserImports(imports) {
|
|
|
626
647
|
return Object.keys(imports).length > 0;
|
|
627
648
|
}
|
|
628
649
|
function createWasmImports(module, imports) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
650
|
+
return stubMissingImports(module, mergeImports(withNodeIo({}), imports));
|
|
651
|
+
}
|
|
652
|
+
function stubMissingImports(module, imports, skipModules = []) {
|
|
653
|
+
const skip = new Set(skipModules);
|
|
654
|
+
const bag = imports;
|
|
633
655
|
for (const entry of WebAssembly.Module.imports(module)) {
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
656
|
+
if (entry.kind != "function" || skip.has(entry.module)) continue;
|
|
657
|
+
let mod = bag[entry.module];
|
|
658
|
+
if (!mod || typeof mod != "object") {
|
|
659
|
+
mod = {};
|
|
660
|
+
bag[entry.module] = mod;
|
|
661
|
+
}
|
|
662
|
+
if (!(entry.name in mod)) {
|
|
663
|
+
mod[entry.name] = () => 0;
|
|
640
664
|
}
|
|
641
665
|
}
|
|
642
|
-
return
|
|
666
|
+
return imports;
|
|
643
667
|
}
|
|
644
668
|
let patchedWasiWarning = false;
|
|
645
669
|
function suppressExperimentalWasiWarning() {
|
|
@@ -1153,6 +1177,12 @@ async function captureHelperInstance(runHelper) {
|
|
|
1153
1177
|
const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
|
|
1154
1178
|
let instance = null;
|
|
1155
1179
|
WebAssembly.instantiate = async (source, importObject) => {
|
|
1180
|
+
if (source instanceof WebAssembly.Module && importObject) {
|
|
1181
|
+
stubMissingImports(source, importObject, [
|
|
1182
|
+
"env",
|
|
1183
|
+
"wasi_snapshot_preview1",
|
|
1184
|
+
]);
|
|
1185
|
+
}
|
|
1156
1186
|
const result = await originalInstantiate(source, importObject);
|
|
1157
1187
|
if (result instanceof WebAssembly.Instance) {
|
|
1158
1188
|
instance = result;
|
|
@@ -109,6 +109,10 @@ async function instantiate(imports) {
|
|
|
109
109
|
if (typeof helper.instantiate != "function") {
|
|
110
110
|
throw new Error("bindings helper missing instantiate export");
|
|
111
111
|
}
|
|
112
|
+
// Stub before the helper runs: it reads imports.<module> while building its
|
|
113
|
+
// import object, so a missing import must be filled here, not just in the
|
|
114
|
+
// WebAssembly.instantiate wrapper (which runs too late for raw bindings).
|
|
115
|
+
stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
|
|
112
116
|
const instance = await captureInstantiateInstance(async () => {
|
|
113
117
|
await helper.instantiate(module, imports);
|
|
114
118
|
});
|
|
@@ -125,11 +129,31 @@ async function instantiate(imports) {
|
|
|
125
129
|
}
|
|
126
130
|
const binary = await fetchWasmBinary(wasmUrl);
|
|
127
131
|
const module = new WebAssembly.Module(binary);
|
|
132
|
+
stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
|
|
128
133
|
const result = await WebAssembly.instantiate(module, imports);
|
|
129
134
|
const instance = result instanceof WebAssembly.Instance ? result : result.instance;
|
|
130
135
|
return decorateInstance(instance);
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
// Fills function imports the module declares but nothing provided with a no-op
|
|
139
|
+
// stub, so instantiation never fails on a missing import (e.g. an unmocked
|
|
140
|
+
// mockImport target with no host implementation). Mirrors the node runner.
|
|
141
|
+
function stubMissingImports(module, importObject, skipModules) {
|
|
142
|
+
const skip = new Set(skipModules || []);
|
|
143
|
+
for (const entry of WebAssembly.Module.imports(module)) {
|
|
144
|
+
if (entry.kind !== "function" || skip.has(entry.module)) continue;
|
|
145
|
+
let mod = importObject[entry.module];
|
|
146
|
+
if (!mod || typeof mod !== "object") {
|
|
147
|
+
mod = {};
|
|
148
|
+
importObject[entry.module] = mod;
|
|
149
|
+
}
|
|
150
|
+
if (!(entry.name in mod)) {
|
|
151
|
+
mod[entry.name] = () => 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return importObject;
|
|
155
|
+
}
|
|
156
|
+
|
|
133
157
|
async function fetchWasmBinary(wasmUrl) {
|
|
134
158
|
const response = await fetch(wasmUrl);
|
|
135
159
|
if (!response.ok) {
|
|
@@ -142,6 +166,9 @@ async function captureInstantiateInstance(run) {
|
|
|
142
166
|
const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
|
|
143
167
|
let captured = null;
|
|
144
168
|
WebAssembly.instantiate = async (source, importObject) => {
|
|
169
|
+
if (source instanceof WebAssembly.Module && importObject) {
|
|
170
|
+
stubMissingImports(source, importObject, ["wasi_snapshot_preview1"]);
|
|
171
|
+
}
|
|
145
172
|
const result = await originalInstantiate(source, importObject);
|
|
146
173
|
if (result instanceof WebAssembly.Instance) {
|
|
147
174
|
captured = result;
|