as-test 1.0.6 → 1.0.9

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.
@@ -396,6 +396,10 @@ function printPlan(root, target, example, fuzzExample, install) {
396
396
  path: ".as-test/runners/default.bindings.js",
397
397
  isDir: false,
398
398
  });
399
+ fileEntries.push({
400
+ path: ".as-test/runners/default.bindings.hooks.js",
401
+ isDir: false,
402
+ });
399
403
  fileEntries.push({
400
404
  path: ".as-test/runners/default.wasi.js",
401
405
  isDir: false,
@@ -404,6 +408,10 @@ function printPlan(root, target, example, fuzzExample, install) {
404
408
  path: ".as-test/runners/default.web.js",
405
409
  isDir: false,
406
410
  });
411
+ fileEntries.push({
412
+ path: ".as-test/runners/default.web.hooks.js",
413
+ isDir: false,
414
+ });
407
415
  }
408
416
  if (example != "none") {
409
417
  fileEntries.push({
@@ -514,10 +522,18 @@ function applyInit(root, target, example, fuzzExample, force) {
514
522
  const runnerPath = path.join(root, ".as-test/runners/default.bindings.js");
515
523
  writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.bindings.js");
516
524
  }
525
+ if (target == "wasi" || target == "bindings" || target == "web") {
526
+ const hooksPath = path.join(root, ".as-test/runners/default.bindings.hooks.js");
527
+ writeManagedFile(hooksPath, buildBindingsRunnerHooks(), force, summary, ".as-test/runners/default.bindings.hooks.js");
528
+ }
517
529
  if (target == "wasi" || target == "bindings" || target == "web") {
518
530
  const runnerPath = path.join(root, ".as-test/runners/default.web.js");
519
531
  writeManagedFile(runnerPath, buildWebRunnerSource(), force, summary, ".as-test/runners/default.web.js");
520
532
  }
533
+ if (target == "wasi" || target == "bindings" || target == "web") {
534
+ const hooksPath = path.join(root, ".as-test/runners/default.web.hooks.js");
535
+ writeManagedFile(hooksPath, buildWebRunnerHooks(), force, summary, ".as-test/runners/default.web.hooks.js");
536
+ }
521
537
  const pkgPath = path.join(root, "package.json");
522
538
  const pkg = existsSync(pkgPath)
523
539
  ? JSON.parse(readFileSync(pkgPath, "utf8"))
@@ -961,7 +977,7 @@ function buildBasicFuzzerExample() {
961
977
  fuzz("basic string fuzzer", (value: string): bool => {
962
978
  expect(value.length >= 0).toBe(true);
963
979
  return value.length <= 24;
964
- }).generate((seed: FuzzSeed, run: (value: string) => bool): void => {
980
+ }, 250).generate((seed: FuzzSeed, run: (value: string) => bool): void => {
965
981
  run(
966
982
  seed.string({
967
983
  charset: "ascii",
@@ -1028,7 +1044,10 @@ function buildBindingsRunner() {
1028
1044
  import path from "path";
1029
1045
  import { pathToFileURL } from "url";
1030
1046
 
1031
- let patched = false;
1047
+ const HOOKS_PATH = path.resolve(
1048
+ path.dirname(new URL(import.meta.url).pathname),
1049
+ "./default.bindings.hooks.js",
1050
+ );
1032
1051
 
1033
1052
  function readExact(length) {
1034
1053
  const out = Buffer.alloc(length);
@@ -1055,20 +1074,76 @@ function writeRaw(data) {
1055
1074
  fs.writeSync(1, view);
1056
1075
  }
1057
1076
 
1058
- function withNodeIo(imports = {}) {
1059
- if (!patched) {
1060
- patched = true;
1061
- const originalWrite = process.stdout.write.bind(process.stdout);
1062
- process.stdout.write = (chunk, ...args) => {
1063
- if (chunk instanceof ArrayBuffer) {
1064
- writeRaw(chunk);
1065
- return true;
1066
- }
1067
- return originalWrite(chunk, ...args);
1077
+ function createRunnerContext({ wasmPath, module, helperPath }) {
1078
+ return {
1079
+ wasmPath,
1080
+ helperPath,
1081
+ module,
1082
+ argv: process.argv.slice(2),
1083
+ env: process.env,
1084
+ readFrame(size) {
1085
+ return readExact(Number(size ?? 0));
1086
+ },
1087
+ writeFrame(data) {
1088
+ writeRaw(data);
1089
+ return true;
1090
+ },
1091
+ };
1092
+ }
1093
+
1094
+ function createAsTestImports(ctx) {
1095
+ const originalWrite = process.stdout.write.bind(process.stdout);
1096
+ process.stdout.write = (chunk, ...args) => {
1097
+ if (chunk instanceof ArrayBuffer) {
1098
+ return ctx.writeFrame(chunk);
1099
+ }
1100
+ return originalWrite(chunk, ...args);
1101
+ };
1102
+ process.stdin.read = (size) => ctx.readFrame(size);
1103
+ return {};
1104
+ }
1105
+
1106
+ function mergeImports(...groups) {
1107
+ const out = {};
1108
+ for (const group of groups) {
1109
+ if (!group || typeof group != "object") continue;
1110
+ for (const moduleName of Object.keys(group)) {
1111
+ out[moduleName] = Object.assign(out[moduleName] || {}, group[moduleName]);
1112
+ }
1113
+ }
1114
+ return out;
1115
+ }
1116
+
1117
+ async function loadRunnerHooks() {
1118
+ if (!fs.existsSync(HOOKS_PATH)) {
1119
+ return {
1120
+ createUserImports() {
1121
+ return {};
1122
+ },
1123
+ async runModule(_exports, _ctx) {},
1068
1124
  };
1069
- process.stdin.read = (size) => readExact(Number(size ?? 0));
1070
1125
  }
1071
- return imports;
1126
+ const mod = await import(pathToFileURL(HOOKS_PATH).href + "?t=" + Date.now());
1127
+ return {
1128
+ createUserImports:
1129
+ typeof mod.createUserImports == "function"
1130
+ ? mod.createUserImports
1131
+ : () => ({}),
1132
+ runModule:
1133
+ typeof mod.runModule == "function" ? mod.runModule : async () => {},
1134
+ };
1135
+ }
1136
+
1137
+ async function instantiateModule(ctx, hooks) {
1138
+ const helper = await import(pathToFileURL(ctx.helperPath).href);
1139
+ if (typeof helper.instantiate !== "function") {
1140
+ throw new Error("bindings helper missing instantiate export");
1141
+ }
1142
+ const imports = mergeImports(
1143
+ createAsTestImports(ctx),
1144
+ await hooks.createUserImports(ctx),
1145
+ );
1146
+ return helper.instantiate(ctx.module, imports);
1072
1147
  }
1073
1148
 
1074
1149
  const wasmPathArg = process.argv[2];
@@ -1083,14 +1158,49 @@ const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
1083
1158
  try {
1084
1159
  const binary = fs.readFileSync(wasmPath);
1085
1160
  const module = new WebAssembly.Module(binary);
1086
- const mod = await import(pathToFileURL(jsPath).href);
1087
- if (typeof mod.instantiate !== "function") {
1088
- throw new Error("bindings helper missing instantiate export");
1089
- }
1090
- mod.instantiate(module, withNodeIo({}));
1161
+ const ctx = createRunnerContext({ wasmPath, module, helperPath: jsPath });
1162
+ const hooks = await loadRunnerHooks();
1163
+ const exports = await instantiateModule(ctx, hooks);
1164
+ await hooks.runModule(exports, ctx);
1091
1165
  } catch (error) {
1092
1166
  process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
1093
1167
  process.exit(1);
1094
1168
  }
1095
1169
  `;
1096
1170
  }
1171
+ function buildBindingsRunnerHooks() {
1172
+ return `export function createUserImports(_ctx) {
1173
+ return {
1174
+ // env: {
1175
+ // now_ms: () => Date.now(),
1176
+ // },
1177
+ };
1178
+ }
1179
+
1180
+ export async function runModule(_exports, _ctx) {
1181
+ // The generated bindings helper already calls exports._start().
1182
+ // Add extra startup calls here when your module exposes them.
1183
+ //
1184
+ // Example:
1185
+ // _exports.run?.();
1186
+ }
1187
+ `;
1188
+ }
1189
+ function buildWebRunnerHooks() {
1190
+ return `export function createUserImports(_ctx) {
1191
+ return {
1192
+ // env: {
1193
+ // now_ms: () => performance.now(),
1194
+ // },
1195
+ };
1196
+ }
1197
+
1198
+ export async function runModule(_exports, _ctx) {
1199
+ // The generated bindings helper already calls exports._start().
1200
+ // Add extra startup calls here when your module exposes them.
1201
+ //
1202
+ // Example:
1203
+ // _exports.run?.();
1204
+ }
1205
+ `;
1206
+ }
@@ -6,10 +6,11 @@ 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 { buildWebRunnerSource } from "./web-runner-source.js";
9
+ import { buildWebRunnerHooksSource, buildWebRunnerSource, } from "./web-runner-source.js";
10
10
  import { createReporter as createDefaultReporter } from "../reporters/default.js";
11
11
  import { createTapReporter } from "../reporters/tap.js";
12
12
  import { persistCrashRecord } from "../crash-store.js";
13
+ import { describeCoveragePoint } from "../coverage-points.js";
13
14
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
14
15
  class SnapshotStore {
15
16
  constructor(specFile, snapshotDir, duplicateSpecBasenames = new Set()) {
@@ -578,9 +579,27 @@ function applyConfiguredFileTotalToStats(stats, fileSummaryTotal) {
578
579
  stats.skippedFiles += unexecuted;
579
580
  }
580
581
  function resolveRuntimeCommand(runtimeRun, target, emitWarnings = true) {
581
- const normalized = resolveLegacyRuntime(runtimeRun, target, emitWarnings);
582
+ const targetDefaultAligned = alignDefaultRuntimeToTarget(runtimeRun, target);
583
+ const normalized = resolveLegacyRuntime(targetDefaultAligned, target, emitWarnings);
582
584
  return fallbackToDefaultRuntime(normalized, target, emitWarnings);
583
585
  }
586
+ function alignDefaultRuntimeToTarget(runtimeRun, target) {
587
+ const fallback = getDefaultRuntimeFallback(target);
588
+ if (!fallback)
589
+ return runtimeRun;
590
+ const trimmed = runtimeRun.trim();
591
+ if (!trimmed.length || trimmed == fallback.command)
592
+ return runtimeRun;
593
+ const defaults = ["wasi", "bindings", "web"]
594
+ .map((kind) => getDefaultRuntimeFallback(kind))
595
+ .filter((item) => item != null);
596
+ for (const entry of defaults) {
597
+ if (entry.command != fallback.command && entry.command == trimmed) {
598
+ return fallback.command;
599
+ }
600
+ }
601
+ return runtimeRun;
602
+ }
584
603
  function resolveLegacyRuntime(runtimeRun, target, emitWarnings) {
585
604
  if (target == "wasi") {
586
605
  const preferredPath = "./.as-test/runners/default.wasi.js";
@@ -681,8 +700,10 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
681
700
  if (!fallback)
682
701
  return null;
683
702
  const resolvedScriptPath = path.join(process.cwd(), fallback.scriptPath);
684
- if (existsSync(resolvedScriptPath))
703
+ if (existsSync(resolvedScriptPath)) {
704
+ ensureDefaultRuntimeHookFiles(target);
685
705
  return fallback;
706
+ }
686
707
  const source = getDefaultRuntimeRunnerSource(target);
687
708
  if (!source)
688
709
  return fallback;
@@ -693,8 +714,39 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
693
714
  if (emitWarnings) {
694
715
  process.stderr.write(chalk.dim(`runtime script missing; created ${fallback.scriptPath}\n`));
695
716
  }
717
+ ensureDefaultRuntimeHookFiles(target);
696
718
  return fallback;
697
719
  }
720
+ function ensureDefaultRuntimeHookFiles(target) {
721
+ const hooks = getDefaultRuntimeRunnerHookFiles(target);
722
+ for (const file of hooks) {
723
+ if (existsSync(file.path))
724
+ continue;
725
+ if (!existsSync(path.dirname(file.path))) {
726
+ mkdirSync(path.dirname(file.path), { recursive: true });
727
+ }
728
+ writeFileSync(file.path, file.source);
729
+ }
730
+ }
731
+ function getDefaultRuntimeRunnerHookFiles(target) {
732
+ if (target == "bindings") {
733
+ return [
734
+ {
735
+ path: path.join(process.cwd(), "./.as-test/runners/default.bindings.hooks.js"),
736
+ source: getDefaultBindingsRunnerHooksSource(),
737
+ },
738
+ ];
739
+ }
740
+ if (target == "web") {
741
+ return [
742
+ {
743
+ path: path.join(process.cwd(), "./.as-test/runners/default.web.hooks.js"),
744
+ source: buildWebRunnerHooksSource(),
745
+ },
746
+ ];
747
+ }
748
+ return [];
749
+ }
698
750
  function getDefaultRuntimeRunnerSource(target) {
699
751
  if (target == "wasi") {
700
752
  return `import { readFileSync } from "fs";
@@ -757,7 +809,10 @@ try {
757
809
  import path from "path";
758
810
  import { pathToFileURL } from "url";
759
811
 
760
- let patched = false;
812
+ const HOOKS_PATH = path.resolve(
813
+ path.dirname(new URL(import.meta.url).pathname),
814
+ "./default.bindings.hooks.js",
815
+ );
761
816
 
762
817
  function readExact(length) {
763
818
  const out = Buffer.alloc(length);
@@ -784,20 +839,76 @@ function writeRaw(data) {
784
839
  fs.writeSync(1, view);
785
840
  }
786
841
 
787
- function withNodeIo(imports = {}) {
788
- if (!patched) {
789
- patched = true;
790
- const originalWrite = process.stdout.write.bind(process.stdout);
791
- process.stdout.write = (chunk, ...args) => {
792
- if (chunk instanceof ArrayBuffer) {
793
- writeRaw(chunk);
794
- return true;
795
- }
796
- return originalWrite(chunk, ...args);
842
+ function createRunnerContext({ wasmPath, module, helperPath }) {
843
+ return {
844
+ wasmPath,
845
+ helperPath,
846
+ module,
847
+ argv: process.argv.slice(2),
848
+ env: process.env,
849
+ readFrame(size) {
850
+ return readExact(Number(size ?? 0));
851
+ },
852
+ writeFrame(data) {
853
+ writeRaw(data);
854
+ return true;
855
+ },
856
+ };
857
+ }
858
+
859
+ function createAsTestImports(ctx) {
860
+ const originalWrite = process.stdout.write.bind(process.stdout);
861
+ process.stdout.write = (chunk, ...args) => {
862
+ if (chunk instanceof ArrayBuffer) {
863
+ return ctx.writeFrame(chunk);
864
+ }
865
+ return originalWrite(chunk, ...args);
866
+ };
867
+ process.stdin.read = (size) => ctx.readFrame(size);
868
+ return {};
869
+ }
870
+
871
+ function mergeImports(...groups) {
872
+ const out = {};
873
+ for (const group of groups) {
874
+ if (!group || typeof group != "object") continue;
875
+ for (const moduleName of Object.keys(group)) {
876
+ out[moduleName] = Object.assign(out[moduleName] || {}, group[moduleName]);
877
+ }
878
+ }
879
+ return out;
880
+ }
881
+
882
+ async function loadRunnerHooks() {
883
+ if (!fs.existsSync(HOOKS_PATH)) {
884
+ return {
885
+ createUserImports() {
886
+ return {};
887
+ },
888
+ async runModule(_exports, _ctx) {},
797
889
  };
798
- process.stdin.read = (size) => readExact(Number(size ?? 0));
799
890
  }
800
- return imports;
891
+ const mod = await import(pathToFileURL(HOOKS_PATH).href + "?t=" + Date.now());
892
+ return {
893
+ createUserImports:
894
+ typeof mod.createUserImports == "function"
895
+ ? mod.createUserImports
896
+ : () => ({}),
897
+ runModule:
898
+ typeof mod.runModule == "function" ? mod.runModule : async () => {},
899
+ };
900
+ }
901
+
902
+ async function instantiateModule(ctx, hooks) {
903
+ const helper = await import(pathToFileURL(ctx.helperPath).href);
904
+ if (typeof helper.instantiate !== "function") {
905
+ throw new Error("bindings helper missing instantiate export");
906
+ }
907
+ const imports = mergeImports(
908
+ createAsTestImports(ctx),
909
+ await hooks.createUserImports(ctx),
910
+ );
911
+ return helper.instantiate(ctx.module, imports);
801
912
  }
802
913
 
803
914
  const wasmPathArg = process.argv[2];
@@ -812,11 +923,10 @@ const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
812
923
  try {
813
924
  const binary = fs.readFileSync(wasmPath);
814
925
  const module = new WebAssembly.Module(binary);
815
- const mod = await import(pathToFileURL(jsPath).href);
816
- if (typeof mod.instantiate !== "function") {
817
- throw new Error("bindings helper missing instantiate export");
818
- }
819
- mod.instantiate(module, withNodeIo({}));
926
+ const ctx = createRunnerContext({ wasmPath, module, helperPath: jsPath });
927
+ const hooks = await loadRunnerHooks();
928
+ const exports = await instantiateModule(ctx, hooks);
929
+ await hooks.runModule(exports, ctx);
820
930
  } catch (error) {
821
931
  process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
822
932
  process.exit(1);
@@ -828,6 +938,24 @@ try {
828
938
  }
829
939
  return null;
830
940
  }
941
+ function getDefaultBindingsRunnerHooksSource() {
942
+ return `export function createUserImports(_ctx) {
943
+ return {
944
+ // env: {
945
+ // now_ms: () => Date.now(),
946
+ // },
947
+ };
948
+ }
949
+
950
+ export async function runModule(_exports, _ctx) {
951
+ // The generated bindings helper already calls exports._start().
952
+ // Add extra startup calls here when your module exposes them.
953
+ //
954
+ // Example:
955
+ // _exports.run?.();
956
+ }
957
+ `;
958
+ }
831
959
  function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
832
960
  const base = path
833
961
  .basename(file)
@@ -1091,6 +1219,8 @@ function collectCoverageSummary(reports, enabled, showPoints, coverage) {
1091
1219
  for (const point of report.coverage.points) {
1092
1220
  if (isIgnoredCoverageFile(point.file, coverage))
1093
1221
  continue;
1222
+ if (isIgnoredCoveragePoint(point, coverage))
1223
+ continue;
1094
1224
  const key = `${point.file}::${point.hash}`;
1095
1225
  const existing = uniquePoints.get(key);
1096
1226
  if (!existing) {
@@ -1246,10 +1376,19 @@ function resolveCoverageOptions(raw) {
1246
1376
  includeSpecs: false,
1247
1377
  include: [],
1248
1378
  exclude: [],
1379
+ ignore: {
1380
+ labels: [],
1381
+ names: [],
1382
+ locations: [],
1383
+ snippets: [],
1384
+ },
1249
1385
  };
1250
1386
  }
1251
1387
  if (raw && typeof raw == "object") {
1252
1388
  const obj = raw;
1389
+ const ignore = obj.ignore && typeof obj.ignore == "object" && !Array.isArray(obj.ignore)
1390
+ ? obj.ignore
1391
+ : null;
1253
1392
  return {
1254
1393
  enabled: obj.enabled == null ? false : Boolean(obj.enabled),
1255
1394
  includeSpecs: Boolean(obj.includeSpecs),
@@ -1259,6 +1398,20 @@ function resolveCoverageOptions(raw) {
1259
1398
  exclude: Array.isArray(obj.exclude)
1260
1399
  ? obj.exclude.filter((item) => typeof item == "string")
1261
1400
  : [],
1401
+ ignore: {
1402
+ labels: Array.isArray(ignore?.labels)
1403
+ ? ignore.labels.filter((item) => typeof item == "string")
1404
+ : [],
1405
+ names: Array.isArray(ignore?.names)
1406
+ ? ignore.names.filter((item) => typeof item == "string")
1407
+ : [],
1408
+ locations: Array.isArray(ignore?.locations)
1409
+ ? ignore.locations.filter((item) => typeof item == "string")
1410
+ : [],
1411
+ snippets: Array.isArray(ignore?.snippets)
1412
+ ? ignore.snippets.filter((item) => typeof item == "string")
1413
+ : [],
1414
+ },
1262
1415
  };
1263
1416
  }
1264
1417
  return {
@@ -1266,8 +1419,49 @@ function resolveCoverageOptions(raw) {
1266
1419
  includeSpecs: false,
1267
1420
  include: [],
1268
1421
  exclude: [],
1422
+ ignore: {
1423
+ labels: [],
1424
+ names: [],
1425
+ locations: [],
1426
+ snippets: [],
1427
+ },
1269
1428
  };
1270
1429
  }
1430
+ function isIgnoredCoveragePoint(point, coverage) {
1431
+ const ignore = coverage.ignore;
1432
+ if (!ignore.labels.length &&
1433
+ !ignore.names.length &&
1434
+ !ignore.locations.length &&
1435
+ !ignore.snippets.length) {
1436
+ return false;
1437
+ }
1438
+ const info = describeCoveragePoint(point.file, point.line, point.column, point.type);
1439
+ const location = `${point.file.replace(/\\/g, "/")}:${point.line}:${point.column}`;
1440
+ const label = info.displayType.toLowerCase();
1441
+ const name = info.subjectName?.toLowerCase() ?? "";
1442
+ const snippet = info.visible.toLowerCase();
1443
+ if (ignore.labels.some((pattern) => matchesCoverageTextPattern(label, pattern.toLowerCase()))) {
1444
+ return true;
1445
+ }
1446
+ if (name.length &&
1447
+ ignore.names.some((pattern) => matchesCoverageTextPattern(name, pattern.toLowerCase()))) {
1448
+ return true;
1449
+ }
1450
+ if (ignore.locations.some((pattern) => matchesCoverageTextPattern(location, pattern.replace(/\\/g, "/")))) {
1451
+ return true;
1452
+ }
1453
+ if (snippet.length &&
1454
+ ignore.snippets.some((pattern) => matchesCoverageTextPattern(snippet, pattern.toLowerCase()))) {
1455
+ return true;
1456
+ }
1457
+ return false;
1458
+ }
1459
+ function matchesCoverageTextPattern(value, pattern) {
1460
+ const normalized = pattern.trim();
1461
+ if (!normalized.length)
1462
+ return false;
1463
+ return globPatternToRegExp(normalized).test(value);
1464
+ }
1271
1465
  function compareCoveragePoints(a, b) {
1272
1466
  if (a.line !== b.line)
1273
1467
  return a.line - b.line;
@@ -1,3 +1,21 @@
1
+ export function buildWebRunnerHooksSource() {
2
+ return `export function createUserImports(_ctx) {
3
+ return {
4
+ // env: {
5
+ // now_ms: () => performance.now(),
6
+ // },
7
+ };
8
+ }
9
+
10
+ export async function runModule(_exports, _ctx) {
11
+ // The generated bindings helper already calls exports._start().
12
+ // Add extra startup calls here when your module exposes them.
13
+ //
14
+ // Example:
15
+ // _exports.run?.();
16
+ }
17
+ `;
18
+ }
1
19
  export function buildWebRunnerSource() {
2
20
  const html = String.raw `<!doctype html>
3
21
  <html lang="en">
@@ -135,6 +153,7 @@ ws.addEventListener("open", () => {
135
153
  worker.postMessage({
136
154
  kind: "init",
137
155
  helperUrl: "/artifact.js",
156
+ hooksUrl: "/runner-hooks.js",
138
157
  wasmUrl: "/artifact.wasm",
139
158
  replyBuffer,
140
159
  });
@@ -165,32 +184,61 @@ ws.addEventListener("error", () => {
165
184
  const worker = String.raw `let replyState = null;
166
185
  let replyBytes = null;
167
186
 
168
- self.onmessage = async (event) => {
169
- const message = event.data ?? {};
170
- if (message.kind != "init") {
171
- return;
172
- }
173
-
174
- const shared = message.replyBuffer;
175
- replyState = new Int32Array(shared, 0, 2);
176
- replyBytes = new Uint8Array(shared, 8);
187
+ function createRunnerContext({ helperUrl, wasmUrl, module }) {
188
+ return {
189
+ helperUrl,
190
+ wasmUrl,
191
+ module,
192
+ postFrame(frame) {
193
+ self.postMessage({ kind: "wipc", frame }, [frame]);
194
+ return true;
195
+ },
196
+ readFrame(size) {
197
+ return readReply(Number(size ?? 0));
198
+ },
199
+ };
200
+ }
177
201
 
202
+ function createAsTestImports(ctx) {
178
203
  globalThis.process = {
179
204
  stdout: {
180
205
  write(data) {
181
206
  const frame = data instanceof ArrayBuffer ? data : data?.buffer;
182
- self.postMessage({ kind: "wipc", frame }, [frame]);
183
- return true;
207
+ return ctx.postFrame(frame);
184
208
  },
185
209
  },
186
210
  stdin: {
187
211
  read(size) {
188
- return readReply(Number(size ?? 0));
212
+ return ctx.readFrame(size);
189
213
  },
190
214
  },
191
215
  };
216
+ return {};
217
+ }
218
+
219
+ function mergeImports(...groups) {
220
+ const out = {};
221
+ for (const group of groups) {
222
+ if (!group || typeof group != "object") continue;
223
+ for (const moduleName of Object.keys(group)) {
224
+ out[moduleName] = Object.assign(out[moduleName] || {}, group[moduleName]);
225
+ }
226
+ }
227
+ return out;
228
+ }
229
+
230
+ self.onmessage = async (event) => {
231
+ const message = event.data ?? {};
232
+ if (message.kind != "init") {
233
+ return;
234
+ }
235
+
236
+ const shared = message.replyBuffer;
237
+ replyState = new Int32Array(shared, 0, 2);
238
+ replyBytes = new Uint8Array(shared, 8);
192
239
 
193
240
  try {
241
+ const hooks = await import(message.hooksUrl);
194
242
  const helper = await import(message.helperUrl);
195
243
  if (typeof helper.instantiate != "function") {
196
244
  throw new Error("bindings helper missing instantiate export");
@@ -201,8 +249,22 @@ self.onmessage = async (event) => {
201
249
  }
202
250
  const binary = await response.arrayBuffer();
203
251
  const module = new WebAssembly.Module(binary);
252
+ const ctx = createRunnerContext({
253
+ helperUrl: message.helperUrl,
254
+ wasmUrl: message.wasmUrl,
255
+ module,
256
+ });
257
+ const imports = mergeImports(
258
+ createAsTestImports(ctx),
259
+ typeof hooks.createUserImports == "function"
260
+ ? await hooks.createUserImports(ctx)
261
+ : {},
262
+ );
204
263
  self.postMessage({ kind: "ready" });
205
- await helper.instantiate(module, {});
264
+ const exports = await helper.instantiate(module, imports);
265
+ if (typeof hooks.runModule == "function") {
266
+ await hooks.runModule(exports, ctx);
267
+ }
206
268
  self.postMessage({ kind: "done" });
207
269
  } catch (error) {
208
270
  const message =
@@ -235,6 +297,7 @@ function readReply(max) {
235
297
  return out.buffer;
236
298
  }
237
299
  `;
300
+ const hooks = buildWebRunnerHooksSource();
238
301
  return `import { createHash } from "crypto";
239
302
  import { existsSync, readFileSync } from "fs";
240
303
  import http from "http";
@@ -244,6 +307,7 @@ import { spawn } from "child_process";
244
307
  const INDEX_HTML = ${JSON.stringify(html)};
245
308
  const CLIENT_JS = ${JSON.stringify(client)};
246
309
  const WORKER_JS = ${JSON.stringify(worker)};
310
+ const DEFAULT_HOOKS_JS = ${JSON.stringify(hooks)};
247
311
  const MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
248
312
  const HEADLESS_FLAGS = [
249
313
  "--headless=new",
@@ -264,6 +328,7 @@ if (!wasmArg) {
264
328
 
265
329
  const wasmPath = path.resolve(process.cwd(), wasmArg);
266
330
  const helperPath = wasmPath.replace(/\\.wasm$/, ".js");
331
+ const hooksPath = path.resolve(process.cwd(), ".as-test/runners/default.web.hooks.js");
267
332
  if (!existsSync(wasmPath)) {
268
333
  process.stderr.write("missing wasm artifact: " + wasmPath + "\\n");
269
334
  process.exit(1);
@@ -321,6 +386,14 @@ const server = http.createServer((req, res) => {
321
386
  res.end(readFileSync(helperPath, "utf8"));
322
387
  return;
323
388
  }
389
+ if (url == "/runner-hooks.js") {
390
+ res.writeHead(200, {
391
+ ...headers,
392
+ "Content-Type": "text/javascript; charset=utf-8",
393
+ });
394
+ res.end(existsSync(hooksPath) ? readFileSync(hooksPath, "utf8") : DEFAULT_HOOKS_JS);
395
+ return;
396
+ }
324
397
  if (url == "/artifact.wasm") {
325
398
  res.writeHead(200, {
326
399
  ...headers,