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 +13 -0
- package/README.md +43 -0
- package/as-test.config.schema.json +71 -5
- package/assembly/as-test.intellisense.d.ts +8 -0
- package/assembly/src/fuzz.ts +277 -10
- package/bin/commands/build-core.js +5 -3
- package/bin/commands/fuzz-core.js +46 -14
- package/bin/commands/fuzz.js +2 -1
- package/bin/commands/run-core.js +144 -3
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +3 -1
- package/bin/index.js +138 -49
- package/bin/reporters/default.js +18 -8
- package/bin/types.js +1 -3
- package/bin/util.js +347 -256
- package/package.json +8 -8
- package/transform/lib/builder.js +14 -15
- package/transform/lib/coverage.js +11 -12
- package/transform/lib/index.js +0 -1
- package/transform/lib/linker.js +3 -4
- package/transform/lib/location.js +0 -1
- package/transform/lib/log.js +0 -1
- package/transform/lib/mock.js +15 -9
- package/transform/lib/range.js +0 -1
- package/transform/lib/types.js +0 -1
- package/transform/lib/util.js +0 -1
- package/transform/lib/visitor.js +64 -65
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
|
|
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
|
-
"
|
|
300
|
-
|
|
301
|
-
|
|
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;
|
package/assembly/src/fuzz.ts
CHANGED
|
@@ -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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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(
|
|
205
|
-
|
|
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
|
|
219
|
-
|
|
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 (!
|
|
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
241
|
-
crashes:
|
|
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:
|
|
252
|
+
fuzzers: selectedFuzzers,
|
|
249
253
|
};
|
|
250
254
|
}
|
|
251
|
-
function
|
|
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 =
|
|
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
|
});
|