as-test 1.0.7 → 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.
- package/README.md +11 -0
- package/assembly/__fuzz__/string.fuzz.ts +1 -1
- package/assembly/as-test.intellisense.d.ts +60 -0
- package/assembly/src/fuzz.ts +24 -7
- package/bin/commands/init-core.js +128 -18
- package/bin/commands/run-core.js +148 -21
- package/bin/commands/web-runner-source.js +86 -13
- package/package.json +2 -1
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
|
|
@@ -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/src/fuzz.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
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
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
+
}
|
package/bin/commands/run-core.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
183
|
-
return true;
|
|
207
|
+
return ctx.postFrame(frame);
|
|
184
208
|
},
|
|
185
209
|
},
|
|
186
210
|
stdin: {
|
|
187
211
|
read(size) {
|
|
188
|
-
return
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "as-test",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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",
|