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
package/bin/commands/run-core.js
CHANGED
|
@@ -6,7 +6,10 @@ import { applyMode, formatTime, getExec, loadConfig, tokenizeCommand, } from "..
|
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { pathToFileURL } from "url";
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
-
import {
|
|
9
|
+
import { PassThrough } from "stream";
|
|
10
|
+
import { buildWebRunnerSource } from "./web-runner-source.js";
|
|
11
|
+
import { PersistentWebSessionHost } from "./web-session.js";
|
|
12
|
+
import { build } from "./build-core.js";
|
|
10
13
|
import { createReporter as createDefaultReporter } from "../reporters/default.js";
|
|
11
14
|
import { createTapReporter } from "../reporters/tap.js";
|
|
12
15
|
import { persistCrashRecord } from "../crash-store.js";
|
|
@@ -597,59 +600,82 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
597
600
|
updated: 0,
|
|
598
601
|
failed: 0,
|
|
599
602
|
};
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
603
|
+
let buildTime = 0;
|
|
604
|
+
const ownedWebSession = options.webSession === undefined &&
|
|
605
|
+
shouldUsePersistentHeadfulWebSession(config.buildOptions.target, runtimeCommand)
|
|
606
|
+
? await PersistentWebSessionHost.start(false)
|
|
607
|
+
: null;
|
|
608
|
+
const webSession = options.webSession ?? ownedWebSession;
|
|
609
|
+
try {
|
|
610
|
+
for (let i = 0; i < inputFiles.length; i++) {
|
|
611
|
+
const file = inputFiles[i];
|
|
612
|
+
const outFile = path.join(config.outDir, resolveArtifactFileName(file, config.buildOptions.target, options.modeName, duplicateSpecBasenames));
|
|
613
|
+
if (!existsSync(outFile)) {
|
|
614
|
+
const buildStartedAt = Date.now();
|
|
615
|
+
await build(resolvedConfigPath, [file], options.modeName, { coverage: flags.coverage }, {}, loadedConfig);
|
|
616
|
+
buildTime += Date.now() - buildStartedAt;
|
|
617
|
+
}
|
|
618
|
+
const fileBase = file
|
|
619
|
+
.slice(file.lastIndexOf("/") + 1)
|
|
620
|
+
.replace(".ts", "")
|
|
621
|
+
.replace(".spec", "");
|
|
622
|
+
const fileToken = outFile;
|
|
623
|
+
const runtimeTargetEnv = resolveRuntimeTargetEnv(config.buildOptions.target, outFile);
|
|
624
|
+
const invocation = {
|
|
625
|
+
command: execPath,
|
|
626
|
+
args: runtimeTokens
|
|
627
|
+
.slice(1)
|
|
628
|
+
.map((token) => token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken)),
|
|
629
|
+
};
|
|
630
|
+
const runCommandForLog = formatInvocation(invocation);
|
|
631
|
+
const snapshotStore = new SnapshotStore(file, config.snapshotDir, duplicateSpecBasenames);
|
|
632
|
+
let report;
|
|
633
|
+
try {
|
|
634
|
+
const runtimeEnv = {
|
|
635
|
+
...mode.env,
|
|
636
|
+
...config.runOptions.env,
|
|
637
|
+
...runtimeTargetEnv,
|
|
638
|
+
...(process.env.BROWSER?.trim()
|
|
639
|
+
? { BROWSER: process.env.BROWSER.trim() }
|
|
640
|
+
: config.runOptions.runtime.browser.trim()
|
|
641
|
+
? { BROWSER: config.runOptions.runtime.browser.trim() }
|
|
642
|
+
: {}),
|
|
643
|
+
};
|
|
644
|
+
report = webSession
|
|
645
|
+
? await runWebSessionProcess(webSession, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv)
|
|
646
|
+
: await runProcess(invocation, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv);
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
const modeLabel = options.modeName ?? "default";
|
|
650
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
651
|
+
throw new Error(`Failed to run ${path.basename(file)} in mode ${modeLabel} with ${details}`);
|
|
652
|
+
}
|
|
653
|
+
const normalized = normalizeReport(report);
|
|
654
|
+
const selectedSuites = options.suiteSelectors?.length
|
|
655
|
+
? filterSelectedSuites(normalized.suites, options.suiteSelectors, file, options.modeName ?? "default")
|
|
656
|
+
: normalized.suites;
|
|
657
|
+
snapshotStore.flush();
|
|
658
|
+
snapshotSummary.matched += snapshotStore.matched;
|
|
659
|
+
snapshotSummary.created += snapshotStore.created;
|
|
660
|
+
snapshotSummary.updated += snapshotStore.updated;
|
|
661
|
+
snapshotSummary.failed += snapshotStore.failed;
|
|
662
|
+
reports.push({
|
|
663
|
+
file,
|
|
664
|
+
modeName: options.modeName ?? "default",
|
|
665
|
+
suites: selectedSuites,
|
|
666
|
+
coverage: normalized.coverage,
|
|
667
|
+
runCommand: runCommandForLog,
|
|
668
|
+
snapshotSummary: {
|
|
669
|
+
matched: snapshotStore.matched,
|
|
670
|
+
created: snapshotStore.created,
|
|
671
|
+
updated: snapshotStore.updated,
|
|
672
|
+
failed: snapshotStore.failed,
|
|
673
|
+
},
|
|
624
674
|
});
|
|
625
675
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
throw new Error(`Failed to run ${path.basename(file)} in mode ${modeLabel} with ${details}`);
|
|
630
|
-
}
|
|
631
|
-
const normalized = normalizeReport(report);
|
|
632
|
-
const selectedSuites = options.suiteSelectors?.length
|
|
633
|
-
? filterSelectedSuites(normalized.suites, options.suiteSelectors, file, options.modeName ?? "default")
|
|
634
|
-
: normalized.suites;
|
|
635
|
-
snapshotStore.flush();
|
|
636
|
-
snapshotSummary.matched += snapshotStore.matched;
|
|
637
|
-
snapshotSummary.created += snapshotStore.created;
|
|
638
|
-
snapshotSummary.updated += snapshotStore.updated;
|
|
639
|
-
snapshotSummary.failed += snapshotStore.failed;
|
|
640
|
-
reports.push({
|
|
641
|
-
file,
|
|
642
|
-
modeName: options.modeName ?? "default",
|
|
643
|
-
suites: selectedSuites,
|
|
644
|
-
coverage: normalized.coverage,
|
|
645
|
-
runCommand: runCommandForLog,
|
|
646
|
-
snapshotSummary: {
|
|
647
|
-
matched: snapshotStore.matched,
|
|
648
|
-
created: snapshotStore.created,
|
|
649
|
-
updated: snapshotStore.updated,
|
|
650
|
-
failed: snapshotStore.failed,
|
|
651
|
-
},
|
|
652
|
-
});
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
await ownedWebSession?.close();
|
|
653
679
|
}
|
|
654
680
|
if (config.logs && config.logs != "none") {
|
|
655
681
|
const logRoot = path.join(process.cwd(), config.logs);
|
|
@@ -686,7 +712,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
686
712
|
clean: cleanOutput,
|
|
687
713
|
snapshotEnabled,
|
|
688
714
|
showCoverage,
|
|
689
|
-
buildTime
|
|
715
|
+
buildTime,
|
|
690
716
|
snapshotSummary,
|
|
691
717
|
coverageSummary,
|
|
692
718
|
stats,
|
|
@@ -705,7 +731,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
|
|
|
705
731
|
}
|
|
706
732
|
return {
|
|
707
733
|
failed,
|
|
708
|
-
buildTime
|
|
734
|
+
buildTime,
|
|
709
735
|
stats,
|
|
710
736
|
snapshotSummary,
|
|
711
737
|
coverageSummary,
|
|
@@ -723,6 +749,9 @@ function resolveRuntimeCommand(runtimeRun, target, emitWarnings = true) {
|
|
|
723
749
|
const normalized = resolveLegacyRuntime(targetDefaultAligned, target, emitWarnings);
|
|
724
750
|
return fallbackToDefaultRuntime(normalized, target, emitWarnings);
|
|
725
751
|
}
|
|
752
|
+
function shouldUsePersistentHeadfulWebSession(target, runtimeCommand) {
|
|
753
|
+
return target == "web" && !runtimeCommand.includes("--headless");
|
|
754
|
+
}
|
|
726
755
|
function alignDefaultRuntimeToTarget(runtimeRun, target) {
|
|
727
756
|
const fallback = getDefaultRuntimeFallback(target);
|
|
728
757
|
if (!fallback)
|
|
@@ -817,19 +846,19 @@ function fallbackToDefaultRuntime(runtimeRun, target, emitWarnings) {
|
|
|
817
846
|
function getDefaultRuntimeFallback(target) {
|
|
818
847
|
if (target == "wasi") {
|
|
819
848
|
return {
|
|
820
|
-
command: "node ./.as-test/runners/default.wasi.js
|
|
849
|
+
command: "node ./.as-test/runners/default.wasi.js",
|
|
821
850
|
scriptPath: "./.as-test/runners/default.wasi.js",
|
|
822
851
|
};
|
|
823
852
|
}
|
|
824
853
|
if (target == "bindings") {
|
|
825
854
|
return {
|
|
826
|
-
command: "node ./.as-test/runners/default.bindings.js
|
|
855
|
+
command: "node ./.as-test/runners/default.bindings.js",
|
|
827
856
|
scriptPath: "./.as-test/runners/default.bindings.js",
|
|
828
857
|
};
|
|
829
858
|
}
|
|
830
859
|
if (target == "web") {
|
|
831
860
|
return {
|
|
832
|
-
command: "node ./.as-test/runners/default.web.js
|
|
861
|
+
command: "node ./.as-test/runners/default.web.js",
|
|
833
862
|
scriptPath: "./.as-test/runners/default.web.js",
|
|
834
863
|
};
|
|
835
864
|
}
|
|
@@ -841,7 +870,6 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
|
|
|
841
870
|
return null;
|
|
842
871
|
const resolvedScriptPath = path.join(process.cwd(), fallback.scriptPath);
|
|
843
872
|
if (existsSync(resolvedScriptPath)) {
|
|
844
|
-
ensureDefaultRuntimeHookFiles(target);
|
|
845
873
|
return fallback;
|
|
846
874
|
}
|
|
847
875
|
const source = getDefaultRuntimeRunnerSource(target);
|
|
@@ -854,223 +882,37 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
|
|
|
854
882
|
if (emitWarnings) {
|
|
855
883
|
process.stderr.write(chalk.dim(`runtime script missing; created ${fallback.scriptPath}\n`));
|
|
856
884
|
}
|
|
857
|
-
ensureDefaultRuntimeHookFiles(target);
|
|
858
885
|
return fallback;
|
|
859
886
|
}
|
|
860
|
-
function ensureDefaultRuntimeHookFiles(target) {
|
|
861
|
-
const hooks = getDefaultRuntimeRunnerHookFiles(target);
|
|
862
|
-
for (const file of hooks) {
|
|
863
|
-
if (existsSync(file.path))
|
|
864
|
-
continue;
|
|
865
|
-
if (!existsSync(path.dirname(file.path))) {
|
|
866
|
-
mkdirSync(path.dirname(file.path), { recursive: true });
|
|
867
|
-
}
|
|
868
|
-
writeFileSync(file.path, file.source);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
function getDefaultRuntimeRunnerHookFiles(target) {
|
|
872
|
-
if (target == "bindings") {
|
|
873
|
-
return [
|
|
874
|
-
{
|
|
875
|
-
path: path.join(process.cwd(), "./.as-test/runners/default.bindings.hooks.js"),
|
|
876
|
-
source: getDefaultBindingsRunnerHooksSource(),
|
|
877
|
-
},
|
|
878
|
-
];
|
|
879
|
-
}
|
|
880
|
-
if (target == "web") {
|
|
881
|
-
return [
|
|
882
|
-
{
|
|
883
|
-
path: path.join(process.cwd(), "./.as-test/runners/default.web.hooks.js"),
|
|
884
|
-
source: buildWebRunnerHooksSource(),
|
|
885
|
-
},
|
|
886
|
-
];
|
|
887
|
-
}
|
|
888
|
-
return [];
|
|
889
|
-
}
|
|
890
887
|
function getDefaultRuntimeRunnerSource(target) {
|
|
891
888
|
if (target == "wasi") {
|
|
892
|
-
return `import {
|
|
893
|
-
import { WASI } from "wasi";
|
|
894
|
-
|
|
895
|
-
const originalEmitWarning = process.emitWarning.bind(process);
|
|
896
|
-
process.emitWarning = ((warning, ...args) => {
|
|
897
|
-
const type = typeof args[0] == "string" ? args[0] : "";
|
|
898
|
-
const name = typeof warning?.name == "string" ? warning.name : type;
|
|
899
|
-
const message =
|
|
900
|
-
typeof warning == "string" ? warning : String(warning?.message ?? "");
|
|
901
|
-
if (
|
|
902
|
-
name == "ExperimentalWarning" &&
|
|
903
|
-
message.includes("WASI is an experimental feature")
|
|
904
|
-
) {
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
return originalEmitWarning(warning, ...args);
|
|
908
|
-
});
|
|
889
|
+
return `import { instantiate } from "as-test/lib";
|
|
909
890
|
|
|
910
|
-
const
|
|
911
|
-
if (!wasmPath) {
|
|
912
|
-
process.stderr.write("usage: node ./.as-test/runners/default.wasi.js <file.wasm>\\n");
|
|
913
|
-
process.exit(1);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
try {
|
|
917
|
-
const wasi = new WASI({
|
|
918
|
-
version: "preview1",
|
|
919
|
-
args: [wasmPath],
|
|
920
|
-
env: process.env,
|
|
921
|
-
preopens: {},
|
|
922
|
-
});
|
|
891
|
+
const imports = {};
|
|
923
892
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
for (const entry of WebAssembly.Module.imports(module)) {
|
|
932
|
-
if (entry.module == "env" && entry.kind == "function" && !(entry.name in envImports)) {
|
|
933
|
-
envImports[entry.name] = () => 0;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
const instance = new WebAssembly.Instance(module, {
|
|
937
|
-
env: envImports,
|
|
938
|
-
wasi_snapshot_preview1: wasi.wasiImport,
|
|
893
|
+
instantiate(imports)
|
|
894
|
+
.then((instance) => {
|
|
895
|
+
instance.exports.start?.();
|
|
896
|
+
// Add extra startup logic here when needed.
|
|
897
|
+
})
|
|
898
|
+
.catch((error) => {
|
|
899
|
+
throw new Error("Failed to run WASI module: " + String(error));
|
|
939
900
|
});
|
|
940
|
-
wasi.start(instance);
|
|
941
|
-
} catch (error) {
|
|
942
|
-
process.stderr.write("failed to run WASI module: " + String(error) + "\\n");
|
|
943
|
-
process.exit(1);
|
|
944
|
-
}
|
|
945
901
|
`;
|
|
946
902
|
}
|
|
947
903
|
if (target == "bindings") {
|
|
948
|
-
return `import
|
|
949
|
-
import path from "path";
|
|
950
|
-
import { pathToFileURL } from "url";
|
|
951
|
-
|
|
952
|
-
const HOOKS_PATH = path.resolve(
|
|
953
|
-
path.dirname(new URL(import.meta.url).pathname),
|
|
954
|
-
"./default.bindings.hooks.js",
|
|
955
|
-
);
|
|
956
|
-
|
|
957
|
-
function readExact(length) {
|
|
958
|
-
const out = Buffer.alloc(length);
|
|
959
|
-
let offset = 0;
|
|
960
|
-
while (offset < length) {
|
|
961
|
-
let read = 0;
|
|
962
|
-
try {
|
|
963
|
-
read = fs.readSync(0, out, offset, length - offset, null);
|
|
964
|
-
} catch (error) {
|
|
965
|
-
if (error && error.code === "EAGAIN") {
|
|
966
|
-
continue;
|
|
967
|
-
}
|
|
968
|
-
throw error;
|
|
969
|
-
}
|
|
970
|
-
if (!read) break;
|
|
971
|
-
offset += read;
|
|
972
|
-
}
|
|
973
|
-
const view = out.subarray(0, offset);
|
|
974
|
-
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
975
|
-
}
|
|
904
|
+
return `import { instantiate } from "as-test/lib";
|
|
976
905
|
|
|
977
|
-
|
|
978
|
-
const view = Buffer.from(data);
|
|
979
|
-
fs.writeSync(1, view);
|
|
980
|
-
}
|
|
906
|
+
const imports = {};
|
|
981
907
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
return readExact(Number(size ?? 0));
|
|
991
|
-
},
|
|
992
|
-
writeFrame(data) {
|
|
993
|
-
writeRaw(data);
|
|
994
|
-
return true;
|
|
995
|
-
},
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function createAsTestImports(ctx) {
|
|
1000
|
-
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
1001
|
-
process.stdout.write = (chunk, ...args) => {
|
|
1002
|
-
if (chunk instanceof ArrayBuffer) {
|
|
1003
|
-
return ctx.writeFrame(chunk);
|
|
1004
|
-
}
|
|
1005
|
-
return originalWrite(chunk, ...args);
|
|
1006
|
-
};
|
|
1007
|
-
process.stdin.read = (size) => ctx.readFrame(size);
|
|
1008
|
-
return {};
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
function mergeImports(...groups) {
|
|
1012
|
-
const out = {};
|
|
1013
|
-
for (const group of groups) {
|
|
1014
|
-
if (!group || typeof group != "object") continue;
|
|
1015
|
-
for (const moduleName of Object.keys(group)) {
|
|
1016
|
-
out[moduleName] = Object.assign(out[moduleName] || {}, group[moduleName]);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
return out;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
async function loadRunnerHooks() {
|
|
1023
|
-
if (!fs.existsSync(HOOKS_PATH)) {
|
|
1024
|
-
return {
|
|
1025
|
-
createUserImports() {
|
|
1026
|
-
return {};
|
|
1027
|
-
},
|
|
1028
|
-
async runModule(_exports, _ctx) {},
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
const mod = await import(pathToFileURL(HOOKS_PATH).href + "?t=" + Date.now());
|
|
1032
|
-
return {
|
|
1033
|
-
createUserImports:
|
|
1034
|
-
typeof mod.createUserImports == "function"
|
|
1035
|
-
? mod.createUserImports
|
|
1036
|
-
: () => ({}),
|
|
1037
|
-
runModule:
|
|
1038
|
-
typeof mod.runModule == "function" ? mod.runModule : async () => {},
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
async function instantiateModule(ctx, hooks) {
|
|
1043
|
-
const helper = await import(pathToFileURL(ctx.helperPath).href);
|
|
1044
|
-
if (typeof helper.instantiate !== "function") {
|
|
1045
|
-
throw new Error("bindings helper missing instantiate export");
|
|
1046
|
-
}
|
|
1047
|
-
const imports = mergeImports(
|
|
1048
|
-
createAsTestImports(ctx),
|
|
1049
|
-
await hooks.createUserImports(ctx),
|
|
1050
|
-
);
|
|
1051
|
-
return helper.instantiate(ctx.module, imports);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const wasmPathArg = process.argv[2];
|
|
1055
|
-
if (!wasmPathArg) {
|
|
1056
|
-
process.stderr.write("usage: node ./.as-test/runners/default.bindings.js <file.wasm>\\n");
|
|
1057
|
-
process.exit(1);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
const wasmPath = path.resolve(process.cwd(), wasmPathArg);
|
|
1061
|
-
const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
|
|
1062
|
-
|
|
1063
|
-
try {
|
|
1064
|
-
const binary = fs.readFileSync(wasmPath);
|
|
1065
|
-
const module = new WebAssembly.Module(binary);
|
|
1066
|
-
const ctx = createRunnerContext({ wasmPath, module, helperPath: jsPath });
|
|
1067
|
-
const hooks = await loadRunnerHooks();
|
|
1068
|
-
const exports = await instantiateModule(ctx, hooks);
|
|
1069
|
-
await hooks.runModule(exports, ctx);
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
|
|
1072
|
-
process.exit(1);
|
|
1073
|
-
}
|
|
908
|
+
instantiate(imports)
|
|
909
|
+
.then((instance) => {
|
|
910
|
+
instance.exports.start?.();
|
|
911
|
+
// Add extra startup logic here when needed.
|
|
912
|
+
})
|
|
913
|
+
.catch((error) => {
|
|
914
|
+
throw new Error("Failed to run bindings module: " + String(error));
|
|
915
|
+
});
|
|
1074
916
|
`;
|
|
1075
917
|
}
|
|
1076
918
|
if (target == "web") {
|
|
@@ -1078,24 +920,6 @@ try {
|
|
|
1078
920
|
}
|
|
1079
921
|
return null;
|
|
1080
922
|
}
|
|
1081
|
-
function getDefaultBindingsRunnerHooksSource() {
|
|
1082
|
-
return `export function createUserImports(_ctx) {
|
|
1083
|
-
return {
|
|
1084
|
-
// env: {
|
|
1085
|
-
// now_ms: () => Date.now(),
|
|
1086
|
-
// },
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
export async function runModule(_exports, _ctx) {
|
|
1091
|
-
// The generated bindings helper already calls exports._start().
|
|
1092
|
-
// Add extra startup calls here when your module exposes them.
|
|
1093
|
-
//
|
|
1094
|
-
// Example:
|
|
1095
|
-
// _exports.run?.();
|
|
1096
|
-
}
|
|
1097
|
-
`;
|
|
1098
|
-
}
|
|
1099
923
|
function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
|
|
1100
924
|
const base = path
|
|
1101
925
|
.basename(file)
|
|
@@ -1141,14 +965,55 @@ function resolveDisambiguator(file, duplicateSpecBasenames) {
|
|
|
1141
965
|
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
1142
966
|
.replace(/^_+|_+$/g, "");
|
|
1143
967
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
968
|
+
function resolveRuntimeTargetEnv(target, wasmPath) {
|
|
969
|
+
if (target == "bindings") {
|
|
970
|
+
return resolveBindingsRuntimeEnv(wasmPath);
|
|
971
|
+
}
|
|
972
|
+
if (target == "web") {
|
|
973
|
+
return resolveWebRuntimeEnv(wasmPath);
|
|
974
|
+
}
|
|
975
|
+
if (target == "wasi") {
|
|
976
|
+
return {
|
|
977
|
+
AS_TEST_RUNTIME_TARGET: "wasi",
|
|
978
|
+
AS_TEST_WASM_PATH: wasmPath,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
return {};
|
|
982
|
+
}
|
|
983
|
+
function resolveBindingsRuntimeEnv(wasmPath) {
|
|
984
|
+
const helperPath = wasmPath.replace(/\.wasm$/, ".js");
|
|
985
|
+
const kind = detectBindingsKind(wasmPath, helperPath);
|
|
986
|
+
const env = {
|
|
987
|
+
AS_TEST_RUNTIME_TARGET: "bindings",
|
|
988
|
+
AS_TEST_WASM_PATH: wasmPath,
|
|
989
|
+
AS_TEST_BINDINGS_KIND: kind,
|
|
990
|
+
};
|
|
991
|
+
if (kind != "none") {
|
|
992
|
+
env.AS_TEST_HELPER_PATH = helperPath;
|
|
993
|
+
}
|
|
994
|
+
return env;
|
|
995
|
+
}
|
|
996
|
+
function resolveWebRuntimeEnv(wasmPath) {
|
|
997
|
+
const env = resolveBindingsRuntimeEnv(wasmPath);
|
|
998
|
+
env.AS_TEST_RUNTIME_TARGET = "web";
|
|
999
|
+
return env;
|
|
1000
|
+
}
|
|
1001
|
+
function detectBindingsKind(wasmPath, helperPath) {
|
|
1002
|
+
if (!existsSync(wasmPath)) {
|
|
1003
|
+
throw new Error(`bindings artifact not found: ${wasmPath}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (!existsSync(helperPath)) {
|
|
1006
|
+
return "none";
|
|
1007
|
+
}
|
|
1008
|
+
const source = readFileSync(helperPath, "utf8");
|
|
1009
|
+
if (/\bexport\s+(async\s+)?function\s+instantiate\b/.test(source)) {
|
|
1010
|
+
return "raw";
|
|
1011
|
+
}
|
|
1012
|
+
if (/\bexport\s+const\b/.test(source) &&
|
|
1013
|
+
/new URL\([^)]*\.wasm["']?,\s*import\.meta\.url\)/.test(source)) {
|
|
1014
|
+
return "esm";
|
|
1015
|
+
}
|
|
1016
|
+
throw new Error(`could not detect bindings kind for ${helperPath}; expected raw or esm helper output`);
|
|
1152
1017
|
}
|
|
1153
1018
|
function extractRuntimeScriptPath(runtimeRun) {
|
|
1154
1019
|
const tokens = runtimeRun
|
|
@@ -1930,6 +1795,285 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1930
1795
|
}
|
|
1931
1796
|
return report;
|
|
1932
1797
|
}
|
|
1798
|
+
async function runWebSessionProcess(session, specFile, crashDir, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
|
|
1799
|
+
const input = new PassThrough();
|
|
1800
|
+
const output = new PassThrough();
|
|
1801
|
+
let report = null;
|
|
1802
|
+
let parseError = null;
|
|
1803
|
+
let stderrBuffer = "";
|
|
1804
|
+
let stdoutBuffer = "";
|
|
1805
|
+
let sawChannelClose = false;
|
|
1806
|
+
const runtimeEvents = {
|
|
1807
|
+
sawFileStart: false,
|
|
1808
|
+
sawFileEnd: false,
|
|
1809
|
+
fileName: path.basename(specFile),
|
|
1810
|
+
fileVerdict: "none",
|
|
1811
|
+
fileTime: "",
|
|
1812
|
+
suiteStarts: 0,
|
|
1813
|
+
suiteEnds: 0,
|
|
1814
|
+
assertionFails: 0,
|
|
1815
|
+
warnings: 0,
|
|
1816
|
+
logs: 0,
|
|
1817
|
+
};
|
|
1818
|
+
const reportStream = {
|
|
1819
|
+
dataFrames: 0,
|
|
1820
|
+
dataBytes: 0,
|
|
1821
|
+
sawChunkStart: false,
|
|
1822
|
+
sawChunkEnd: false,
|
|
1823
|
+
chunkCountExpected: 0,
|
|
1824
|
+
chunkBytesExpected: 0,
|
|
1825
|
+
chunkTotalBytesExpected: 0,
|
|
1826
|
+
chunkFramesReceived: 0,
|
|
1827
|
+
chunkBytesReceived: 0,
|
|
1828
|
+
chunks: [],
|
|
1829
|
+
};
|
|
1830
|
+
class TestChannel extends Channel {
|
|
1831
|
+
onPassthrough(data) {
|
|
1832
|
+
stdoutBuffer += data.toString("utf8");
|
|
1833
|
+
if (tapMode) {
|
|
1834
|
+
process.stderr.write(data);
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
process.stdout.write(data);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
onCall(msg) {
|
|
1841
|
+
const event = msg;
|
|
1842
|
+
const kind = String(event.kind ?? "");
|
|
1843
|
+
if (kind === "event:assert-fail") {
|
|
1844
|
+
runtimeEvents.assertionFails++;
|
|
1845
|
+
reporter.onAssertionFail?.({
|
|
1846
|
+
key: String(event.key ?? ""),
|
|
1847
|
+
instr: String(event.instr ?? ""),
|
|
1848
|
+
left: String(event.left ?? ""),
|
|
1849
|
+
right: String(event.right ?? ""),
|
|
1850
|
+
message: String(event.message ?? ""),
|
|
1851
|
+
});
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (kind === "event:file-start") {
|
|
1855
|
+
runtimeEvents.sawFileStart = true;
|
|
1856
|
+
runtimeEvents.fileName = String(event.file ?? runtimeEvents.fileName);
|
|
1857
|
+
reporter.onFileStart?.({
|
|
1858
|
+
file: String(event.file ?? "unknown"),
|
|
1859
|
+
depth: 0,
|
|
1860
|
+
suiteKind: "file",
|
|
1861
|
+
description: String(event.file ?? "unknown"),
|
|
1862
|
+
});
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
if (kind === "event:file-end") {
|
|
1866
|
+
runtimeEvents.sawFileEnd = true;
|
|
1867
|
+
runtimeEvents.fileName = String(event.file ?? runtimeEvents.fileName);
|
|
1868
|
+
runtimeEvents.fileVerdict = String(event.verdict ?? "none");
|
|
1869
|
+
runtimeEvents.fileTime = String(event.time ?? "");
|
|
1870
|
+
reporter.onFileEnd?.({
|
|
1871
|
+
file: String(event.file ?? "unknown"),
|
|
1872
|
+
depth: 0,
|
|
1873
|
+
suiteKind: "file",
|
|
1874
|
+
description: String(event.file ?? "unknown"),
|
|
1875
|
+
verdict: String(event.verdict ?? "none"),
|
|
1876
|
+
time: String(event.time ?? ""),
|
|
1877
|
+
});
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (kind === "event:suite-start") {
|
|
1881
|
+
runtimeEvents.suiteStarts++;
|
|
1882
|
+
reporter.onSuiteStart?.({
|
|
1883
|
+
file: String(event.file ?? "unknown"),
|
|
1884
|
+
depth: Number(event.depth ?? 0),
|
|
1885
|
+
suiteKind: String(event.suiteKind ?? ""),
|
|
1886
|
+
description: String(event.description ?? ""),
|
|
1887
|
+
});
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (kind === "event:suite-end") {
|
|
1891
|
+
runtimeEvents.suiteEnds++;
|
|
1892
|
+
reporter.onSuiteEnd?.({
|
|
1893
|
+
file: String(event.file ?? "unknown"),
|
|
1894
|
+
depth: Number(event.depth ?? 0),
|
|
1895
|
+
suiteKind: String(event.suiteKind ?? ""),
|
|
1896
|
+
description: String(event.description ?? ""),
|
|
1897
|
+
verdict: String(event.verdict ?? "none"),
|
|
1898
|
+
});
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
if (kind === "event:warn") {
|
|
1902
|
+
runtimeEvents.warnings++;
|
|
1903
|
+
reporter.onWarning?.({
|
|
1904
|
+
message: String(event.message ?? ""),
|
|
1905
|
+
});
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (kind === "event:log") {
|
|
1909
|
+
runtimeEvents.logs++;
|
|
1910
|
+
reporter.onLog?.({
|
|
1911
|
+
file: String(event.file ?? "unknown"),
|
|
1912
|
+
depth: Number(event.depth ?? 0),
|
|
1913
|
+
text: String(event.text ?? ""),
|
|
1914
|
+
});
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (kind === "snapshot:assert") {
|
|
1918
|
+
const key = String(event.key ?? "");
|
|
1919
|
+
const actual = String(event.actual ?? "");
|
|
1920
|
+
const result = snapshots.assert(key, actual, snapshotEnabled, createSnapshots, overwriteSnapshots);
|
|
1921
|
+
if (result.warnMissing) {
|
|
1922
|
+
reporter.onSnapshotMissing?.({ key });
|
|
1923
|
+
}
|
|
1924
|
+
this.send(MessageType.CALL, Buffer.from(`${result.ok ? "1" : "0"}\n${result.expected}`, "utf8"));
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (kind === "report:start") {
|
|
1928
|
+
reportStream.sawChunkStart = true;
|
|
1929
|
+
reportStream.sawChunkEnd = false;
|
|
1930
|
+
reportStream.chunkCountExpected = Number(event.chunkCount ?? 0);
|
|
1931
|
+
reportStream.chunkBytesExpected = Number(event.chunkBytes ?? 0);
|
|
1932
|
+
reportStream.chunkTotalBytesExpected = Number(event.totalBytes ?? 0);
|
|
1933
|
+
reportStream.chunkFramesReceived = 0;
|
|
1934
|
+
reportStream.chunkBytesReceived = 0;
|
|
1935
|
+
reportStream.chunks = [];
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (kind === "report:end") {
|
|
1939
|
+
reportStream.sawChunkEnd = true;
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
this.sendJSON(MessageType.CALL, { ok: true, expected: "" });
|
|
1943
|
+
}
|
|
1944
|
+
onDataMessage(data) {
|
|
1945
|
+
reportStream.dataFrames++;
|
|
1946
|
+
reportStream.dataBytes += data.length;
|
|
1947
|
+
if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
|
|
1948
|
+
reportStream.chunkFramesReceived++;
|
|
1949
|
+
reportStream.chunkBytesReceived += data.length;
|
|
1950
|
+
reportStream.chunks.push(data.toString("utf8"));
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
report = JSON.parse(data.toString("utf8"));
|
|
1955
|
+
parseError = null;
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
parseError = String(error);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
onClose() {
|
|
1962
|
+
sawChannelClose = true;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
const channel = new TestChannel(input, output);
|
|
1966
|
+
output.on("data", (chunk) => {
|
|
1967
|
+
session.sendReply(Buffer.from(chunk));
|
|
1968
|
+
});
|
|
1969
|
+
let code = 0;
|
|
1970
|
+
try {
|
|
1971
|
+
await session.runJob(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] == "string")), path.basename(specFile), (frame) => {
|
|
1972
|
+
input.write(frame);
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
catch (error) {
|
|
1976
|
+
code = 1;
|
|
1977
|
+
await session.close(error instanceof Error ? error : new Error(String(error)));
|
|
1978
|
+
stderrBuffer +=
|
|
1979
|
+
(error instanceof Error ? error.stack ?? error.message : String(error)) +
|
|
1980
|
+
"\n";
|
|
1981
|
+
}
|
|
1982
|
+
finally {
|
|
1983
|
+
input.end();
|
|
1984
|
+
output.end();
|
|
1985
|
+
}
|
|
1986
|
+
if (reportStream.sawChunkStart) {
|
|
1987
|
+
if (!reportStream.sawChunkEnd) {
|
|
1988
|
+
parseError =
|
|
1989
|
+
parseError ??
|
|
1990
|
+
"missing report:end marker for chunked report payload";
|
|
1991
|
+
}
|
|
1992
|
+
else {
|
|
1993
|
+
const chunkedPayload = reportStream.chunks.join("");
|
|
1994
|
+
try {
|
|
1995
|
+
report = JSON.parse(chunkedPayload);
|
|
1996
|
+
parseError = null;
|
|
1997
|
+
}
|
|
1998
|
+
catch (error) {
|
|
1999
|
+
parseError = `could not parse chunked report payload: ${String(error)}`;
|
|
2000
|
+
}
|
|
2001
|
+
if (reportStream.chunkCountExpected > 0 &&
|
|
2002
|
+
reportStream.chunkFramesReceived !== reportStream.chunkCountExpected) {
|
|
2003
|
+
parseError =
|
|
2004
|
+
parseError ??
|
|
2005
|
+
`chunk count mismatch: expected ${reportStream.chunkCountExpected}, received ${reportStream.chunkFramesReceived}`;
|
|
2006
|
+
}
|
|
2007
|
+
if (reportStream.chunkTotalBytesExpected > 0 &&
|
|
2008
|
+
reportStream.chunkBytesReceived !== reportStream.chunkTotalBytesExpected) {
|
|
2009
|
+
parseError =
|
|
2010
|
+
parseError ??
|
|
2011
|
+
`chunk size mismatch: expected ${reportStream.chunkTotalBytesExpected} bytes, received ${reportStream.chunkBytesReceived}`;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (parseError) {
|
|
2016
|
+
const errorText = `could not parse report payload: ${parseError}`;
|
|
2017
|
+
const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
|
|
2018
|
+
const fullError = `${errorText}\n${diagnostics}`;
|
|
2019
|
+
persistCrashRecord(crashDir, {
|
|
2020
|
+
kind: "test",
|
|
2021
|
+
file: specFile,
|
|
2022
|
+
mode: modeName ?? "default",
|
|
2023
|
+
error: fullError,
|
|
2024
|
+
stdout: stdoutBuffer,
|
|
2025
|
+
stderr: stderrBuffer,
|
|
2026
|
+
});
|
|
2027
|
+
return createRuntimeFailureReport(specFile, modeName, "runtime returned an invalid report payload", fullError, stdoutBuffer, stderrBuffer);
|
|
2028
|
+
}
|
|
2029
|
+
if (!report) {
|
|
2030
|
+
const synthesized = synthesizeReportFromRuntimeEvents(specFile, runtimeEvents);
|
|
2031
|
+
if (synthesized) {
|
|
2032
|
+
reporter.onWarning?.({
|
|
2033
|
+
message: "runtime report payload missing; reconstructed result from streamed lifecycle events",
|
|
2034
|
+
});
|
|
2035
|
+
if (code !== 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
|
|
2036
|
+
const errorParts = [];
|
|
2037
|
+
if (code !== 0) {
|
|
2038
|
+
errorParts.push(`child process exited with code ${code}`);
|
|
2039
|
+
}
|
|
2040
|
+
const stderrText = normalizeRuntimeOutput(stderrBuffer);
|
|
2041
|
+
if (stderrText.length) {
|
|
2042
|
+
errorParts.push(stderrText);
|
|
2043
|
+
}
|
|
2044
|
+
const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
|
|
2045
|
+
reporter.onWarning?.({
|
|
2046
|
+
message: `${errorParts.join("; ")}\n${diagnostics}`,
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
return synthesized;
|
|
2050
|
+
}
|
|
2051
|
+
const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
|
|
2052
|
+
const fullError = `missing report payload from test runtime\n${diagnostics}`;
|
|
2053
|
+
persistCrashRecord(crashDir, {
|
|
2054
|
+
kind: "test",
|
|
2055
|
+
file: specFile,
|
|
2056
|
+
mode: modeName ?? "default",
|
|
2057
|
+
error: fullError,
|
|
2058
|
+
stdout: stdoutBuffer,
|
|
2059
|
+
stderr: stderrBuffer,
|
|
2060
|
+
});
|
|
2061
|
+
return createRuntimeFailureReport(specFile, modeName, "missing report payload from test runtime", fullError, stdoutBuffer, stderrBuffer);
|
|
2062
|
+
}
|
|
2063
|
+
if (code != 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
|
|
2064
|
+
const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
|
|
2065
|
+
reporter.onWarning?.({
|
|
2066
|
+
message: [
|
|
2067
|
+
code !== 0 ? `child process exited with code ${code}` : "",
|
|
2068
|
+
normalizeRuntimeOutput(stderrBuffer),
|
|
2069
|
+
diagnostics,
|
|
2070
|
+
]
|
|
2071
|
+
.filter(Boolean)
|
|
2072
|
+
.join("\n"),
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
return report;
|
|
2076
|
+
}
|
|
1933
2077
|
function synthesizeReportFromRuntimeEvents(specFile, runtimeEvents) {
|
|
1934
2078
|
if (!runtimeEvents.sawFileEnd &&
|
|
1935
2079
|
runtimeEvents.suiteStarts <= 0 &&
|