as-test 1.0.1 → 1.0.4
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 +118 -1
- package/README.md +138 -406
- package/as-test.config.schema.json +210 -17
- package/assembly/__fuzz__/array.fuzz.ts +10 -0
- package/assembly/__fuzz__/bytes.fuzz.ts +8 -0
- package/assembly/__fuzz__/math.fuzz.ts +9 -0
- package/assembly/__fuzz__/string.fuzz.ts +21 -0
- package/assembly/index.ts +141 -86
- package/assembly/src/expectation.ts +104 -19
- package/assembly/src/fuzz.ts +723 -0
- package/assembly/src/log.ts +6 -1
- package/assembly/src/suite.ts +45 -3
- package/assembly/util/json.ts +38 -4
- package/assembly/util/wipc.ts +35 -26
- package/bin/build-worker-pool.js +149 -0
- package/bin/build-worker.js +43 -0
- package/bin/commands/build-core.js +221 -28
- package/bin/commands/build.js +17 -1
- package/bin/commands/fuzz-core.js +306 -0
- package/bin/commands/fuzz.js +10 -0
- package/bin/commands/init-core.js +129 -24
- package/bin/commands/run-core.js +525 -123
- package/bin/commands/run.js +4 -1
- package/bin/commands/test.js +8 -3
- package/bin/commands/web-runner-source.js +634 -0
- package/bin/crash-store.js +64 -0
- package/bin/index.js +1526 -148
- package/bin/reporters/default.js +281 -49
- package/bin/reporters/tap.js +83 -2
- package/bin/types.js +19 -2
- package/bin/util.js +315 -33
- package/bin/wipc.js +79 -0
- package/package.json +19 -9
- package/transform/lib/coverage.js +1 -2
- package/transform/lib/index.js +3 -3
- package/transform/lib/log.js +1 -1
|
@@ -1,38 +1,129 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { glob } from "glob";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
5
|
import * as path from "path";
|
|
6
|
+
import { createMemoryStream, main as ascMain, } from "assemblyscript/dist/asc.js";
|
|
6
7
|
import { applyMode, getPkgRunner, loadConfig, tokenizeCommand, resolveProjectModule, } from "../util.js";
|
|
8
|
+
import { persistCrashRecord } from "../crash-store.js";
|
|
9
|
+
import { BuildWorkerPool } from "../build-worker-pool.js";
|
|
7
10
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
8
|
-
|
|
11
|
+
class BuildFailureError extends Error {
|
|
12
|
+
constructor(args) {
|
|
13
|
+
super(args.message);
|
|
14
|
+
this.name = "BuildFailureError";
|
|
15
|
+
this.file = args.file;
|
|
16
|
+
this.mode = args.mode;
|
|
17
|
+
this.invocation = args.invocation;
|
|
18
|
+
this.stdout = args.stdout;
|
|
19
|
+
this.stderr = args.stderr;
|
|
20
|
+
this.kind = args.kind;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, featureToggles = {}, overrides = {}) {
|
|
9
24
|
const loadedConfig = loadConfig(configPath, false);
|
|
10
25
|
const mode = applyMode(loadedConfig, modeName);
|
|
11
|
-
const config = mode.config;
|
|
26
|
+
const config = Object.assign(Object.create(Object.getPrototypeOf(mode.config)), mode.config);
|
|
27
|
+
config.buildOptions = Object.assign(Object.create(Object.getPrototypeOf(mode.config.buildOptions)), mode.config.buildOptions);
|
|
28
|
+
if (overrides.target) {
|
|
29
|
+
config.buildOptions.target = overrides.target;
|
|
30
|
+
}
|
|
31
|
+
if (overrides.args?.length) {
|
|
32
|
+
config.buildOptions.args = [...config.buildOptions.args, ...overrides.args];
|
|
33
|
+
}
|
|
12
34
|
if (!hasCustomBuildCommand(config)) {
|
|
13
35
|
ensureDeps(config);
|
|
14
36
|
}
|
|
15
37
|
const pkgRunner = getPkgRunner();
|
|
16
38
|
const inputPatterns = resolveInputPatterns(config.input, selectors);
|
|
17
39
|
const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
|
|
18
|
-
const duplicateSpecBasenames =
|
|
40
|
+
const duplicateSpecBasenames = resolveDuplicateBasenames(inputFiles);
|
|
19
41
|
const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
|
|
20
42
|
const buildEnv = {
|
|
21
43
|
...mode.env,
|
|
44
|
+
...config.buildOptions.env,
|
|
22
45
|
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
23
46
|
};
|
|
47
|
+
if (!process.env.AS_TEST_BUILD_API && !hasCustomBuildCommand(config)) {
|
|
48
|
+
const pool = getSerialBuildWorkerPool();
|
|
49
|
+
for (const file of inputFiles) {
|
|
50
|
+
await pool.buildFileMode({
|
|
51
|
+
configPath,
|
|
52
|
+
file,
|
|
53
|
+
modeName,
|
|
54
|
+
featureToggles,
|
|
55
|
+
overrides,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
24
60
|
for (const file of inputFiles) {
|
|
25
61
|
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
|
|
26
62
|
const invocation = getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles);
|
|
27
63
|
try {
|
|
28
|
-
buildFile(invocation, buildEnv);
|
|
64
|
+
await buildFile(invocation, buildEnv);
|
|
29
65
|
}
|
|
30
66
|
catch (error) {
|
|
31
67
|
const modeLabel = modeName ?? "default";
|
|
32
|
-
|
|
68
|
+
const stdout = getBuildStdout(error);
|
|
69
|
+
const stderr = getBuildStderr(error);
|
|
70
|
+
const buildCommand = formatInvocation(invocation);
|
|
71
|
+
const kind = overrides.kind ?? "test";
|
|
72
|
+
const crash = persistCrashRecord(config.fuzz.crashDir, {
|
|
73
|
+
kind,
|
|
74
|
+
stage: "build",
|
|
75
|
+
file,
|
|
76
|
+
mode: modeLabel,
|
|
77
|
+
cwd: process.cwd(),
|
|
78
|
+
buildCommand,
|
|
79
|
+
reproCommand: buildCommand,
|
|
80
|
+
error: stderr || stdout || "unknown build error",
|
|
81
|
+
stdout,
|
|
82
|
+
stderr,
|
|
83
|
+
});
|
|
84
|
+
throw new BuildFailureError({
|
|
85
|
+
file,
|
|
86
|
+
mode: modeLabel,
|
|
87
|
+
invocation,
|
|
88
|
+
stdout,
|
|
89
|
+
stderr,
|
|
90
|
+
kind,
|
|
91
|
+
message: `Failed to build ${path.basename(file)} in mode ${modeLabel} with ${stderr || stdout || "unknown build error"}\n` +
|
|
92
|
+
`Build command: ${buildCommand}\n` +
|
|
93
|
+
`Crash log: ${crash.logPath}`,
|
|
94
|
+
});
|
|
33
95
|
}
|
|
34
96
|
}
|
|
35
97
|
}
|
|
98
|
+
let serialBuildWorkerPool = null;
|
|
99
|
+
function getSerialBuildWorkerPool() {
|
|
100
|
+
if (!serialBuildWorkerPool) {
|
|
101
|
+
serialBuildWorkerPool = new BuildWorkerPool(1);
|
|
102
|
+
}
|
|
103
|
+
return serialBuildWorkerPool;
|
|
104
|
+
}
|
|
105
|
+
export async function closeSerialBuildWorkerPool() {
|
|
106
|
+
if (!serialBuildWorkerPool)
|
|
107
|
+
return;
|
|
108
|
+
const pool = serialBuildWorkerPool;
|
|
109
|
+
serialBuildWorkerPool = null;
|
|
110
|
+
await pool.close();
|
|
111
|
+
}
|
|
112
|
+
export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
|
|
113
|
+
const loadedConfig = loadConfig(configPath, false);
|
|
114
|
+
const mode = applyMode(loadedConfig, modeName);
|
|
115
|
+
const config = Object.assign(Object.create(Object.getPrototypeOf(mode.config)), mode.config);
|
|
116
|
+
config.buildOptions = Object.assign(Object.create(Object.getPrototypeOf(mode.config.buildOptions)), mode.config.buildOptions);
|
|
117
|
+
if (overrides.target) {
|
|
118
|
+
config.buildOptions.target = overrides.target;
|
|
119
|
+
}
|
|
120
|
+
if (overrides.args?.length) {
|
|
121
|
+
config.buildOptions.args = [...config.buildOptions.args, ...overrides.args];
|
|
122
|
+
}
|
|
123
|
+
const duplicateSpecBasenames = resolveDuplicateBasenames([file]);
|
|
124
|
+
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
|
|
125
|
+
return getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
|
|
126
|
+
}
|
|
36
127
|
function hasCustomBuildCommand(config) {
|
|
37
128
|
return !!config.buildOptions.cmd.trim().length;
|
|
38
129
|
}
|
|
@@ -49,13 +140,31 @@ function getBuildCommand(config, pkgRunner, file, outFile, modeName, featureTogg
|
|
|
49
140
|
};
|
|
50
141
|
}
|
|
51
142
|
const defaultArgs = getDefaultBuildArgs(config, featureToggles);
|
|
52
|
-
const
|
|
143
|
+
const ascInvocation = resolveAscInvocation(pkgRunner);
|
|
144
|
+
const args = [...ascInvocation.args, file, ...userArgs, ...defaultArgs];
|
|
53
145
|
if (config.outDir.length) {
|
|
54
146
|
args.push("-o", outFile);
|
|
55
147
|
}
|
|
56
148
|
return {
|
|
57
|
-
command:
|
|
149
|
+
command: ascInvocation.command,
|
|
58
150
|
args,
|
|
151
|
+
apiArgs: args.slice(1),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function resolveAscInvocation(pkgRunner) {
|
|
155
|
+
const assemblyscriptPkg = resolveProjectModule("assemblyscript/package.json");
|
|
156
|
+
if (assemblyscriptPkg) {
|
|
157
|
+
const ascPath = path.join(path.dirname(assemblyscriptPkg), "bin", "asc.js");
|
|
158
|
+
if (existsSync(ascPath)) {
|
|
159
|
+
return {
|
|
160
|
+
command: process.execPath,
|
|
161
|
+
args: [ascPath],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
command: pkgRunner,
|
|
167
|
+
args: ["asc"],
|
|
59
168
|
};
|
|
60
169
|
}
|
|
61
170
|
function getUserBuildArgs(config) {
|
|
@@ -98,9 +207,7 @@ function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames
|
|
|
98
207
|
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
99
208
|
return `${stem}.${disambiguator}${ext}`;
|
|
100
209
|
}
|
|
101
|
-
|
|
102
|
-
const patterns = Array.isArray(configured) ? configured : [configured];
|
|
103
|
-
const files = await glob(patterns);
|
|
210
|
+
function resolveDuplicateBasenames(files) {
|
|
104
211
|
const counts = new Map();
|
|
105
212
|
for (const file of files) {
|
|
106
213
|
const base = path.basename(file);
|
|
@@ -183,21 +290,96 @@ function ensureDeps(config) {
|
|
|
183
290
|
}
|
|
184
291
|
}
|
|
185
292
|
}
|
|
186
|
-
function buildFile(invocation, env) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
293
|
+
async function buildFile(invocation, env) {
|
|
294
|
+
if (process.env.AS_TEST_BUILD_API == "1" && invocation.apiArgs?.length) {
|
|
295
|
+
await buildFileViaApi(invocation.apiArgs, env);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await buildFileViaSpawn(invocation, env);
|
|
299
|
+
}
|
|
300
|
+
async function buildFileViaApi(args, env) {
|
|
301
|
+
const stdoutChunks = [];
|
|
302
|
+
const stderrChunks = [];
|
|
303
|
+
const stdout = createMemoryStream((chunk) => {
|
|
304
|
+
stdoutChunks.push(typeof chunk == "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
305
|
+
});
|
|
306
|
+
const stderr = createMemoryStream((chunk) => {
|
|
307
|
+
stderrChunks.push(typeof chunk == "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
308
|
+
});
|
|
309
|
+
const previousEnv = snapshotEnv();
|
|
310
|
+
applyEnv(env);
|
|
311
|
+
try {
|
|
312
|
+
const result = await ascMain(args, { stdout, stderr });
|
|
313
|
+
if (result.error) {
|
|
314
|
+
const error = result.error;
|
|
315
|
+
error.stderr = stderrChunks.join("").trim();
|
|
316
|
+
error.stdout = stdoutChunks.join("").trim();
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
restoreEnv(previousEnv);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function buildFileViaSpawn(invocation, env) {
|
|
325
|
+
await new Promise((resolve, reject) => {
|
|
326
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
327
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
328
|
+
env,
|
|
329
|
+
shell: false,
|
|
330
|
+
});
|
|
331
|
+
let stdout = "";
|
|
332
|
+
let stderr = "";
|
|
333
|
+
child.stdout?.on("data", (chunk) => {
|
|
334
|
+
stdout += chunk.toString();
|
|
335
|
+
});
|
|
336
|
+
child.stderr?.on("data", (chunk) => {
|
|
337
|
+
stderr += chunk.toString();
|
|
338
|
+
});
|
|
339
|
+
child.on("error", reject);
|
|
340
|
+
child.on("close", (code) => {
|
|
341
|
+
if (code === 0) {
|
|
342
|
+
resolve();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const error = new Error(stderr.trim() || stdout.trim() || `command exited with code ${code}`);
|
|
346
|
+
error.stderr = stderr.trim();
|
|
347
|
+
error.stdout = stdout.trim();
|
|
348
|
+
reject(error);
|
|
349
|
+
});
|
|
192
350
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
351
|
+
}
|
|
352
|
+
function snapshotEnv() {
|
|
353
|
+
return { ...process.env };
|
|
354
|
+
}
|
|
355
|
+
function applyEnv(nextEnv) {
|
|
356
|
+
const keys = new Set([
|
|
357
|
+
...Object.keys(process.env),
|
|
358
|
+
...Object.keys(nextEnv),
|
|
359
|
+
]);
|
|
360
|
+
for (const key of keys) {
|
|
361
|
+
const value = nextEnv[key];
|
|
362
|
+
if (value == undefined) {
|
|
363
|
+
delete process.env[key];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
process.env[key] = value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function restoreEnv(previousEnv) {
|
|
371
|
+
const keys = new Set([
|
|
372
|
+
...Object.keys(process.env),
|
|
373
|
+
...Object.keys(previousEnv),
|
|
374
|
+
]);
|
|
375
|
+
for (const key of keys) {
|
|
376
|
+
const value = previousEnv[key];
|
|
377
|
+
if (value == undefined) {
|
|
378
|
+
delete process.env[key];
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
process.env[key] = value;
|
|
382
|
+
}
|
|
201
383
|
}
|
|
202
384
|
}
|
|
203
385
|
function formatInvocation(invocation) {
|
|
@@ -205,6 +387,7 @@ function formatInvocation(invocation) {
|
|
|
205
387
|
.map((token) => (/\s/.test(token) ? JSON.stringify(token) : token))
|
|
206
388
|
.join(" ");
|
|
207
389
|
}
|
|
390
|
+
export { getBuildCommand, formatInvocation };
|
|
208
391
|
function getBuildStderr(error) {
|
|
209
392
|
const err = error;
|
|
210
393
|
const stderr = err?.stderr;
|
|
@@ -221,6 +404,15 @@ function getBuildStderr(error) {
|
|
|
221
404
|
const message = typeof err?.message == "string" ? err.message.trim() : "";
|
|
222
405
|
return message || "unknown error";
|
|
223
406
|
}
|
|
407
|
+
function getBuildStdout(error) {
|
|
408
|
+
const err = error;
|
|
409
|
+
const stdout = err?.stdout;
|
|
410
|
+
if (typeof stdout == "string")
|
|
411
|
+
return stdout.trim();
|
|
412
|
+
if (stdout instanceof Buffer)
|
|
413
|
+
return stdout.toString("utf8").trim();
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
224
416
|
function getDefaultBuildArgs(config, featureToggles) {
|
|
225
417
|
const buildArgs = [];
|
|
226
418
|
const tryAsEnabled = resolveTryAsEnabled(featureToggles.tryAs);
|
|
@@ -235,7 +427,8 @@ function getDefaultBuildArgs(config, featureToggles) {
|
|
|
235
427
|
buildArgs.push("--use", "AS_TEST_TRY_AS=1");
|
|
236
428
|
}
|
|
237
429
|
// Should also strip any bindings-enabling from asconfig
|
|
238
|
-
if (config.buildOptions.target == "bindings"
|
|
430
|
+
if (config.buildOptions.target == "bindings" ||
|
|
431
|
+
config.buildOptions.target == "web") {
|
|
239
432
|
buildArgs.push("--use", "AS_TEST_BINDINGS=1", "--bindings", "raw", "--exportRuntime", "--exportStart", "_start");
|
|
240
433
|
}
|
|
241
434
|
else if (config.buildOptions.target == "wasi") {
|
|
@@ -246,7 +439,7 @@ function getDefaultBuildArgs(config, featureToggles) {
|
|
|
246
439
|
buildArgs.push("--use", "AS_TEST_WASI=1", "--config", wasiShim.configPath);
|
|
247
440
|
}
|
|
248
441
|
else {
|
|
249
|
-
console.log(`${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not determine target in config! Set target to 'bindings' or 'wasi'`);
|
|
442
|
+
console.log(`${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not determine target in config! Set target to 'bindings', 'web', or 'wasi'`);
|
|
250
443
|
process.exit(1);
|
|
251
444
|
}
|
|
252
445
|
return buildArgs;
|
|
@@ -258,7 +451,7 @@ function resolveTryAsEnabled(override) {
|
|
|
258
451
|
if (override === true && !installed) {
|
|
259
452
|
throw new Error('try-as feature was enabled, but package "try-as" is not installed');
|
|
260
453
|
}
|
|
261
|
-
return
|
|
454
|
+
return false;
|
|
262
455
|
}
|
|
263
456
|
function resolveCoverageEnabled(rawCoverage, override) {
|
|
264
457
|
if (override != undefined)
|
package/bin/commands/build.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { closeSerialBuildWorkerPool, } from "./build-core.js";
|
|
1
2
|
export { build } from "./build-core.js";
|
|
3
|
+
export { formatInvocation, getBuildInvocationPreview } from "./build-core.js";
|
|
2
4
|
export async function executeBuildCommand(rawArgs, configPath, selectedModes, deps) {
|
|
3
5
|
const commandArgs = deps.resolveCommandArgs(rawArgs, "build");
|
|
4
6
|
const listFlags = deps.resolveListFlags(rawArgs, "build");
|
|
5
7
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "build");
|
|
8
|
+
const parallel = deps.resolveBuildParallelJobs(rawArgs);
|
|
6
9
|
const buildFeatureToggles = {
|
|
7
10
|
tryAs: featureToggles.tryAs,
|
|
8
11
|
coverage: featureToggles.coverage,
|
|
@@ -12,5 +15,18 @@ export async function executeBuildCommand(rawArgs, configPath, selectedModes, de
|
|
|
12
15
|
await deps.listExecutionPlan("build", configPath, commandArgs, modeTargets, listFlags);
|
|
13
16
|
return;
|
|
14
17
|
}
|
|
15
|
-
|
|
18
|
+
const previousBuildApi = process.env.AS_TEST_BUILD_API;
|
|
19
|
+
process.env.AS_TEST_BUILD_API = "1";
|
|
20
|
+
try {
|
|
21
|
+
await deps.runBuildModes(configPath, commandArgs, modeTargets, buildFeatureToggles, parallel);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
if (previousBuildApi == undefined) {
|
|
25
|
+
delete process.env.AS_TEST_BUILD_API;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
process.env.AS_TEST_BUILD_API = previousBuildApi;
|
|
29
|
+
}
|
|
30
|
+
await closeSerialBuildWorkerPool();
|
|
31
|
+
}
|
|
16
32
|
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
import { build } from "./build-core.js";
|
|
6
|
+
import { applyMode, loadConfig } from "../util.js";
|
|
7
|
+
import { persistCrashRecord } from "../crash-store.js";
|
|
8
|
+
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
9
|
+
const MAGIC = Buffer.from("WIPC");
|
|
10
|
+
const HEADER_SIZE = 9;
|
|
11
|
+
export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, overrides = {}) {
|
|
12
|
+
const loadedConfig = loadConfig(configPath, false);
|
|
13
|
+
const mode = applyMode(loadedConfig, modeName);
|
|
14
|
+
const config = resolveFuzzConfig(loadedConfig.fuzz, overrides);
|
|
15
|
+
const inputPatterns = resolveFuzzInputPatterns(config.input, selectors);
|
|
16
|
+
const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
|
|
17
|
+
if (!inputFiles.length) {
|
|
18
|
+
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
19
|
+
}
|
|
20
|
+
const duplicateBasenames = resolveDuplicateBasenames(inputFiles);
|
|
21
|
+
const results = [];
|
|
22
|
+
for (const file of inputFiles) {
|
|
23
|
+
const buildStartedAt = Date.now();
|
|
24
|
+
await build(configPath, [file], modeName, { coverage: false }, { target: "bindings", args: ["--use", "AS_TEST_FUZZ=1"], kind: "fuzz" });
|
|
25
|
+
const buildFinishedAt = Date.now();
|
|
26
|
+
const buildTime = buildFinishedAt - buildStartedAt;
|
|
27
|
+
results.push(await runFuzzTarget(file, mode.config.outDir, duplicateBasenames, config, buildStartedAt, buildFinishedAt, buildTime, modeName));
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
function resolveFuzzConfig(raw, overrides) {
|
|
32
|
+
const config = Object.assign({}, raw, overrides);
|
|
33
|
+
if (config.target != "bindings") {
|
|
34
|
+
throw new Error(`fuzz target must be "bindings"; received "${config.target}"`);
|
|
35
|
+
}
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStartedAt, buildFinishedAt, buildTime, modeName) {
|
|
39
|
+
const startedAt = Date.now();
|
|
40
|
+
const artifact = resolveArtifactFileName(file, duplicateBasenames, modeName);
|
|
41
|
+
const wasmPath = path.resolve(process.cwd(), outDir, artifact);
|
|
42
|
+
const jsPath = resolveBindingsHelperPath(wasmPath);
|
|
43
|
+
const helper = await import(pathToFileURL(jsPath).href + `?t=${Date.now()}`);
|
|
44
|
+
const binary = readFileSync(wasmPath);
|
|
45
|
+
const module = new WebAssembly.Module(binary);
|
|
46
|
+
let report = null;
|
|
47
|
+
const captured = captureFrames((type, payload, respond) => {
|
|
48
|
+
if (type == 0x02) {
|
|
49
|
+
const event = JSON.parse(payload.toString("utf8"));
|
|
50
|
+
if (String(event.kind ?? "") == "fuzz:config") {
|
|
51
|
+
respond(`${config.runs}\n${config.seed}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
respond("");
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (type == 0x03) {
|
|
59
|
+
report = JSON.parse(payload.toString("utf8"));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
await helper.instantiate(module, {});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const passthrough = captured.restore();
|
|
67
|
+
const crashMessage = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
68
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
69
|
+
kind: "fuzz",
|
|
70
|
+
file,
|
|
71
|
+
mode: modeName ?? "default",
|
|
72
|
+
seed: config.seed,
|
|
73
|
+
error: crashMessage,
|
|
74
|
+
stdout: passthrough.stdout,
|
|
75
|
+
stderr: "",
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
file,
|
|
79
|
+
target: path.basename(file),
|
|
80
|
+
modeName: modeName ?? "default",
|
|
81
|
+
runs: config.runs,
|
|
82
|
+
crashes: 1,
|
|
83
|
+
crashFiles: [crash.jsonPath],
|
|
84
|
+
seed: config.seed,
|
|
85
|
+
time: Date.now() - startedAt,
|
|
86
|
+
buildTime,
|
|
87
|
+
buildStartedAt,
|
|
88
|
+
buildFinishedAt,
|
|
89
|
+
fuzzers: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const passthrough = captured.restore();
|
|
93
|
+
if (!report?.fuzzers) {
|
|
94
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
95
|
+
kind: "fuzz",
|
|
96
|
+
file,
|
|
97
|
+
mode: modeName ?? "default",
|
|
98
|
+
seed: config.seed,
|
|
99
|
+
error: `missing fuzz report payload from ${path.basename(file)}`,
|
|
100
|
+
stdout: passthrough.stdout,
|
|
101
|
+
stderr: "",
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
file,
|
|
105
|
+
target: path.basename(file),
|
|
106
|
+
modeName: modeName ?? "default",
|
|
107
|
+
runs: config.runs,
|
|
108
|
+
crashes: 1,
|
|
109
|
+
crashFiles: [crash.jsonPath],
|
|
110
|
+
seed: config.seed,
|
|
111
|
+
time: Date.now() - startedAt,
|
|
112
|
+
buildTime,
|
|
113
|
+
buildStartedAt,
|
|
114
|
+
buildFinishedAt,
|
|
115
|
+
fuzzers: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
file,
|
|
120
|
+
target: path.basename(file),
|
|
121
|
+
modeName: modeName ?? "default",
|
|
122
|
+
runs: report.fuzzers.reduce((sum, item) => sum + item.runs, 0),
|
|
123
|
+
crashes: report.fuzzers.reduce((sum, item) => sum + item.crashed, 0),
|
|
124
|
+
crashFiles: [],
|
|
125
|
+
seed: config.seed,
|
|
126
|
+
time: Date.now() - startedAt,
|
|
127
|
+
buildTime,
|
|
128
|
+
buildStartedAt,
|
|
129
|
+
buildFinishedAt,
|
|
130
|
+
fuzzers: report.fuzzers,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function captureFrames(onFrame) {
|
|
134
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
135
|
+
const originalRead = typeof process.stdin.read == "function"
|
|
136
|
+
? process.stdin.read.bind(process.stdin)
|
|
137
|
+
: null;
|
|
138
|
+
let buffer = Buffer.alloc(0);
|
|
139
|
+
let passthrough = Buffer.alloc(0);
|
|
140
|
+
let replies = Buffer.alloc(0);
|
|
141
|
+
function encodeReply(body) {
|
|
142
|
+
const payload = Buffer.from(body, "utf8");
|
|
143
|
+
const header = Buffer.alloc(HEADER_SIZE);
|
|
144
|
+
MAGIC.copy(header, 0);
|
|
145
|
+
header.writeUInt8(0x02, 4);
|
|
146
|
+
header.writeUInt32LE(payload.length, 5);
|
|
147
|
+
return Buffer.concat([header, payload]);
|
|
148
|
+
}
|
|
149
|
+
function dequeueReply(length) {
|
|
150
|
+
const available = Math.min(length, replies.length);
|
|
151
|
+
const view = replies.subarray(0, available);
|
|
152
|
+
replies = replies.subarray(available);
|
|
153
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
154
|
+
}
|
|
155
|
+
process.stdout.write = ((chunk, ...args) => {
|
|
156
|
+
if (!(chunk instanceof ArrayBuffer) && !Buffer.isBuffer(chunk)) {
|
|
157
|
+
return originalWrite(chunk, ...args);
|
|
158
|
+
}
|
|
159
|
+
const incoming = Buffer.from(chunk);
|
|
160
|
+
buffer = Buffer.concat([buffer, incoming]);
|
|
161
|
+
while (true) {
|
|
162
|
+
const index = buffer.indexOf(MAGIC);
|
|
163
|
+
if (index == -1) {
|
|
164
|
+
if (buffer.length) {
|
|
165
|
+
passthrough = Buffer.concat([passthrough, buffer]);
|
|
166
|
+
originalWrite(buffer);
|
|
167
|
+
buffer = Buffer.alloc(0);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (index > 0) {
|
|
172
|
+
const raw = buffer.subarray(0, index);
|
|
173
|
+
passthrough = Buffer.concat([passthrough, raw]);
|
|
174
|
+
originalWrite(raw);
|
|
175
|
+
buffer = buffer.subarray(index);
|
|
176
|
+
}
|
|
177
|
+
if (buffer.length < HEADER_SIZE)
|
|
178
|
+
return true;
|
|
179
|
+
const type = buffer.readUInt8(4);
|
|
180
|
+
const length = buffer.readUInt32LE(5);
|
|
181
|
+
const frameSize = HEADER_SIZE + length;
|
|
182
|
+
if (buffer.length < frameSize)
|
|
183
|
+
return true;
|
|
184
|
+
const payload = buffer.subarray(HEADER_SIZE, frameSize);
|
|
185
|
+
buffer = buffer.subarray(frameSize);
|
|
186
|
+
onFrame(type, payload, (body) => {
|
|
187
|
+
replies = Buffer.concat([replies, encodeReply(body)]);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
process.stdin.read = ((size) => {
|
|
192
|
+
const max = Number(size ?? 0);
|
|
193
|
+
if (max > 0 && replies.length) {
|
|
194
|
+
return dequeueReply(max);
|
|
195
|
+
}
|
|
196
|
+
if (originalRead) {
|
|
197
|
+
return originalRead(size);
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
restore() {
|
|
203
|
+
process.stdout.write = originalWrite;
|
|
204
|
+
if (originalRead) {
|
|
205
|
+
process.stdin.read = originalRead;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
stdout: passthrough.toString("utf8"),
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function resolveFuzzInputPatterns(configured, selectors) {
|
|
214
|
+
const configuredInputs = Array.isArray(configured)
|
|
215
|
+
? configured
|
|
216
|
+
: [configured];
|
|
217
|
+
if (!selectors.length)
|
|
218
|
+
return configuredInputs;
|
|
219
|
+
const patterns = new Set();
|
|
220
|
+
for (const selector of expandSelectors(selectors)) {
|
|
221
|
+
if (!selector)
|
|
222
|
+
continue;
|
|
223
|
+
if (isBareSelector(selector)) {
|
|
224
|
+
const base = selector.replace(/\.fuzz\.ts$/, "").replace(/\.ts$/, "");
|
|
225
|
+
for (const configuredInput of configuredInputs) {
|
|
226
|
+
patterns.add(path.join(path.dirname(configuredInput), `${base}.fuzz.ts`));
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
patterns.add(selector);
|
|
231
|
+
}
|
|
232
|
+
return [...patterns];
|
|
233
|
+
}
|
|
234
|
+
function resolveArtifactFileName(file, duplicateBasenames, modeName) {
|
|
235
|
+
const base = path
|
|
236
|
+
.basename(file)
|
|
237
|
+
.replace(/\.spec\.ts$/, "")
|
|
238
|
+
.replace(/\.ts$/, "");
|
|
239
|
+
const legacy = !modeName
|
|
240
|
+
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
241
|
+
: `${base}.${modeName}.bindings.wasm`;
|
|
242
|
+
if (!duplicateBasenames.has(path.basename(file))) {
|
|
243
|
+
return legacy;
|
|
244
|
+
}
|
|
245
|
+
const disambiguator = resolveDisambiguator(file);
|
|
246
|
+
if (!disambiguator.length) {
|
|
247
|
+
return legacy;
|
|
248
|
+
}
|
|
249
|
+
const ext = path.extname(legacy);
|
|
250
|
+
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
251
|
+
return `${stem}.${disambiguator}${ext}`;
|
|
252
|
+
}
|
|
253
|
+
function resolveDuplicateBasenames(files) {
|
|
254
|
+
const counts = new Map();
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
const base = path.basename(file);
|
|
257
|
+
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
258
|
+
}
|
|
259
|
+
const duplicates = new Set();
|
|
260
|
+
for (const [base, count] of counts) {
|
|
261
|
+
if (count > 1)
|
|
262
|
+
duplicates.add(base);
|
|
263
|
+
}
|
|
264
|
+
return duplicates;
|
|
265
|
+
}
|
|
266
|
+
function resolveDisambiguator(file) {
|
|
267
|
+
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
268
|
+
if (!relDir.length || relDir == ".")
|
|
269
|
+
return "";
|
|
270
|
+
return relDir
|
|
271
|
+
.replace(/[\\/]+/g, "__")
|
|
272
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
273
|
+
.replace(/^_+|_+$/g, "");
|
|
274
|
+
}
|
|
275
|
+
function resolveBindingsHelperPath(wasmPath) {
|
|
276
|
+
const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
|
|
277
|
+
if (existsSync(bindingsPath))
|
|
278
|
+
return bindingsPath;
|
|
279
|
+
const directPath = wasmPath.replace(/\.wasm$/, ".js");
|
|
280
|
+
if (existsSync(directPath))
|
|
281
|
+
return directPath;
|
|
282
|
+
return bindingsPath;
|
|
283
|
+
}
|
|
284
|
+
function expandSelectors(selectors) {
|
|
285
|
+
const expanded = [];
|
|
286
|
+
for (const selector of selectors) {
|
|
287
|
+
if (selector.includes(",") &&
|
|
288
|
+
!selector.includes("/") &&
|
|
289
|
+
!selector.includes("\\") &&
|
|
290
|
+
!/[*?[\]{}]/.test(selector)) {
|
|
291
|
+
for (const token of selector.split(",")) {
|
|
292
|
+
const trimmed = token.trim();
|
|
293
|
+
if (trimmed.length)
|
|
294
|
+
expanded.push(trimmed);
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
expanded.push(selector);
|
|
299
|
+
}
|
|
300
|
+
return expanded;
|
|
301
|
+
}
|
|
302
|
+
function isBareSelector(selector) {
|
|
303
|
+
return (!selector.includes("/") &&
|
|
304
|
+
!selector.includes("\\") &&
|
|
305
|
+
!/[*?[\]{}]/.test(selector));
|
|
306
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function executeFuzzCommand(rawArgs, configPath, selectedModes, deps) {
|
|
2
|
+
const commandArgs = deps.resolveCommandArgs(rawArgs, "fuzz");
|
|
3
|
+
const listFlags = deps.resolveListFlags(rawArgs, "fuzz");
|
|
4
|
+
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
5
|
+
if (listFlags.list || listFlags.listModes) {
|
|
6
|
+
await deps.listExecutionPlan("fuzz", configPath, commandArgs, modeTargets, listFlags);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
await deps.runFuzzModes(configPath, commandArgs, modeTargets, rawArgs);
|
|
10
|
+
}
|