as-test 1.0.7 → 1.0.10

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 CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2026-04-18 - v1.0.10
6
+
7
+ ### Reporting
8
+
9
+ - feat: when coverage is incomplete and `--show-coverage` is not passed, the default reporter now prints `Coverage (run with --show-coverage to display uncovered points)`; otherwise it keeps the plain `Coverage` header.
10
+
11
+ ### Assertions
12
+
13
+ - fix: register top-level `expect(...)` and `log(...)` calls (outside explicit suites) into a synthetic global suite so they are counted and reported in final totals.
14
+
15
+ ## 2026-04-18 - v1.0.9
16
+
17
+ ### Runtime
18
+
19
+ - fix: align default runtime command selection with `buildOptions.target` so `bindings` runs no longer probe `default.wasi.js` first or emit unnecessary fallback warnings.
20
+
21
+ ### Fuzzing
22
+
23
+ - feat: allow per-fuzzer operation overrides via `.generate(generator, operations)` and `.generateTyped(generator, operations)` in addition to the existing third `fuzz(..., operations)` argument.
24
+
25
+ ### Type Ergonomics
26
+
27
+ - feat: add and publish an IDE-only declaration shim (`assembly/as-test.intellisense.d.ts`) so `FuzzSeed` option objects can omit fields like `exclude` without TypeScript IntelliSense errors.
28
+
29
+ ### Examples
30
+
31
+ - chore: remove `examples/08-json-as-runner-compare` and update `examples/package.json` test scripts accordingly.
32
+
5
33
  ## 2026-03-31 - v1.0.7
6
34
 
7
35
  ### Coverage
package/README.md CHANGED
@@ -243,6 +243,17 @@ fuzz("hot path stays stable", (): void => {
243
243
  }, 250);
244
244
  ```
245
245
 
246
+ Or pass it as the second argument to `.generate(...)`:
247
+
248
+ ```ts
249
+ fuzz("ascii strings survive concatenation boundaries", (input: string): bool => {
250
+ expect(input.length <= 40).toBe(true);
251
+ return true;
252
+ }).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
253
+ run(seed.string({ charset: "ascii", min: 0, max: 40 }));
254
+ }, 250);
255
+ ```
256
+
246
257
  You can still override fuzz runs from the CLI when you want to force a different count for the current command:
247
258
 
248
259
  ```bash
@@ -18,4 +18,4 @@ fuzz(
18
18
  exclude: [0x00, 0x0a, 0x0d],
19
19
  }),
20
20
  );
21
- });
21
+ }, 250);
@@ -0,0 +1,60 @@
1
+ export {};
2
+
3
+ declare module "as-test" {
4
+ export interface IntellisenseIntegerOptions {
5
+ min?: number;
6
+ max?: number;
7
+ exclude?: number[];
8
+ }
9
+
10
+ export interface IntellisenseFloatOptions {
11
+ min?: number;
12
+ max?: number;
13
+ exclude?: number[];
14
+ }
15
+
16
+ export interface IntellisenseBytesOptions {
17
+ min?: number;
18
+ max?: number;
19
+ include?: number[];
20
+ exclude?: number[];
21
+ }
22
+
23
+ export interface IntellisenseStringOptions {
24
+ charset?:
25
+ | "ascii"
26
+ | "alpha"
27
+ | "alnum"
28
+ | "digit"
29
+ | "hex"
30
+ | "base64"
31
+ | "identifier"
32
+ | "whitespace"
33
+ | "custom";
34
+ min?: number;
35
+ max?: number;
36
+ include?: number[];
37
+ exclude?: number[];
38
+ prefix?: string;
39
+ suffix?: string;
40
+ }
41
+
42
+ export interface IntellisenseArrayOptions {
43
+ min?: number;
44
+ max?: number;
45
+ }
46
+
47
+ export interface FuzzSeed {
48
+ i32(options?: IntellisenseIntegerOptions): number;
49
+ u32(options?: IntellisenseIntegerOptions): number;
50
+ f32(options?: IntellisenseFloatOptions): number;
51
+ f64(options?: IntellisenseFloatOptions): number;
52
+ bytes(options?: IntellisenseBytesOptions): Uint8Array;
53
+ buffer(options?: IntellisenseBytesOptions): ArrayBuffer;
54
+ string(options?: IntellisenseStringOptions): string;
55
+ array<T>(
56
+ item: (seed: FuzzSeed) => T,
57
+ options?: IntellisenseArrayOptions,
58
+ ): Array<T>;
59
+ }
60
+ }
package/assembly/index.ts CHANGED
@@ -41,6 +41,7 @@ export { __as_test_json_value } from "./util/json";
41
41
 
42
42
  let entrySuites: Suite[] = [];
43
43
  let entryFuzzers: FuzzerBase[] = [];
44
+ let globalExpectationSuite: Suite | null = null;
44
45
 
45
46
  // @ts-ignore
46
47
  const FILE = isDefined(ENTRY_FILE) ? ENTRY_FILE : "unknown";
@@ -210,10 +211,7 @@ export function expect<T>(
210
211
  location: string = "",
211
212
  ): Expectation<T> {
212
213
  const test = new Expectation<T>(value, message, snapshotKey(), location);
213
-
214
- if (current_suite) {
215
- current_suite!.addExpectation(test);
216
- }
214
+ resolveExpectationSuite().addExpectation(test);
217
215
 
218
216
  return test;
219
217
  }
@@ -257,11 +255,10 @@ export function __as_test_log_is_enabled(): bool {
257
255
  export function __as_test_log_serialized(formatted: string): void {
258
256
  if (!formatted) return;
259
257
  const lines = formatted.split("\n");
258
+ const suite = resolveExpectationSuite();
260
259
  for (let i = 0; i < lines.length; i++) {
261
260
  const line = unchecked(lines[i]);
262
- if (current_suite) {
263
- current_suite!.addLog(new Log(line));
264
- }
261
+ suite.addLog(new Log(line));
265
262
  }
266
263
  }
267
264
 
@@ -509,6 +506,20 @@ function registerSuite(
509
506
  suites.push(suite);
510
507
  }
511
508
 
509
+ function resolveExpectationSuite(): Suite {
510
+ if (current_suite) return current_suite!;
511
+ return ensureGlobalExpectationSuite();
512
+ }
513
+
514
+ function ensureGlobalExpectationSuite(): Suite {
515
+ if (globalExpectationSuite) return globalExpectationSuite!;
516
+ const suite = new Suite("global", (): void => {}, "describe");
517
+ suite.file = FILE;
518
+ globalExpectationSuite = suite;
519
+ entrySuites.push(suite);
520
+ return suite;
521
+ }
522
+
512
523
  class CoverageReport {
513
524
  total: i32 = 0;
514
525
  covered: i32 = 0;
@@ -193,7 +193,8 @@ export abstract class FuzzerBase {
193
193
  this.operations = operations > 0 ? operations : 0;
194
194
  }
195
195
 
196
- generate<T extends Function>(_generator: T): this {
196
+ generate<T extends Function>(_generator: T, operations: i32 = 0): this {
197
+ if (operations > 0) this.operations = operations;
197
198
  return this;
198
199
  }
199
200
 
@@ -389,14 +390,19 @@ export class Fuzzer0<R> extends FuzzerBase {
389
390
  this.returnsBool = !isVoid<R>();
390
391
  }
391
392
 
392
- generate<T extends Function>(generator: T): this {
393
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
393
394
  this.generator =
394
395
  changetype<(seed: FuzzSeed, run: () => R) => void>(generator);
396
+ if (operations > 0) this.operations = operations;
395
397
  return this;
396
398
  }
397
399
 
398
- generateTyped(generator: (seed: FuzzSeed, run: () => R) => void): this {
400
+ generateTyped(
401
+ generator: (seed: FuzzSeed, run: () => R) => void,
402
+ operations: i32 = 0,
403
+ ): this {
399
404
  this.generator = generator;
405
+ if (operations > 0) this.operations = operations;
400
406
  return this;
401
407
  }
402
408
 
@@ -444,14 +450,19 @@ export class Fuzzer1<A, R> extends FuzzerBase {
444
450
  this.returnsBool = !isVoid<R>();
445
451
  }
446
452
 
447
- generate<T extends Function>(generator: T): this {
453
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
448
454
  this.generator =
449
455
  changetype<(seed: FuzzSeed, run: (a: A) => R) => void>(generator);
456
+ if (operations > 0) this.operations = operations;
450
457
  return this;
451
458
  }
452
459
 
453
- generateTyped(generator: (seed: FuzzSeed, run: (a: A) => R) => void): this {
460
+ generateTyped(
461
+ generator: (seed: FuzzSeed, run: (a: A) => R) => void,
462
+ operations: i32 = 0,
463
+ ): this {
454
464
  this.generator = generator;
465
+ if (operations > 0) this.operations = operations;
455
466
  return this;
456
467
  }
457
468
 
@@ -508,16 +519,19 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
508
519
  this.returnsBool = !isVoid<R>();
509
520
  }
510
521
 
511
- generate<T extends Function>(generator: T): this {
522
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
512
523
  this.generator =
513
524
  changetype<(seed: FuzzSeed, run: (a: A, b: B) => R) => void>(generator);
525
+ if (operations > 0) this.operations = operations;
514
526
  return this;
515
527
  }
516
528
 
517
529
  generateTyped(
518
530
  generator: (seed: FuzzSeed, run: (a: A, b: B) => R) => void,
531
+ operations: i32 = 0,
519
532
  ): this {
520
533
  this.generator = generator;
534
+ if (operations > 0) this.operations = operations;
521
535
  return this;
522
536
  }
523
537
 
@@ -574,15 +588,18 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
574
588
  this.returnsBool = !isVoid<R>();
575
589
  }
576
590
 
577
- generate<T extends Function>(generator: T): this {
591
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
578
592
  this.generator = changetype<usize>(generator);
593
+ if (operations > 0) this.operations = operations;
579
594
  return this;
580
595
  }
581
596
 
582
597
  generateTyped(
583
598
  generator: (seed: FuzzSeed, run: (a: A, b: B, c: C) => R) => void,
599
+ operations: i32 = 0,
584
600
  ): this {
585
601
  this.generator = changetype<usize>(generator);
602
+ if (operations > 0) this.operations = operations;
586
603
  return this;
587
604
  }
588
605
 
@@ -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"))
@@ -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,7 +6,7 @@ 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";
@@ -579,9 +579,27 @@ function applyConfiguredFileTotalToStats(stats, fileSummaryTotal) {
579
579
  stats.skippedFiles += unexecuted;
580
580
  }
581
581
  function resolveRuntimeCommand(runtimeRun, target, emitWarnings = true) {
582
- const normalized = resolveLegacyRuntime(runtimeRun, target, emitWarnings);
582
+ const targetDefaultAligned = alignDefaultRuntimeToTarget(runtimeRun, target);
583
+ const normalized = resolveLegacyRuntime(targetDefaultAligned, target, emitWarnings);
583
584
  return fallbackToDefaultRuntime(normalized, target, emitWarnings);
584
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
+ }
585
603
  function resolveLegacyRuntime(runtimeRun, target, emitWarnings) {
586
604
  if (target == "wasi") {
587
605
  const preferredPath = "./.as-test/runners/default.wasi.js";
@@ -682,8 +700,10 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
682
700
  if (!fallback)
683
701
  return null;
684
702
  const resolvedScriptPath = path.join(process.cwd(), fallback.scriptPath);
685
- if (existsSync(resolvedScriptPath))
703
+ if (existsSync(resolvedScriptPath)) {
704
+ ensureDefaultRuntimeHookFiles(target);
686
705
  return fallback;
706
+ }
687
707
  const source = getDefaultRuntimeRunnerSource(target);
688
708
  if (!source)
689
709
  return fallback;
@@ -694,8 +714,39 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
694
714
  if (emitWarnings) {
695
715
  process.stderr.write(chalk.dim(`runtime script missing; created ${fallback.scriptPath}\n`));
696
716
  }
717
+ ensureDefaultRuntimeHookFiles(target);
697
718
  return fallback;
698
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
+ }
699
750
  function getDefaultRuntimeRunnerSource(target) {
700
751
  if (target == "wasi") {
701
752
  return `import { readFileSync } from "fs";
@@ -758,7 +809,10 @@ try {
758
809
  import path from "path";
759
810
  import { pathToFileURL } from "url";
760
811
 
761
- let patched = false;
812
+ const HOOKS_PATH = path.resolve(
813
+ path.dirname(new URL(import.meta.url).pathname),
814
+ "./default.bindings.hooks.js",
815
+ );
762
816
 
763
817
  function readExact(length) {
764
818
  const out = Buffer.alloc(length);
@@ -785,20 +839,76 @@ function writeRaw(data) {
785
839
  fs.writeSync(1, view);
786
840
  }
787
841
 
788
- function withNodeIo(imports = {}) {
789
- if (!patched) {
790
- patched = true;
791
- const originalWrite = process.stdout.write.bind(process.stdout);
792
- process.stdout.write = (chunk, ...args) => {
793
- if (chunk instanceof ArrayBuffer) {
794
- writeRaw(chunk);
795
- return true;
796
- }
797
- 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) {},
798
889
  };
799
- process.stdin.read = (size) => readExact(Number(size ?? 0));
800
890
  }
801
- 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);
802
912
  }
803
913
 
804
914
  const wasmPathArg = process.argv[2];
@@ -813,11 +923,10 @@ const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
813
923
  try {
814
924
  const binary = fs.readFileSync(wasmPath);
815
925
  const module = new WebAssembly.Module(binary);
816
- const mod = await import(pathToFileURL(jsPath).href);
817
- if (typeof mod.instantiate !== "function") {
818
- throw new Error("bindings helper missing instantiate export");
819
- }
820
- 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);
821
930
  } catch (error) {
822
931
  process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
823
932
  process.exit(1);
@@ -829,6 +938,24 @@ try {
829
938
  }
830
939
  return null;
831
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
+ }
832
959
  function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
833
960
  const base = path
834
961
  .basename(file)
@@ -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,
@@ -283,7 +283,7 @@ class DefaultReporter {
283
283
  renderSnapshotSummary(event.snapshotSummary, true);
284
284
  }
285
285
  if (event.coverageSummary.enabled) {
286
- renderCoverageSummary(event.coverageSummary);
286
+ renderCoverageSummary(event.coverageSummary, event.showCoverage);
287
287
  if (event.showCoverage && event.coverageSummary.uncovered) {
288
288
  renderCoveragePoints(event.coverageSummary.files);
289
289
  }
@@ -633,9 +633,13 @@ function renderSummaryLine(label, summary, layout = {
633
633
  process.stdout.write(", ");
634
634
  process.stdout.write(totalText.padStart(layout.totalWidth) + "\n");
635
635
  }
636
- function renderCoverageSummary(summary) {
636
+ function renderCoverageSummary(summary, showCoverage) {
637
637
  console.log("");
638
- console.log(chalk.bold("Coverage"));
638
+ const shouldShowCoverageHint = !showCoverage && summary.total > 0 && summary.uncovered > 0;
639
+ const coverageHeading = shouldShowCoverageHint
640
+ ? "Coverage (run with --show-coverage to display uncovered points)"
641
+ : "Coverage";
642
+ console.log(chalk.bold(coverageHeading));
639
643
  if (!summary.files.length || summary.total <= 0) {
640
644
  console.log(` ${chalk.dim("No eligible source files were tracked for coverage.")}`);
641
645
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,6 +39,7 @@
39
39
  "description": "Testing framework for AssemblyScript. Compatible with WASI or Bindings",
40
40
  "files": [
41
41
  "assembly/**/*.ts",
42
+ "assembly/**/*.d.ts",
42
43
  "!assembly/__tests__/**",
43
44
  "!assembly/tsconfig.json",
44
45
  "bin/**/*.js",