as-test 1.0.12 → 1.0.14

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,18 @@
1
1
  # Change Log
2
2
 
3
+ ## 2026-05-04 - v1.0.14
4
+
5
+ ### Fuzzing
6
+
7
+ - feat: add `FuzzSeed` helpers for `i8`, `u8`, `i16`, `u16`, `i64`, `u64`, and `bool()`.
8
+ - feat: make integer `FuzzSeed` helpers default to the full range of their target type when no options are provided, instead of collapsing to `0`.
9
+ - perf: add unchecked full-range fast paths for default integer seed generation while keeping explicit user-provided ranges validated.
10
+
11
+ ## 2025-05-03 - v1.0.13
12
+
13
+ - feat: add `--fuzzer` / `--fuzzers` filtering for `ast fuzz` and `ast test --fuzz`, accept `--suite` / `--suites` as fuzz aliases, and include target-specific repro commands in fuzz failure output.
14
+ - feat: add `--suite` / `--suites` filtering for `ast run` and `ast test`, and print suite-specific repro commands on failing test assertions.
15
+
3
16
  ## 2026-04-28 - v1.0.12
4
17
 
5
18
  - perf: faster seed generation
package/README.md CHANGED
@@ -134,6 +134,13 @@ Run one matching file:
134
134
  npx ast test math
135
135
  ```
136
136
 
137
+ Re-run one suite inside a matching file:
138
+
139
+ ```bash
140
+ npx ast run math --suite array-check
141
+ npx ast run math --suite array-manipulation/array-check
142
+ ```
143
+
137
144
  You do not need to learn every CLI flag to get started. Most projects can begin with `npx ast test`, then add more configuration only when they need it.
138
145
 
139
146
  ## Mocking
@@ -286,6 +293,12 @@ Run only fuzzers:
286
293
  npx ast fuzz
287
294
  ```
288
295
 
296
+ Run one matching fuzz target:
297
+
298
+ ```bash
299
+ npx ast fuzz string --fuzzer ascii-strings-survive-concatenation-boundaries
300
+ ```
301
+
289
302
  Run tests and fuzzers together:
290
303
 
291
304
  ```bash
@@ -352,6 +365,36 @@ If you want to keep more than one runtime around, use modes:
352
365
  }
353
366
  ```
354
367
 
368
+ Modes can also be full config objects. That means a mode can override fuzzing, input globs, output aliases, runtime, build flags, and the rest of the normal config surface:
369
+
370
+ ```json
371
+ {
372
+ "modes": {
373
+ "web": {
374
+ "fuzz": {
375
+ "input": ["./assembly/__fuzz__/web/*.fuzz.ts"],
376
+ "runs": 200
377
+ },
378
+ "runOptions": {
379
+ "runtime": {
380
+ "browser": "chromium"
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ ```
387
+
388
+ If you prefer to keep one mode in a separate file, point the mode directly at that config file:
389
+
390
+ ```json
391
+ {
392
+ "modes": {
393
+ "simd": "./as-test.config.simd.json"
394
+ }
395
+ }
396
+ ```
397
+
355
398
  Run a specific mode with:
356
399
 
357
400
  ```bash
@@ -294,11 +294,43 @@
294
294
  },
295
295
  "modes": {
296
296
  "type": "object",
297
- "description": "Named build/run modes. Each mode can override target/args/runtime/env and artifact directories.",
297
+ "description": "Named build/run modes. Each mode can be a config object or a path to another as-test config file.",
298
298
  "additionalProperties": {
299
- "type": "object",
300
- "additionalProperties": false,
301
- "properties": {
299
+ "oneOf": [
300
+ {
301
+ "type": "string",
302
+ "description": "Path to another as-test config file used as the selected mode's override config."
303
+ },
304
+ {
305
+ "type": "object",
306
+ "additionalProperties": false,
307
+ "properties": {
308
+ "$schema": {
309
+ "type": "string"
310
+ },
311
+ "input": {
312
+ "type": "array",
313
+ "items": {
314
+ "type": "string"
315
+ }
316
+ },
317
+ "output": {
318
+ "oneOf": [
319
+ {
320
+ "type": "string"
321
+ },
322
+ {
323
+ "type": "object",
324
+ "additionalProperties": false,
325
+ "properties": {
326
+ "build": { "type": "string" },
327
+ "logs": { "type": "string" },
328
+ "coverage": { "type": "string" },
329
+ "snapshots": { "type": "string" }
330
+ }
331
+ }
332
+ ]
333
+ },
302
334
  "outDir": {
303
335
  "type": "string",
304
336
  "description": "Mode-specific build output directory. If omitted, defaults to <outDir>/<mode-name>."
@@ -339,6 +371,38 @@
339
371
  }
340
372
  ]
341
373
  },
374
+ "fuzz": {
375
+ "type": "object",
376
+ "additionalProperties": false,
377
+ "description": "Mode-specific fuzz overrides applied before running `ast fuzz` or `ast test --fuzz` in this mode.",
378
+ "properties": {
379
+ "input": {
380
+ "type": "array",
381
+ "items": {
382
+ "type": "string"
383
+ }
384
+ },
385
+ "runs": {
386
+ "type": "number"
387
+ },
388
+ "seed": {
389
+ "type": "number"
390
+ },
391
+ "maxInputBytes": {
392
+ "type": "number"
393
+ },
394
+ "target": {
395
+ "type": "string",
396
+ "enum": ["bindings"]
397
+ },
398
+ "corpusDir": {
399
+ "type": "string"
400
+ },
401
+ "crashDir": {
402
+ "type": "string"
403
+ }
404
+ }
405
+ },
342
406
  "buildOptions": {
343
407
  "type": "object",
344
408
  "additionalProperties": false,
@@ -469,7 +533,9 @@
469
533
  }
470
534
  ]
471
535
  }
472
- }
536
+ }
537
+ }
538
+ ]
473
539
  },
474
540
  "default": {}
475
541
  },
@@ -45,8 +45,16 @@ declare module "as-test" {
45
45
  }
46
46
 
47
47
  export interface FuzzSeed {
48
+ boolean(): boolean;
49
+ bool(): boolean;
50
+ i8(options?: IntellisenseIntegerOptions): number;
51
+ u8(options?: IntellisenseIntegerOptions): number;
52
+ i16(options?: IntellisenseIntegerOptions): number;
53
+ u16(options?: IntellisenseIntegerOptions): number;
48
54
  i32(options?: IntellisenseIntegerOptions): number;
49
55
  u32(options?: IntellisenseIntegerOptions): number;
56
+ i64(options?: IntellisenseIntegerOptions): number;
57
+ u64(options?: IntellisenseIntegerOptions): number;
50
58
  f32(options?: IntellisenseFloatOptions): number;
51
59
  f64(options?: IntellisenseFloatOptions): number;
52
60
  bytes(options?: IntellisenseBytesOptions): Uint8Array;
@@ -34,13 +34,20 @@ export class ArrayOptions {
34
34
  max: i32 = 16;
35
35
  }
36
36
 
37
- const DEFAULT_I32_OPTIONS = new IntegerOptions<i32>();
38
- const DEFAULT_U32_OPTIONS = new IntegerOptions<u32>();
39
37
  const DEFAULT_F32_OPTIONS = new FloatOptions<f32>();
40
38
  const DEFAULT_F64_OPTIONS = new FloatOptions<f64>();
41
39
  const DEFAULT_STRING_OPTIONS = new StringOptions();
42
40
  const DEFAULT_BYTES_OPTIONS = new BytesOptions();
43
41
  const DEFAULT_ARRAY_OPTIONS = new ArrayOptions();
42
+ const I64_SIGN_MASK: u64 = 0x8000000000000000;
43
+ const EMPTY_I8_EXCLUDE: i8[] = [];
44
+ const EMPTY_U8_EXCLUDE: u8[] = [];
45
+ const EMPTY_I16_EXCLUDE: i16[] = [];
46
+ const EMPTY_U16_EXCLUDE: u16[] = [];
47
+ const EMPTY_I32_EXCLUDE: i32[] = [];
48
+ const EMPTY_U32_EXCLUDE: u32[] = [];
49
+ const EMPTY_I64_EXCLUDE: i64[] = [];
50
+ const EMPTY_U64_EXCLUDE: u64[] = [];
44
51
 
45
52
  export class FuzzSeed {
46
53
  private state: u32;
@@ -61,19 +68,99 @@ export class FuzzSeed {
61
68
  return (this.nextU32() & 1) == 1;
62
69
  }
63
70
 
71
+ bool(): bool {
72
+ return this.boolean();
73
+ }
74
+
64
75
  pick<T>(values: T[]): T {
65
76
  if (!values.length) panic();
66
77
  return unchecked(values[this.nextRange(0, values.length - 1)]);
67
78
  }
68
79
 
80
+ i8(options: IntegerOptions<i8> | null = null): i8 {
81
+ if (options == null) {
82
+ return this.nextI8InRange(i8.MIN_VALUE, i8.MAX_VALUE, EMPTY_I8_EXCLUDE, false);
83
+ }
84
+ return this.nextI8InRange(options.min, options.max, options.exclude, true);
85
+ }
86
+
87
+ u8(options: IntegerOptions<u8> | null = null): u8 {
88
+ if (options == null) {
89
+ return this.nextU8InRange(u8.MIN_VALUE, u8.MAX_VALUE, EMPTY_U8_EXCLUDE, false);
90
+ }
91
+ return this.nextU8InRange(options.min, options.max, options.exclude, true);
92
+ }
93
+
94
+ i16(options: IntegerOptions<i16> | null = null): i16 {
95
+ if (options == null) {
96
+ return this.nextI16InRange(
97
+ i16.MIN_VALUE,
98
+ i16.MAX_VALUE,
99
+ EMPTY_I16_EXCLUDE,
100
+ false,
101
+ );
102
+ }
103
+ return this.nextI16InRange(options.min, options.max, options.exclude, true);
104
+ }
105
+
106
+ u16(options: IntegerOptions<u16> | null = null): u16 {
107
+ if (options == null) {
108
+ return this.nextU16InRange(
109
+ u16.MIN_VALUE,
110
+ u16.MAX_VALUE,
111
+ EMPTY_U16_EXCLUDE,
112
+ false,
113
+ );
114
+ }
115
+ return this.nextU16InRange(options.min, options.max, options.exclude, true);
116
+ }
117
+
69
118
  i32(options: IntegerOptions<i32> | null = null): i32 {
70
- const config = options != null ? options : DEFAULT_I32_OPTIONS;
71
- return this.nextI32InRange(config.min, config.max, config.exclude);
119
+ if (options == null) {
120
+ return this.nextI32InRange(
121
+ i32.MIN_VALUE,
122
+ i32.MAX_VALUE,
123
+ EMPTY_I32_EXCLUDE,
124
+ false,
125
+ );
126
+ }
127
+ return this.nextI32InRange(options.min, options.max, options.exclude, true);
72
128
  }
73
129
 
74
130
  u32(options: IntegerOptions<u32> | null = null): u32 {
75
- const config = options != null ? options : DEFAULT_U32_OPTIONS;
76
- return this.nextU32InRange(config.min, config.max, config.exclude);
131
+ if (options == null) {
132
+ return this.nextU32InRange(
133
+ u32.MIN_VALUE,
134
+ u32.MAX_VALUE,
135
+ EMPTY_U32_EXCLUDE,
136
+ false,
137
+ );
138
+ }
139
+ return this.nextU32InRange(options.min, options.max, options.exclude, true);
140
+ }
141
+
142
+ i64(options: IntegerOptions<i64> | null = null): i64 {
143
+ if (options == null) {
144
+ return this.nextI64InRange(
145
+ i64.MIN_VALUE,
146
+ i64.MAX_VALUE,
147
+ EMPTY_I64_EXCLUDE,
148
+ false,
149
+ );
150
+ }
151
+ return this.nextI64InRange(options.min, options.max, options.exclude, true);
152
+ }
153
+
154
+ u64(options: IntegerOptions<u64> | null = null): u64 {
155
+ if (options == null) {
156
+ return this.nextU64InRange(
157
+ u64.MIN_VALUE,
158
+ u64.MAX_VALUE,
159
+ EMPTY_U64_EXCLUDE,
160
+ false,
161
+ );
162
+ }
163
+ return this.nextU64InRange(options.min, options.max, options.exclude, true);
77
164
  }
78
165
 
79
166
  f32(options: FloatOptions<f32> | null = null): f32 {
@@ -201,8 +288,16 @@ export class FuzzSeed {
201
288
  return 0;
202
289
  }
203
290
 
204
- private nextI32InRange(min: i32, max: i32, exclude: i32[]): i32 {
205
- if (max < min) panic();
291
+ private nextI32InRange(
292
+ min: i32,
293
+ max: i32,
294
+ exclude: i32[],
295
+ validateRange: bool = true,
296
+ ): i32 {
297
+ if (validateRange && max < min) panic();
298
+ if (!validateRange && min == i32.MIN_VALUE && max == i32.MAX_VALUE) {
299
+ return <i32>this.nextU32();
300
+ }
206
301
  if (!exclude.length) {
207
302
  return max <= min ? min : min + <i32>(this.nextU32() % <u32>(max - min + 1));
208
303
  }
@@ -215,8 +310,120 @@ export class FuzzSeed {
215
310
  return min;
216
311
  }
217
312
 
218
- private nextU32InRange(min: u32, max: u32, exclude: u32[]): u32 {
219
- if (max < min) panic();
313
+ private nextI8InRange(
314
+ min: i8,
315
+ max: i8,
316
+ exclude: i8[],
317
+ validateRange: bool = true,
318
+ ): i8 {
319
+ if (validateRange && max < min) panic();
320
+ if (!validateRange && min == i8.MIN_VALUE && max == i8.MAX_VALUE) {
321
+ return <i8>this.nextU32();
322
+ }
323
+ const left = <i32>min;
324
+ const right = <i32>max;
325
+ if (!exclude.length) {
326
+ return <i8>(
327
+ right <= left ? left : left + <i32>(this.nextU32() % <u32>(right - left + 1))
328
+ );
329
+ }
330
+ for (let attempts = 0; attempts < 1024; attempts++) {
331
+ const value = <i8>(
332
+ right <= left ? left : left + <i32>(this.nextU32() % <u32>(right - left + 1))
333
+ );
334
+ if (!containsValue<i8>(exclude, value)) return value;
335
+ }
336
+ panic();
337
+ return min;
338
+ }
339
+
340
+ private nextU8InRange(
341
+ min: u8,
342
+ max: u8,
343
+ exclude: u8[],
344
+ validateRange: bool = true,
345
+ ): u8 {
346
+ if (validateRange && max < min) panic();
347
+ if (!validateRange && min == u8.MIN_VALUE && max == u8.MAX_VALUE) {
348
+ return <u8>this.nextU32();
349
+ }
350
+ const left = <u32>min;
351
+ const right = <u32>max;
352
+ if (!exclude.length) {
353
+ return <u8>(right <= left ? left : left + (this.nextU32() % (right - left + 1)));
354
+ }
355
+ for (let attempts = 0; attempts < 1024; attempts++) {
356
+ const value = <u8>(
357
+ right <= left ? left : left + (this.nextU32() % (right - left + 1))
358
+ );
359
+ if (!containsValue<u8>(exclude, value)) return value;
360
+ }
361
+ panic();
362
+ return min;
363
+ }
364
+
365
+ private nextI16InRange(
366
+ min: i16,
367
+ max: i16,
368
+ exclude: i16[],
369
+ validateRange: bool = true,
370
+ ): i16 {
371
+ if (validateRange && max < min) panic();
372
+ if (!validateRange && min == i16.MIN_VALUE && max == i16.MAX_VALUE) {
373
+ return <i16>this.nextU32();
374
+ }
375
+ const left = <i32>min;
376
+ const right = <i32>max;
377
+ if (!exclude.length) {
378
+ return <i16>(
379
+ right <= left ? left : left + <i32>(this.nextU32() % <u32>(right - left + 1))
380
+ );
381
+ }
382
+ for (let attempts = 0; attempts < 1024; attempts++) {
383
+ const value = <i16>(
384
+ right <= left ? left : left + <i32>(this.nextU32() % <u32>(right - left + 1))
385
+ );
386
+ if (!containsValue<i16>(exclude, value)) return value;
387
+ }
388
+ panic();
389
+ return min;
390
+ }
391
+
392
+ private nextU16InRange(
393
+ min: u16,
394
+ max: u16,
395
+ exclude: u16[],
396
+ validateRange: bool = true,
397
+ ): u16 {
398
+ if (validateRange && max < min) panic();
399
+ if (!validateRange && min == u16.MIN_VALUE && max == u16.MAX_VALUE) {
400
+ return <u16>this.nextU32();
401
+ }
402
+ const left = <u32>min;
403
+ const right = <u32>max;
404
+ if (!exclude.length) {
405
+ return <u16>(right <= left ? left : left + (this.nextU32() % (right - left + 1)));
406
+ }
407
+ for (let attempts = 0; attempts < 1024; attempts++) {
408
+ const value = <u16>(
409
+ right <= left ? left : left + (this.nextU32() % (right - left + 1))
410
+ );
411
+ if (!containsValue<u16>(exclude, value)) return value;
412
+ }
413
+ panic();
414
+ return min;
415
+ }
416
+
417
+ private nextU32InRange(
418
+ min: u32,
419
+ max: u32,
420
+ exclude: u32[],
421
+ validateRange: bool = true,
422
+ ): u32 {
423
+ if (validateRange && max < min) panic();
424
+ if (!validateRange && min == u32.MIN_VALUE && max == u32.MAX_VALUE) {
425
+ return this.nextU32();
426
+ }
220
427
  if (!exclude.length) {
221
428
  return max <= min ? min : min + (this.nextU32() % (max - min + 1));
222
429
  }
@@ -228,6 +435,52 @@ export class FuzzSeed {
228
435
  return min;
229
436
  }
230
437
 
438
+ private nextI64InRange(
439
+ min: i64,
440
+ max: i64,
441
+ exclude: i64[],
442
+ validateRange: bool = true,
443
+ ): i64 {
444
+ if (validateRange && max < min) panic();
445
+ if (!validateRange && min == i64.MIN_VALUE && max == i64.MAX_VALUE) {
446
+ return <i64>this.nextU64();
447
+ }
448
+ const left = this.toOrderedU64(min);
449
+ const right = this.toOrderedU64(max);
450
+ if (!exclude.length) {
451
+ return this.fromOrderedU64(
452
+ left + this.nextU64Offset(left, right),
453
+ );
454
+ }
455
+ for (let attempts = 0; attempts < 1024; attempts++) {
456
+ const value = this.fromOrderedU64(left + this.nextU64Offset(left, right));
457
+ if (!containsValue<i64>(exclude, value)) return value;
458
+ }
459
+ panic();
460
+ return min;
461
+ }
462
+
463
+ private nextU64InRange(
464
+ min: u64,
465
+ max: u64,
466
+ exclude: u64[],
467
+ validateRange: bool = true,
468
+ ): u64 {
469
+ if (validateRange && max < min) panic();
470
+ if (!validateRange && min == u64.MIN_VALUE && max == u64.MAX_VALUE) {
471
+ return this.nextU64();
472
+ }
473
+ if (!exclude.length) {
474
+ return min + this.nextU64Offset(min, max);
475
+ }
476
+ for (let attempts = 0; attempts < 1024; attempts++) {
477
+ const value = min + this.nextU64Offset(min, max);
478
+ if (!containsValue<u64>(exclude, value)) return value;
479
+ }
480
+ panic();
481
+ return min;
482
+ }
483
+
231
484
  private nextF64InRange<T>(min: T, max: T, exclude: T[]): f64 {
232
485
  const left = <f64>min;
233
486
  const right = <f64>max;
@@ -268,6 +521,20 @@ export class FuzzSeed {
268
521
  const lo = <u64>this.nextU32();
269
522
  return (hi << 32) | lo;
270
523
  }
524
+
525
+ private nextU64Offset(min: u64, max: u64): u64 {
526
+ if (max <= min) return 0;
527
+ if (min == 0 && max == u64.MAX_VALUE) return this.nextU64();
528
+ return this.nextU64() % (max - min + 1);
529
+ }
530
+
531
+ private toOrderedU64(value: i64): u64 {
532
+ return <u64>value ^ I64_SIGN_MASK;
533
+ }
534
+
535
+ private fromOrderedU64(value: u64): i64 {
536
+ return <i64>(value ^ I64_SIGN_MASK);
537
+ }
271
538
  }
272
539
 
273
540
  const ASCII_ALPHABET: i32[] = rangeChars(32, 126);
@@ -20,8 +20,8 @@ class BuildFailureError extends Error {
20
20
  this.kind = args.kind;
21
21
  }
22
22
  }
23
- export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, featureToggles = {}, overrides = {}) {
24
- const loadedConfig = loadConfig(configPath, false);
23
+ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, featureToggles = {}, overrides = {}, resolvedConfig) {
24
+ const loadedConfig = resolvedConfig ?? loadConfig(configPath, false);
25
25
  const mode = applyMode(loadedConfig, modeName);
26
26
  const config = Object.assign(Object.create(Object.getPrototypeOf(mode.config)), mode.config);
27
27
  config.buildOptions = Object.assign(Object.create(Object.getPrototypeOf(mode.config.buildOptions)), mode.config.buildOptions);
@@ -44,7 +44,9 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
44
44
  ...config.buildOptions.env,
45
45
  AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
46
46
  };
47
- if (!process.env.AS_TEST_BUILD_API && !hasCustomBuildCommand(config)) {
47
+ if (!resolvedConfig &&
48
+ !process.env.AS_TEST_BUILD_API &&
49
+ !hasCustomBuildCommand(config)) {
48
50
  const pool = getSerialBuildWorkerPool();
49
51
  for (const file of inputFiles) {
50
52
  await pool.buildFileMode({
@@ -9,10 +9,11 @@ const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
9
9
  const MAGIC = Buffer.from("WIPC");
10
10
  const HEADER_SIZE = 9;
11
11
  const MAX_DEFAULT_SEED = 0x7fffffff;
12
- export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, overrides = {}) {
12
+ export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, overrides = {}, fuzzerSelectors = []) {
13
13
  const loadedConfig = loadConfig(configPath, false);
14
14
  const mode = applyMode(loadedConfig, modeName);
15
- const config = resolveFuzzConfig(loadedConfig.fuzz, overrides);
15
+ const activeConfig = mode.config;
16
+ const config = resolveFuzzConfig(activeConfig.fuzz, overrides);
16
17
  const inputPatterns = resolveFuzzInputPatterns(config.input, selectors);
17
18
  const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
18
19
  if (!inputFiles.length) {
@@ -22,10 +23,10 @@ export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], mod
22
23
  const results = [];
23
24
  for (const file of inputFiles) {
24
25
  const buildStartedAt = Date.now();
25
- await build(configPath, [file], modeName, { coverage: false }, { target: "bindings", args: ["--use", "AS_TEST_FUZZ=1"], kind: "fuzz" });
26
+ await build(configPath, [file], modeName, { coverage: false }, { target: "bindings", args: ["--use", "AS_TEST_FUZZ=1"], kind: "fuzz" }, activeConfig);
26
27
  const buildFinishedAt = Date.now();
27
28
  const buildTime = buildFinishedAt - buildStartedAt;
28
- results.push(await runFuzzTarget(file, mode.config.outDir, duplicateBasenames, config, buildStartedAt, buildFinishedAt, buildTime, modeName));
29
+ results.push(await runFuzzTarget(file, activeConfig.outDir, duplicateBasenames, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName));
29
30
  }
30
31
  return results;
31
32
  }
@@ -69,7 +70,7 @@ function encodeRunsOverrideKind(kind) {
69
70
  return 4;
70
71
  }
71
72
  }
72
- async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStartedAt, buildFinishedAt, buildTime, modeName) {
73
+ async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName) {
73
74
  const startedAt = Date.now();
74
75
  const artifact = resolveArtifactFileName(file, duplicateBasenames, modeName);
75
76
  const wasmPath = path.resolve(process.cwd(), outDir, artifact);
@@ -210,7 +211,10 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
210
211
  };
211
212
  }
212
213
  const crashFiles = [];
213
- for (const fuzzer of report.fuzzers) {
214
+ const selectedFuzzers = fuzzerSelectors.length
215
+ ? filterSelectedFuzzers(report.fuzzers, fuzzerSelectors, file)
216
+ : report.fuzzers;
217
+ for (const fuzzer of selectedFuzzers) {
214
218
  if (fuzzer.failed <= 0 && fuzzer.crashed <= 0)
215
219
  continue;
216
220
  const firstFailureSeed = typeof fuzzer.failures?.[0]?.seed == "number"
@@ -222,7 +226,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
222
226
  entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
223
227
  mode: modeName ?? "default",
224
228
  seed: firstFailureSeed,
225
- reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", 1),
229
+ reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", fuzzer.selector, 1),
226
230
  error: fuzzer.failure?.message ||
227
231
  `fuzz failure in ${fuzzer.name} after ${fuzzer.runs} runs`,
228
232
  stdout: passthrough.stdout,
@@ -237,21 +241,49 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
237
241
  file,
238
242
  target: path.basename(file),
239
243
  modeName: modeName ?? "default",
240
- runs: report.fuzzers.reduce((sum, item) => sum + item.runs, 0),
241
- crashes: report.fuzzers.reduce((sum, item) => sum + item.crashed, 0),
244
+ runs: selectedFuzzers.reduce((sum, item) => sum + item.runs, 0),
245
+ crashes: selectedFuzzers.reduce((sum, item) => sum + item.crashed, 0),
242
246
  crashFiles,
243
247
  seed: config.seed,
244
248
  time: Date.now() - startedAt,
245
249
  buildTime,
246
250
  buildStartedAt,
247
251
  buildFinishedAt,
248
- fuzzers: report.fuzzers,
252
+ fuzzers: selectedFuzzers,
249
253
  };
250
254
  }
251
- function buildFuzzReproCommand(file, seed, modeName, runs) {
255
+ function filterSelectedFuzzers(fuzzers, selectors, file) {
256
+ const annotated = fuzzers.map((fuzzer) => ({
257
+ ...fuzzer,
258
+ selector: slugifyFuzzerSelector(fuzzer.name),
259
+ }));
260
+ const selected = new Set();
261
+ for (const selector of selectors) {
262
+ const slug = slugifyFuzzerSelector(selector);
263
+ if (!slug.length)
264
+ continue;
265
+ const matches = annotated.filter((fuzzer) => fuzzer.selector == slug);
266
+ if (!matches.length) {
267
+ throw new Error(`No fuzz targets matched "${selector}" in ${path.basename(file)}.`);
268
+ }
269
+ for (const match of matches) {
270
+ selected.add(match.selector);
271
+ }
272
+ }
273
+ return annotated.filter((fuzzer) => selected.has(fuzzer.selector ?? ""));
274
+ }
275
+ function slugifyFuzzerSelector(value) {
276
+ return value
277
+ .trim()
278
+ .toLowerCase()
279
+ .replace(/[^a-z0-9]+/g, "-")
280
+ .replace(/^-+|-+$/g, "");
281
+ }
282
+ function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
252
283
  const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
284
+ const fuzzerArg = fuzzer?.length ? ` --fuzzer ${fuzzer}` : "";
253
285
  const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
254
- return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
286
+ return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
255
287
  }
256
288
  function buildFuzzFailureEntryKey(file, name, modeName) {
257
289
  return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
@@ -324,12 +356,12 @@ function captureFrames(onFrame) {
324
356
  }
325
357
  });
326
358
  process.stdin.read = ((size) => {
327
- const max = Number(size ?? 0);
359
+ const max = size == null ? 0 : Number(size);
328
360
  if (max > 0 && replies.length) {
329
361
  return dequeueReply(max);
330
362
  }
331
363
  if (originalRead) {
332
- return originalRead(size);
364
+ return originalRead(size === null ? undefined : size);
333
365
  }
334
366
  return null;
335
367
  });