as-test 1.0.5 → 1.0.6
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 +10 -0
- package/README.md +2 -0
- package/assembly/src/fuzz.ts +68 -10
- package/bin/commands/fuzz-core.js +44 -1
- package/bin/crash-store.js +12 -1
- package/bin/reporters/default.js +19 -3
- package/bin/reporters/tap.js +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 2026-03-31 - v1.0.6
|
|
4
|
+
|
|
5
|
+
### Fuzzing
|
|
6
|
+
|
|
7
|
+
- feat: print exact failing fuzz seeds and one-run repro commands on logical fuzz failures, and persist captured `run(...)` inputs in `.as-test/crashes` so side-effectful generators still leave behind replayable failure data.
|
|
8
|
+
|
|
9
|
+
### Reporting
|
|
10
|
+
|
|
11
|
+
- fix: correct reporter override output behavior.
|
|
12
|
+
|
|
3
13
|
## 2026-03-30 - v1.0.5
|
|
4
14
|
|
|
5
15
|
### CLI
|
package/README.md
CHANGED
|
@@ -231,6 +231,8 @@ If you used `npx ast init` with a fuzzer example, the config is already there. O
|
|
|
231
231
|
|
|
232
232
|
`ast fuzz` runs fuzz files across the selected modes, reports one result per file, and keeps the final summary separate from the normal test totals. If you want one combined command, use `ast test --fuzz`.
|
|
233
233
|
|
|
234
|
+
When a fuzzer fails, `as-test` now prints the exact failing seeds and one-run repro commands such as `ast fuzz ... --seed <seed+n> --runs 1`. Crash records in `.as-test/crashes` also include the captured inputs passed to `run(...)`, which helps when the generator itself has side effects.
|
|
235
|
+
|
|
234
236
|
Run only fuzzers:
|
|
235
237
|
|
|
236
238
|
```bash
|
package/assembly/src/fuzz.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { quote } from "../util/json";
|
|
1
|
+
import { quote, rawOrNull, stringifyValue } from "../util/json";
|
|
2
2
|
|
|
3
3
|
export class StringOptions {
|
|
4
4
|
charset: string = "ascii";
|
|
@@ -211,6 +211,7 @@ export class FuzzerResult {
|
|
|
211
211
|
public failureLeft: string = "";
|
|
212
212
|
public failureRight: string = "";
|
|
213
213
|
public failureMessage: string = "";
|
|
214
|
+
public failures: FuzzFailure[] = [];
|
|
214
215
|
|
|
215
216
|
serialize(): string {
|
|
216
217
|
return (
|
|
@@ -238,7 +239,27 @@ export class FuzzerResult {
|
|
|
238
239
|
quote(this.failureRight) +
|
|
239
240
|
',"message":' +
|
|
240
241
|
quote(this.failureMessage) +
|
|
241
|
-
|
|
242
|
+
'},"failures":' +
|
|
243
|
+
serializeFuzzFailures(this.failures) +
|
|
244
|
+
"}"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export class FuzzFailure {
|
|
250
|
+
public run: i32 = 0;
|
|
251
|
+
public seed: u64 = 0;
|
|
252
|
+
public input: string = "";
|
|
253
|
+
|
|
254
|
+
serialize(): string {
|
|
255
|
+
return (
|
|
256
|
+
'{"run":' +
|
|
257
|
+
this.run.toString() +
|
|
258
|
+
',"seed":' +
|
|
259
|
+
this.seed.toString() +
|
|
260
|
+
',"input":' +
|
|
261
|
+
rawOrNull(this.input) +
|
|
262
|
+
"}"
|
|
242
263
|
);
|
|
243
264
|
}
|
|
244
265
|
}
|
|
@@ -329,7 +350,7 @@ function createSkippedResult(name: string): FuzzerResult {
|
|
|
329
350
|
return result;
|
|
330
351
|
}
|
|
331
352
|
|
|
332
|
-
function recordResult(result: FuzzerResult): void {
|
|
353
|
+
function recordResult(result: FuzzerResult, run: i32, seed: u64): void {
|
|
333
354
|
if (__as_test_fuzz_failed) {
|
|
334
355
|
result.failed++;
|
|
335
356
|
if (!result.failureInstr.length && __as_test_fuzz_failure_instr.length) {
|
|
@@ -338,6 +359,11 @@ function recordResult(result: FuzzerResult): void {
|
|
|
338
359
|
result.failureRight = __as_test_fuzz_failure_right;
|
|
339
360
|
result.failureMessage = __as_test_fuzz_failure_message;
|
|
340
361
|
}
|
|
362
|
+
const failure = new FuzzFailure();
|
|
363
|
+
failure.run = run;
|
|
364
|
+
failure.seed = seed;
|
|
365
|
+
failure.input = __as_test_fuzz_input;
|
|
366
|
+
result.failures.push(failure);
|
|
341
367
|
} else {
|
|
342
368
|
result.passed++;
|
|
343
369
|
}
|
|
@@ -389,7 +415,7 @@ export class Fuzzer0<R> extends FuzzerBase {
|
|
|
389
415
|
"fuzz generator must call run() exactly once",
|
|
390
416
|
);
|
|
391
417
|
}
|
|
392
|
-
recordResult(result);
|
|
418
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
393
419
|
}
|
|
394
420
|
__fuzz_callback0 = null;
|
|
395
421
|
result.timeEnd = performance.now();
|
|
@@ -438,7 +464,10 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
438
464
|
"fuzzers with arguments must call .generate(...)",
|
|
439
465
|
);
|
|
440
466
|
} else {
|
|
441
|
-
this.generator(seed,
|
|
467
|
+
this.generator(seed, (a: A): R => {
|
|
468
|
+
__as_test_fuzz_input = "[" + stringifyValue<A>(a) + "]";
|
|
469
|
+
return changetype<(a: A) => R>(__fuzz_run1)(a);
|
|
470
|
+
});
|
|
442
471
|
}
|
|
443
472
|
if (__fuzz_calls != 1) {
|
|
444
473
|
failFuzzIteration(
|
|
@@ -448,7 +477,7 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
448
477
|
"fuzz generator must call run() exactly once",
|
|
449
478
|
);
|
|
450
479
|
}
|
|
451
|
-
recordResult(result);
|
|
480
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
452
481
|
}
|
|
453
482
|
__fuzz_callback1 = null;
|
|
454
483
|
result.timeEnd = performance.now();
|
|
@@ -500,7 +529,11 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
500
529
|
"fuzzers with arguments must call .generate(...)",
|
|
501
530
|
);
|
|
502
531
|
} else {
|
|
503
|
-
this.generator(seed,
|
|
532
|
+
this.generator(seed, (a: A, b: B): R => {
|
|
533
|
+
__as_test_fuzz_input =
|
|
534
|
+
"[" + stringifyValue<A>(a) + "," + stringifyValue<B>(b) + "]";
|
|
535
|
+
return changetype<(a: A, b: B) => R>(__fuzz_run2)(a, b);
|
|
536
|
+
});
|
|
504
537
|
}
|
|
505
538
|
if (__fuzz_calls != 1) {
|
|
506
539
|
failFuzzIteration(
|
|
@@ -510,7 +543,7 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
510
543
|
"fuzz generator must call run() exactly once",
|
|
511
544
|
);
|
|
512
545
|
}
|
|
513
|
-
recordResult(result);
|
|
546
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
514
547
|
}
|
|
515
548
|
__fuzz_callback2 = null;
|
|
516
549
|
result.timeEnd = performance.now();
|
|
@@ -564,7 +597,17 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
564
597
|
} else {
|
|
565
598
|
changetype<(seed: FuzzSeed, run: (a: A, b: B, c: C) => R) => void>(
|
|
566
599
|
this.generator,
|
|
567
|
-
)(seed,
|
|
600
|
+
)(seed, (a: A, b: B, c: C): R => {
|
|
601
|
+
__as_test_fuzz_input =
|
|
602
|
+
"[" +
|
|
603
|
+
stringifyValue<A>(a) +
|
|
604
|
+
"," +
|
|
605
|
+
stringifyValue<B>(b) +
|
|
606
|
+
"," +
|
|
607
|
+
stringifyValue<C>(c) +
|
|
608
|
+
"]";
|
|
609
|
+
return changetype<(a: A, b: B, c: C) => R>(__fuzz_run3)(a, b, c);
|
|
610
|
+
});
|
|
568
611
|
}
|
|
569
612
|
if (__fuzz_calls != 1) {
|
|
570
613
|
failFuzzIteration(
|
|
@@ -574,7 +617,7 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
574
617
|
"fuzz generator must call run() exactly once",
|
|
575
618
|
);
|
|
576
619
|
}
|
|
577
|
-
recordResult(result);
|
|
620
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
578
621
|
}
|
|
579
622
|
__fuzz_callback3 = null;
|
|
580
623
|
result.timeEnd = performance.now();
|
|
@@ -694,6 +737,8 @@ function containsFloatValue<T>(values: T[], needle: T): bool {
|
|
|
694
737
|
@global export let __as_test_fuzz_failure_right: string = "";
|
|
695
738
|
// @ts-ignore
|
|
696
739
|
@global export let __as_test_fuzz_failure_message: string = "";
|
|
740
|
+
// @ts-ignore
|
|
741
|
+
@global export let __as_test_fuzz_input: string = "";
|
|
697
742
|
|
|
698
743
|
export function prepareFuzzIteration(): void {
|
|
699
744
|
__as_test_fuzz_failed = false;
|
|
@@ -701,6 +746,7 @@ export function prepareFuzzIteration(): void {
|
|
|
701
746
|
__as_test_fuzz_failure_left = "";
|
|
702
747
|
__as_test_fuzz_failure_right = "";
|
|
703
748
|
__as_test_fuzz_failure_message = "";
|
|
749
|
+
__as_test_fuzz_input = "[]";
|
|
704
750
|
}
|
|
705
751
|
|
|
706
752
|
export function failFuzzIteration(
|
|
@@ -721,3 +767,15 @@ export function failFuzzIteration(
|
|
|
721
767
|
function panic(): void {
|
|
722
768
|
unreachable();
|
|
723
769
|
}
|
|
770
|
+
|
|
771
|
+
function serializeFuzzFailures(values: FuzzFailure[]): string {
|
|
772
|
+
if (!values.length) return "[]";
|
|
773
|
+
|
|
774
|
+
let out = "[";
|
|
775
|
+
for (let i = 0; i < values.length; i++) {
|
|
776
|
+
if (i) out += ",";
|
|
777
|
+
out += unchecked(values[i]).serialize();
|
|
778
|
+
}
|
|
779
|
+
out += "]";
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
@@ -68,6 +68,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
68
68
|
const crash = persistCrashRecord(config.crashDir, {
|
|
69
69
|
kind: "fuzz",
|
|
70
70
|
file,
|
|
71
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
71
72
|
mode: modeName ?? "default",
|
|
72
73
|
seed: config.seed,
|
|
73
74
|
error: crashMessage,
|
|
@@ -94,6 +95,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
94
95
|
const crash = persistCrashRecord(config.crashDir, {
|
|
95
96
|
kind: "fuzz",
|
|
96
97
|
file,
|
|
98
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
97
99
|
mode: modeName ?? "default",
|
|
98
100
|
seed: config.seed,
|
|
99
101
|
error: `missing fuzz report payload from ${path.basename(file)}`,
|
|
@@ -115,13 +117,37 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
115
117
|
fuzzers: [],
|
|
116
118
|
};
|
|
117
119
|
}
|
|
120
|
+
const crashFiles = [];
|
|
121
|
+
for (const fuzzer of report.fuzzers) {
|
|
122
|
+
if (fuzzer.failed <= 0 && fuzzer.crashed <= 0)
|
|
123
|
+
continue;
|
|
124
|
+
const firstFailureSeed = typeof fuzzer.failures?.[0]?.seed == "number"
|
|
125
|
+
? fuzzer.failures[0].seed
|
|
126
|
+
: config.seed;
|
|
127
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
128
|
+
kind: "fuzz",
|
|
129
|
+
file,
|
|
130
|
+
entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
|
|
131
|
+
mode: modeName ?? "default",
|
|
132
|
+
seed: firstFailureSeed,
|
|
133
|
+
reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", 1),
|
|
134
|
+
error: fuzzer.failure?.message ||
|
|
135
|
+
`fuzz failure in ${fuzzer.name} after ${fuzzer.runs} runs`,
|
|
136
|
+
stdout: passthrough.stdout,
|
|
137
|
+
stderr: "",
|
|
138
|
+
failure: fuzzer.failure,
|
|
139
|
+
failures: fuzzer.failures,
|
|
140
|
+
});
|
|
141
|
+
crashFiles.push(crash.jsonPath);
|
|
142
|
+
fuzzer.crashFile = crash.jsonPath;
|
|
143
|
+
}
|
|
118
144
|
return {
|
|
119
145
|
file,
|
|
120
146
|
target: path.basename(file),
|
|
121
147
|
modeName: modeName ?? "default",
|
|
122
148
|
runs: report.fuzzers.reduce((sum, item) => sum + item.runs, 0),
|
|
123
149
|
crashes: report.fuzzers.reduce((sum, item) => sum + item.crashed, 0),
|
|
124
|
-
crashFiles
|
|
150
|
+
crashFiles,
|
|
125
151
|
seed: config.seed,
|
|
126
152
|
time: Date.now() - startedAt,
|
|
127
153
|
buildTime,
|
|
@@ -130,6 +156,23 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
130
156
|
fuzzers: report.fuzzers,
|
|
131
157
|
};
|
|
132
158
|
}
|
|
159
|
+
function buildFuzzReproCommand(file, seed, modeName, runs) {
|
|
160
|
+
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
161
|
+
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
162
|
+
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
163
|
+
}
|
|
164
|
+
function buildFuzzFailureEntryKey(file, name, modeName) {
|
|
165
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
|
|
166
|
+
}
|
|
167
|
+
function buildFuzzCrashEntryKey(file, modeName) {
|
|
168
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}`;
|
|
169
|
+
}
|
|
170
|
+
function sanitizeEntryName(name) {
|
|
171
|
+
return name
|
|
172
|
+
.toLowerCase()
|
|
173
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
174
|
+
.replace(/^-+|-+$/g, "") || "fuzzer";
|
|
175
|
+
}
|
|
133
176
|
function captureFrames(onFrame) {
|
|
134
177
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
135
178
|
const originalRead = typeof process.stdin.read == "function"
|
package/bin/crash-store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
export function persistCrashRecord(rootDir, record) {
|
|
4
|
-
const entry = crashEntryKey(record.file);
|
|
4
|
+
const entry = record.entryKey?.length ? record.entryKey : crashEntryKey(record.file);
|
|
5
5
|
const dir = path.resolve(process.cwd(), rootDir);
|
|
6
6
|
mkdirSync(dir, { recursive: true });
|
|
7
7
|
const jsonPath = path.join(dir, `${entry}.json`);
|
|
@@ -53,6 +53,17 @@ function buildCrashLog(payload) {
|
|
|
53
53
|
lines.push(`message: ${payload.failure.message}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
if (payload.failures?.length) {
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push("[fuzz-failures]");
|
|
59
|
+
for (const failure of payload.failures) {
|
|
60
|
+
lines.push(`run: ${failure.run}`);
|
|
61
|
+
lines.push(`seed: ${failure.seed}`);
|
|
62
|
+
if (failure.input)
|
|
63
|
+
lines.push(`input: ${JSON.stringify(failure.input)}`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
56
67
|
lines.push("");
|
|
57
68
|
lines.push("[stdout]");
|
|
58
69
|
lines.push(payload.stdout ?? "");
|
package/bin/reporters/default.js
CHANGED
|
@@ -360,7 +360,19 @@ function renderFailedFuzzers(results) {
|
|
|
360
360
|
console.log(chalk.dim(`Runs: ${fuzzer.passed + fuzzer.failed + fuzzer.crashed} completed (${fuzzer.passed} passed, ${fuzzer.failed} failed, ${fuzzer.crashed} crashed)`));
|
|
361
361
|
console.log(chalk.dim(`Repro: ${repro}`));
|
|
362
362
|
console.log(chalk.dim(`Seed: ${modeResult.seed}`));
|
|
363
|
-
if (
|
|
363
|
+
if (fuzzer.failures?.length) {
|
|
364
|
+
console.log(chalk.dim(`Failing seeds: ${formatFailingSeeds(fuzzer)}`));
|
|
365
|
+
for (const failure of fuzzer.failures) {
|
|
366
|
+
console.log(chalk.dim(`Repro ${failure.run + 1}: ${buildFuzzReproCommand(relativeFile, failure.seed, modeResult.modeName, 1)}`));
|
|
367
|
+
if (failure.input) {
|
|
368
|
+
console.log(chalk.dim(`Input ${failure.run + 1}: ${JSON.stringify(failure.input)}`));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (fuzzer.crashFile?.length) {
|
|
373
|
+
console.log(chalk.dim(`Crash: ${fuzzer.crashFile}`));
|
|
374
|
+
}
|
|
375
|
+
else if (modeResult.crashFiles.length) {
|
|
364
376
|
console.log(chalk.dim(`Crash: ${modeResult.crashFiles[0]}`));
|
|
365
377
|
}
|
|
366
378
|
console.log("");
|
|
@@ -392,9 +404,13 @@ function averageFuzzModeTime(results) {
|
|
|
392
404
|
return 0;
|
|
393
405
|
return results.reduce((sum, result) => sum + result.time, 0) / results.length;
|
|
394
406
|
}
|
|
395
|
-
function buildFuzzReproCommand(file, seed, modeName) {
|
|
407
|
+
function buildFuzzReproCommand(file, seed, modeName, runs) {
|
|
396
408
|
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
397
|
-
|
|
409
|
+
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
410
|
+
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
411
|
+
}
|
|
412
|
+
function formatFailingSeeds(fuzzer) {
|
|
413
|
+
return (fuzzer.failures ?? []).map((failure) => String(failure.seed)).join(", ");
|
|
398
414
|
}
|
|
399
415
|
function toRelativeResultPath(file) {
|
|
400
416
|
const relative = path.relative(process.cwd(), path.resolve(process.cwd(), file));
|
package/bin/reporters/tap.js
CHANGED
|
@@ -221,10 +221,13 @@ function buildFuzzerFailureMessage(result, fuzzer) {
|
|
|
221
221
|
if (fuzzer.crashed > 0 || result.crashes > 0) {
|
|
222
222
|
return buildFuzzMessage(result.runs, result.seed, result.crashFiles[0]);
|
|
223
223
|
}
|
|
224
|
+
const failureSeeds = fuzzer.failures?.length
|
|
225
|
+
? `, failing seeds ${fuzzer.failures.map((failure) => failure.seed).join(", ")}`
|
|
226
|
+
: "";
|
|
224
227
|
if (fuzzer.failure?.message?.length) {
|
|
225
|
-
return `${fuzzer.failure.message} (runs ${fuzzer.runs}, seed ${result.seed})`;
|
|
228
|
+
return `${fuzzer.failure.message} (runs ${fuzzer.runs}, seed ${result.seed}${failureSeeds})`;
|
|
226
229
|
}
|
|
227
|
-
return `fuzz failed after ${fuzzer.runs} runs (seed ${result.seed})`;
|
|
230
|
+
return `fuzz failed after ${fuzzer.runs} runs (seed ${result.seed}${failureSeeds})`;
|
|
228
231
|
}
|
|
229
232
|
function buildFuzzMessage(runs, seed, crashFile) {
|
|
230
233
|
const crashSuffix = crashFile?.length ? `, crash ${crashFile}` : "";
|