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 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
@@ -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, changetype<(a: A) => R>(__fuzz_run1));
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, changetype<(a: A, b: B) => R>(__fuzz_run2));
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, changetype<(a: A, b: B, c: C) => R>(__fuzz_run3));
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"
@@ -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 ?? "");
@@ -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 (modeResult.crashFiles.length) {
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
- return `ast fuzz ${file}${modeArg} --seed ${seed}`;
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));
@@ -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}` : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",