as-test 1.0.5 → 1.0.7
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 +23 -0
- package/README.md +36 -0
- package/as-test.config.schema.json +39 -0
- package/assembly/index.ts +32 -3
- package/assembly/src/fuzz.ts +99 -17
- package/assembly/util/wipc.ts +14 -4
- package/bin/commands/fuzz-core.js +74 -3
- package/bin/commands/init-core.js +1 -1
- package/bin/commands/run-core.js +67 -0
- package/bin/coverage-points.js +173 -0
- package/bin/crash-store.js +12 -1
- package/bin/index.js +62 -4
- package/bin/reporters/default.js +122 -7
- package/bin/reporters/tap.js +5 -2
- package/bin/types.js +9 -0
- package/bin/util.js +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 2026-03-31 - v1.0.7
|
|
6
|
+
|
|
7
|
+
### Coverage
|
|
8
|
+
|
|
9
|
+
- feat: make CLI coverage output easier to scan with a summarized coverage block, per-file breakdown, grouped uncovered points, clickable `file:line:column` locations, trimmed source snippets, aligned gap columns, source-aware labels such as `Function`, `Method`, `Constructor`, `Property`, and `Call`, and coverage-point ignore rules for labels, names, locations, and snippets.
|
|
10
|
+
|
|
11
|
+
### Fuzzing
|
|
12
|
+
|
|
13
|
+
- feat: allow `fuzz(...)` and `xfuzz(...)` targets to override their own operation count with an optional third argument, so one file can mix short smoke fuzzers and heavier targets without changing the global `fuzz.runs` config.
|
|
14
|
+
- feat: make `--runs` / `--fuzz-runs` accept absolute and relative overrides such as `500`, `1.5x`, `+10%`, and `+100000`, applying them to each fuzzer's effective run count for that command.
|
|
15
|
+
|
|
16
|
+
## 2026-03-31 - v1.0.6
|
|
17
|
+
|
|
18
|
+
### Fuzzing
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
### Reporting
|
|
23
|
+
|
|
24
|
+
- fix: correct reporter override output behavior.
|
|
25
|
+
|
|
3
26
|
## 2026-03-30 - v1.0.5
|
|
4
27
|
|
|
5
28
|
### CLI
|
package/README.md
CHANGED
|
@@ -83,6 +83,23 @@ Minimal `as-test.config.json`:
|
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
Coverage point filtering is configurable when you want to ignore known-noisy gaps:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"coverage": {
|
|
91
|
+
"enabled": true,
|
|
92
|
+
"include": ["assembly/src/**/*.ts"],
|
|
93
|
+
"ignore": {
|
|
94
|
+
"labels": ["Call"],
|
|
95
|
+
"names": ["panic", "serialize"],
|
|
96
|
+
"locations": ["assembly/src/fuzz.ts:38:*"],
|
|
97
|
+
"snippets": ["*message: string*"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
86
103
|
## Writing Tests
|
|
87
104
|
|
|
88
105
|
Tests usually live in `assembly/__tests__/*.spec.ts`.
|
|
@@ -218,6 +235,23 @@ fuzz("bounded integer addition", (left: i32, right: i32): bool => {
|
|
|
218
235
|
});
|
|
219
236
|
```
|
|
220
237
|
|
|
238
|
+
Pass a third argument to override the operation count for one target without changing the global fuzz config:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
fuzz("hot path stays stable", (): void => {
|
|
242
|
+
expect(1 + 1).toBe(2);
|
|
243
|
+
}, 250);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
You can still override fuzz runs from the CLI when you want to force a different count for the current command:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npx ast fuzz --runs 500
|
|
250
|
+
npx ast fuzz --runs 1.5x
|
|
251
|
+
npx ast fuzz --runs +10%
|
|
252
|
+
npx ast fuzz --runs +100000
|
|
253
|
+
```
|
|
254
|
+
|
|
221
255
|
If you used `npx ast init` with a fuzzer example, the config is already there. Otherwise, add a `fuzz` block to `as-test.config.json` so `npx ast fuzz` knows what to build:
|
|
222
256
|
|
|
223
257
|
```json
|
|
@@ -231,6 +265,8 @@ If you used `npx ast init` with a fuzzer example, the config is already there. O
|
|
|
231
265
|
|
|
232
266
|
`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
267
|
|
|
268
|
+
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.
|
|
269
|
+
|
|
234
270
|
Run only fuzzers:
|
|
235
271
|
|
|
236
272
|
```bash
|
|
@@ -121,6 +121,45 @@
|
|
|
121
121
|
"type": "string"
|
|
122
122
|
},
|
|
123
123
|
"default": []
|
|
124
|
+
},
|
|
125
|
+
"ignore": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"description": "Ignore specific coverage points by derived label, symbol name, location, or source snippet.",
|
|
128
|
+
"additionalProperties": true,
|
|
129
|
+
"properties": {
|
|
130
|
+
"labels": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"description": "Ignore all points whose rendered label matches, such as Call, Method, Constructor, Property, or Function.",
|
|
133
|
+
"items": {
|
|
134
|
+
"type": "string"
|
|
135
|
+
},
|
|
136
|
+
"default": []
|
|
137
|
+
},
|
|
138
|
+
"names": {
|
|
139
|
+
"type": "array",
|
|
140
|
+
"description": "Ignore declarations or calls whose extracted symbol name matches a glob pattern.",
|
|
141
|
+
"items": {
|
|
142
|
+
"type": "string"
|
|
143
|
+
},
|
|
144
|
+
"default": []
|
|
145
|
+
},
|
|
146
|
+
"locations": {
|
|
147
|
+
"type": "array",
|
|
148
|
+
"description": "Ignore points whose file:line:column location matches a glob pattern.",
|
|
149
|
+
"items": {
|
|
150
|
+
"type": "string"
|
|
151
|
+
},
|
|
152
|
+
"default": []
|
|
153
|
+
},
|
|
154
|
+
"snippets": {
|
|
155
|
+
"type": "array",
|
|
156
|
+
"description": "Ignore points whose trimmed source snippet matches a glob pattern.",
|
|
157
|
+
"items": {
|
|
158
|
+
"type": "string"
|
|
159
|
+
},
|
|
160
|
+
"default": []
|
|
161
|
+
}
|
|
162
|
+
}
|
|
124
163
|
}
|
|
125
164
|
}
|
|
126
165
|
}
|
package/assembly/index.ts
CHANGED
|
@@ -169,8 +169,9 @@ export function xit(description: string, callback: () => void): void {
|
|
|
169
169
|
export function fuzz<T extends Function>(
|
|
170
170
|
description: string,
|
|
171
171
|
callback: T,
|
|
172
|
+
operations: i32 = 0,
|
|
172
173
|
): FuzzerBase {
|
|
173
|
-
const entry = createFuzzer(description, callback);
|
|
174
|
+
const entry = createFuzzer(description, callback, false, operations);
|
|
174
175
|
entryFuzzers.push(entry);
|
|
175
176
|
return entry;
|
|
176
177
|
}
|
|
@@ -178,8 +179,9 @@ export function fuzz<T extends Function>(
|
|
|
178
179
|
export function xfuzz<T extends Function>(
|
|
179
180
|
description: string,
|
|
180
181
|
callback: T,
|
|
182
|
+
operations: i32 = 0,
|
|
181
183
|
): FuzzerBase {
|
|
182
|
-
const entry = createFuzzer(description, callback, true);
|
|
184
|
+
const entry = createFuzzer(description, callback, true, operations);
|
|
183
185
|
entryFuzzers.push(entry);
|
|
184
186
|
return entry;
|
|
185
187
|
}
|
|
@@ -421,6 +423,8 @@ function containsOnlySuites(values: Suite[]): bool {
|
|
|
421
423
|
class FuzzConfig {
|
|
422
424
|
runs: i32 = 1000;
|
|
423
425
|
seed: u64 = 1337;
|
|
426
|
+
runsOverrideKind: i32 = 0;
|
|
427
|
+
runsOverrideValue: f64 = 0.0;
|
|
424
428
|
}
|
|
425
429
|
|
|
426
430
|
class FuzzReport {
|
|
@@ -444,7 +448,10 @@ function runFuzzers(): void {
|
|
|
444
448
|
for (let i = 0; i < entryFuzzers.length; i++) {
|
|
445
449
|
const fuzzer = unchecked(entryFuzzers[i]);
|
|
446
450
|
prepareFuzzIteration();
|
|
447
|
-
const result = fuzzer.run(
|
|
451
|
+
const result = fuzzer.run(
|
|
452
|
+
config.seed,
|
|
453
|
+
resolveFuzzerRuns(fuzzer, config),
|
|
454
|
+
);
|
|
448
455
|
report.fuzzers.push(result);
|
|
449
456
|
}
|
|
450
457
|
sendReport(report.serialize());
|
|
@@ -455,9 +462,31 @@ function requestFuzzConfig(): FuzzConfig {
|
|
|
455
462
|
const reply = requestHostFuzzConfig();
|
|
456
463
|
out.runs = reply.runs;
|
|
457
464
|
out.seed = reply.seed;
|
|
465
|
+
out.runsOverrideKind = reply.runsOverrideKind;
|
|
466
|
+
out.runsOverrideValue = reply.runsOverrideValue;
|
|
458
467
|
return out;
|
|
459
468
|
}
|
|
460
469
|
|
|
470
|
+
function resolveFuzzerRuns(fuzzer: FuzzerBase, config: FuzzConfig): i32 {
|
|
471
|
+
const baseRuns = fuzzer.runsOr(config.runs);
|
|
472
|
+
const resolved = applyFuzzRunsOverride(
|
|
473
|
+
baseRuns,
|
|
474
|
+
config.runsOverrideKind,
|
|
475
|
+
config.runsOverrideValue,
|
|
476
|
+
);
|
|
477
|
+
return resolved > 0 ? resolved : 1;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function applyFuzzRunsOverride(baseRuns: i32, kind: i32, value: f64): i32 {
|
|
481
|
+
if (kind == 1) return <i32>value;
|
|
482
|
+
if (kind == 2) return <i32>Math.round(<f64>baseRuns * value);
|
|
483
|
+
if (kind == 3) return baseRuns + <i32>value;
|
|
484
|
+
if (kind == 4) {
|
|
485
|
+
return baseRuns + <i32>Math.round((<f64>baseRuns * value) / 100.0);
|
|
486
|
+
}
|
|
487
|
+
return baseRuns;
|
|
488
|
+
}
|
|
489
|
+
|
|
461
490
|
function registerSuite(
|
|
462
491
|
description: string,
|
|
463
492
|
callback: () => void,
|
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";
|
|
@@ -186,15 +186,21 @@ export class FuzzSeed {
|
|
|
186
186
|
export abstract class FuzzerBase {
|
|
187
187
|
public name: string;
|
|
188
188
|
public skipped: bool;
|
|
189
|
-
|
|
189
|
+
public operations: i32;
|
|
190
|
+
constructor(name: string, skipped: bool = false, operations: i32 = 0) {
|
|
190
191
|
this.name = name;
|
|
191
192
|
this.skipped = skipped;
|
|
193
|
+
this.operations = operations > 0 ? operations : 0;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
generate<T extends Function>(_generator: T): this {
|
|
195
197
|
return this;
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
runsOr(defaultRuns: i32): i32 {
|
|
201
|
+
return this.operations > 0 ? this.operations : defaultRuns;
|
|
202
|
+
}
|
|
203
|
+
|
|
198
204
|
abstract run(seed: u64, runs: i32): FuzzerResult;
|
|
199
205
|
}
|
|
200
206
|
|
|
@@ -211,6 +217,7 @@ export class FuzzerResult {
|
|
|
211
217
|
public failureLeft: string = "";
|
|
212
218
|
public failureRight: string = "";
|
|
213
219
|
public failureMessage: string = "";
|
|
220
|
+
public failures: FuzzFailure[] = [];
|
|
214
221
|
|
|
215
222
|
serialize(): string {
|
|
216
223
|
return (
|
|
@@ -238,7 +245,27 @@ export class FuzzerResult {
|
|
|
238
245
|
quote(this.failureRight) +
|
|
239
246
|
',"message":' +
|
|
240
247
|
quote(this.failureMessage) +
|
|
241
|
-
|
|
248
|
+
'},"failures":' +
|
|
249
|
+
serializeFuzzFailures(this.failures) +
|
|
250
|
+
"}"
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export class FuzzFailure {
|
|
256
|
+
public run: i32 = 0;
|
|
257
|
+
public seed: u64 = 0;
|
|
258
|
+
public input: string = "";
|
|
259
|
+
|
|
260
|
+
serialize(): string {
|
|
261
|
+
return (
|
|
262
|
+
'{"run":' +
|
|
263
|
+
this.run.toString() +
|
|
264
|
+
',"seed":' +
|
|
265
|
+
this.seed.toString() +
|
|
266
|
+
',"input":' +
|
|
267
|
+
rawOrNull(this.input) +
|
|
268
|
+
"}"
|
|
242
269
|
);
|
|
243
270
|
}
|
|
244
271
|
}
|
|
@@ -329,7 +356,7 @@ function createSkippedResult(name: string): FuzzerResult {
|
|
|
329
356
|
return result;
|
|
330
357
|
}
|
|
331
358
|
|
|
332
|
-
function recordResult(result: FuzzerResult): void {
|
|
359
|
+
function recordResult(result: FuzzerResult, run: i32, seed: u64): void {
|
|
333
360
|
if (__as_test_fuzz_failed) {
|
|
334
361
|
result.failed++;
|
|
335
362
|
if (!result.failureInstr.length && __as_test_fuzz_failure_instr.length) {
|
|
@@ -338,6 +365,11 @@ function recordResult(result: FuzzerResult): void {
|
|
|
338
365
|
result.failureRight = __as_test_fuzz_failure_right;
|
|
339
366
|
result.failureMessage = __as_test_fuzz_failure_message;
|
|
340
367
|
}
|
|
368
|
+
const failure = new FuzzFailure();
|
|
369
|
+
failure.run = run;
|
|
370
|
+
failure.seed = seed;
|
|
371
|
+
failure.input = __as_test_fuzz_input;
|
|
372
|
+
result.failures.push(failure);
|
|
341
373
|
} else {
|
|
342
374
|
result.passed++;
|
|
343
375
|
}
|
|
@@ -351,8 +383,9 @@ export class Fuzzer0<R> extends FuzzerBase {
|
|
|
351
383
|
name: string,
|
|
352
384
|
private callback: () => R,
|
|
353
385
|
skipped: bool = false,
|
|
386
|
+
operations: i32 = 0,
|
|
354
387
|
) {
|
|
355
|
-
super(name, skipped);
|
|
388
|
+
super(name, skipped, operations);
|
|
356
389
|
this.returnsBool = !isVoid<R>();
|
|
357
390
|
}
|
|
358
391
|
|
|
@@ -389,7 +422,7 @@ export class Fuzzer0<R> extends FuzzerBase {
|
|
|
389
422
|
"fuzz generator must call run() exactly once",
|
|
390
423
|
);
|
|
391
424
|
}
|
|
392
|
-
recordResult(result);
|
|
425
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
393
426
|
}
|
|
394
427
|
__fuzz_callback0 = null;
|
|
395
428
|
result.timeEnd = performance.now();
|
|
@@ -405,8 +438,9 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
405
438
|
name: string,
|
|
406
439
|
private callback: (a: A) => R,
|
|
407
440
|
skipped: bool = false,
|
|
441
|
+
operations: i32 = 0,
|
|
408
442
|
) {
|
|
409
|
-
super(name, skipped);
|
|
443
|
+
super(name, skipped, operations);
|
|
410
444
|
this.returnsBool = !isVoid<R>();
|
|
411
445
|
}
|
|
412
446
|
|
|
@@ -438,7 +472,10 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
438
472
|
"fuzzers with arguments must call .generate(...)",
|
|
439
473
|
);
|
|
440
474
|
} else {
|
|
441
|
-
this.generator(seed,
|
|
475
|
+
this.generator(seed, (a: A): R => {
|
|
476
|
+
__as_test_fuzz_input = "[" + stringifyValue<A>(a) + "]";
|
|
477
|
+
return changetype<(a: A) => R>(__fuzz_run1)(a);
|
|
478
|
+
});
|
|
442
479
|
}
|
|
443
480
|
if (__fuzz_calls != 1) {
|
|
444
481
|
failFuzzIteration(
|
|
@@ -448,7 +485,7 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
448
485
|
"fuzz generator must call run() exactly once",
|
|
449
486
|
);
|
|
450
487
|
}
|
|
451
|
-
recordResult(result);
|
|
488
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
452
489
|
}
|
|
453
490
|
__fuzz_callback1 = null;
|
|
454
491
|
result.timeEnd = performance.now();
|
|
@@ -465,8 +502,9 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
465
502
|
name: string,
|
|
466
503
|
private callback: (a: A, b: B) => R,
|
|
467
504
|
skipped: bool = false,
|
|
505
|
+
operations: i32 = 0,
|
|
468
506
|
) {
|
|
469
|
-
super(name, skipped);
|
|
507
|
+
super(name, skipped, operations);
|
|
470
508
|
this.returnsBool = !isVoid<R>();
|
|
471
509
|
}
|
|
472
510
|
|
|
@@ -500,7 +538,11 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
500
538
|
"fuzzers with arguments must call .generate(...)",
|
|
501
539
|
);
|
|
502
540
|
} else {
|
|
503
|
-
this.generator(seed,
|
|
541
|
+
this.generator(seed, (a: A, b: B): R => {
|
|
542
|
+
__as_test_fuzz_input =
|
|
543
|
+
"[" + stringifyValue<A>(a) + "," + stringifyValue<B>(b) + "]";
|
|
544
|
+
return changetype<(a: A, b: B) => R>(__fuzz_run2)(a, b);
|
|
545
|
+
});
|
|
504
546
|
}
|
|
505
547
|
if (__fuzz_calls != 1) {
|
|
506
548
|
failFuzzIteration(
|
|
@@ -510,7 +552,7 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
510
552
|
"fuzz generator must call run() exactly once",
|
|
511
553
|
);
|
|
512
554
|
}
|
|
513
|
-
recordResult(result);
|
|
555
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
514
556
|
}
|
|
515
557
|
__fuzz_callback2 = null;
|
|
516
558
|
result.timeEnd = performance.now();
|
|
@@ -526,8 +568,9 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
526
568
|
name: string,
|
|
527
569
|
private callback: (a: A, b: B, c: C) => R,
|
|
528
570
|
skipped: bool = false,
|
|
571
|
+
operations: i32 = 0,
|
|
529
572
|
) {
|
|
530
|
-
super(name, skipped);
|
|
573
|
+
super(name, skipped, operations);
|
|
531
574
|
this.returnsBool = !isVoid<R>();
|
|
532
575
|
}
|
|
533
576
|
|
|
@@ -564,7 +607,17 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
564
607
|
} else {
|
|
565
608
|
changetype<(seed: FuzzSeed, run: (a: A, b: B, c: C) => R) => void>(
|
|
566
609
|
this.generator,
|
|
567
|
-
)(seed,
|
|
610
|
+
)(seed, (a: A, b: B, c: C): R => {
|
|
611
|
+
__as_test_fuzz_input =
|
|
612
|
+
"[" +
|
|
613
|
+
stringifyValue<A>(a) +
|
|
614
|
+
"," +
|
|
615
|
+
stringifyValue<B>(b) +
|
|
616
|
+
"," +
|
|
617
|
+
stringifyValue<C>(c) +
|
|
618
|
+
"]";
|
|
619
|
+
return changetype<(a: A, b: B, c: C) => R>(__fuzz_run3)(a, b, c);
|
|
620
|
+
});
|
|
568
621
|
}
|
|
569
622
|
if (__fuzz_calls != 1) {
|
|
570
623
|
failFuzzIteration(
|
|
@@ -574,7 +627,7 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
574
627
|
"fuzz generator must call run() exactly once",
|
|
575
628
|
);
|
|
576
629
|
}
|
|
577
|
-
recordResult(result);
|
|
630
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
578
631
|
}
|
|
579
632
|
__fuzz_callback3 = null;
|
|
580
633
|
result.timeEnd = performance.now();
|
|
@@ -586,16 +639,23 @@ export function createFuzzer<T extends Function>(
|
|
|
586
639
|
name: string,
|
|
587
640
|
callback: T,
|
|
588
641
|
skipped: bool = false,
|
|
642
|
+
operations: i32 = 0,
|
|
589
643
|
): FuzzerBase {
|
|
590
644
|
const length = callback.length;
|
|
591
645
|
if (length == 0) {
|
|
592
|
-
return new Fuzzer0<usize>(
|
|
646
|
+
return new Fuzzer0<usize>(
|
|
647
|
+
name,
|
|
648
|
+
changetype<() => usize>(callback),
|
|
649
|
+
skipped,
|
|
650
|
+
operations,
|
|
651
|
+
);
|
|
593
652
|
}
|
|
594
653
|
if (length == 1) {
|
|
595
654
|
return new Fuzzer1<usize, usize>(
|
|
596
655
|
name,
|
|
597
656
|
changetype<(a: usize) => usize>(callback),
|
|
598
657
|
skipped,
|
|
658
|
+
operations,
|
|
599
659
|
);
|
|
600
660
|
}
|
|
601
661
|
if (length == 2) {
|
|
@@ -603,6 +663,7 @@ export function createFuzzer<T extends Function>(
|
|
|
603
663
|
name,
|
|
604
664
|
changetype<(a: usize, b: usize) => usize>(callback),
|
|
605
665
|
skipped,
|
|
666
|
+
operations,
|
|
606
667
|
);
|
|
607
668
|
}
|
|
608
669
|
if (length == 3) {
|
|
@@ -610,10 +671,16 @@ export function createFuzzer<T extends Function>(
|
|
|
610
671
|
name,
|
|
611
672
|
changetype<(a: usize, b: usize, c: usize) => usize>(callback),
|
|
612
673
|
skipped,
|
|
674
|
+
operations,
|
|
613
675
|
);
|
|
614
676
|
}
|
|
615
677
|
panic();
|
|
616
|
-
return new Fuzzer0<usize>(
|
|
678
|
+
return new Fuzzer0<usize>(
|
|
679
|
+
name,
|
|
680
|
+
changetype<() => usize>(callback),
|
|
681
|
+
skipped,
|
|
682
|
+
operations,
|
|
683
|
+
);
|
|
617
684
|
}
|
|
618
685
|
|
|
619
686
|
function buildAlphabet(options: StringOptions): i32[] {
|
|
@@ -694,6 +761,8 @@ function containsFloatValue<T>(values: T[], needle: T): bool {
|
|
|
694
761
|
@global export let __as_test_fuzz_failure_right: string = "";
|
|
695
762
|
// @ts-ignore
|
|
696
763
|
@global export let __as_test_fuzz_failure_message: string = "";
|
|
764
|
+
// @ts-ignore
|
|
765
|
+
@global export let __as_test_fuzz_input: string = "";
|
|
697
766
|
|
|
698
767
|
export function prepareFuzzIteration(): void {
|
|
699
768
|
__as_test_fuzz_failed = false;
|
|
@@ -701,6 +770,7 @@ export function prepareFuzzIteration(): void {
|
|
|
701
770
|
__as_test_fuzz_failure_left = "";
|
|
702
771
|
__as_test_fuzz_failure_right = "";
|
|
703
772
|
__as_test_fuzz_failure_message = "";
|
|
773
|
+
__as_test_fuzz_input = "[]";
|
|
704
774
|
}
|
|
705
775
|
|
|
706
776
|
export function failFuzzIteration(
|
|
@@ -721,3 +791,15 @@ export function failFuzzIteration(
|
|
|
721
791
|
function panic(): void {
|
|
722
792
|
unreachable();
|
|
723
793
|
}
|
|
794
|
+
|
|
795
|
+
function serializeFuzzFailures(values: FuzzFailure[]): string {
|
|
796
|
+
if (!values.length) return "[]";
|
|
797
|
+
|
|
798
|
+
let out = "[";
|
|
799
|
+
for (let i = 0; i < values.length; i++) {
|
|
800
|
+
if (i) out += ",";
|
|
801
|
+
out += unchecked(values[i]).serialize();
|
|
802
|
+
}
|
|
803
|
+
out += "]";
|
|
804
|
+
return out;
|
|
805
|
+
}
|
package/assembly/util/wipc.ts
CHANGED
|
@@ -52,6 +52,8 @@ export class SnapshotReply {
|
|
|
52
52
|
export class FuzzConfigReply {
|
|
53
53
|
public runs: i32 = 1000;
|
|
54
54
|
public seed: u64 = 1337;
|
|
55
|
+
public runsOverrideKind: i32 = 0;
|
|
56
|
+
public runsOverrideValue: f64 = 0.0;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export function sendAssertionFailure(
|
|
@@ -141,13 +143,21 @@ export function requestFuzzConfig(): FuzzConfigReply {
|
|
|
141
143
|
if (!body.length) {
|
|
142
144
|
return new FuzzConfigReply();
|
|
143
145
|
}
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
+
const first = body.indexOf("\n");
|
|
147
|
+
if (first < 0) return new FuzzConfigReply();
|
|
146
148
|
const reply = new FuzzConfigReply();
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
+
const second = body.indexOf("\n", first + 1);
|
|
150
|
+
const third = second >= 0 ? body.indexOf("\n", second + 1) : -1;
|
|
151
|
+
const runs = body.slice(0, first);
|
|
152
|
+
const seed =
|
|
153
|
+
second >= 0 ? body.slice(first + 1, second) : body.slice(first + 1);
|
|
154
|
+
const kind =
|
|
155
|
+
second >= 0 && third >= 0 ? body.slice(second + 1, third) : "";
|
|
156
|
+
const value = third >= 0 ? body.slice(third + 1) : "";
|
|
149
157
|
if (runs.length) reply.runs = I32.parseInt(runs);
|
|
150
158
|
if (seed.length) reply.seed = U64.parseInt(seed);
|
|
159
|
+
if (kind.length) reply.runsOverrideKind = I32.parseInt(kind);
|
|
160
|
+
if (value.length) reply.runsOverrideValue = parseFloat(value);
|
|
151
161
|
return reply;
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -29,12 +29,39 @@ export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], mod
|
|
|
29
29
|
return results;
|
|
30
30
|
}
|
|
31
31
|
function resolveFuzzConfig(raw, overrides) {
|
|
32
|
-
const config = Object.assign({}, raw
|
|
32
|
+
const config = Object.assign({}, raw);
|
|
33
|
+
if (typeof overrides.seed == "number") {
|
|
34
|
+
config.seed = overrides.seed;
|
|
35
|
+
}
|
|
36
|
+
if (typeof overrides.runs == "number") {
|
|
37
|
+
config.runs = overrides.runs;
|
|
38
|
+
}
|
|
39
|
+
config.runsOverrideKind = 0;
|
|
40
|
+
config.runsOverrideValue = 0;
|
|
41
|
+
if (overrides.runsOverride) {
|
|
42
|
+
config.runsOverrideKind = encodeRunsOverrideKind(overrides.runsOverride.kind);
|
|
43
|
+
config.runsOverrideValue = overrides.runsOverride.value;
|
|
44
|
+
if (overrides.runsOverride.kind == "set") {
|
|
45
|
+
config.runs = Math.max(1, Math.round(overrides.runsOverride.value));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
33
48
|
if (config.target != "bindings") {
|
|
34
49
|
throw new Error(`fuzz target must be "bindings"; received "${config.target}"`);
|
|
35
50
|
}
|
|
36
51
|
return config;
|
|
37
52
|
}
|
|
53
|
+
function encodeRunsOverrideKind(kind) {
|
|
54
|
+
switch (kind) {
|
|
55
|
+
case "set":
|
|
56
|
+
return 1;
|
|
57
|
+
case "scale":
|
|
58
|
+
return 2;
|
|
59
|
+
case "add":
|
|
60
|
+
return 3;
|
|
61
|
+
case "percent-add":
|
|
62
|
+
return 4;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
38
65
|
async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStartedAt, buildFinishedAt, buildTime, modeName) {
|
|
39
66
|
const startedAt = Date.now();
|
|
40
67
|
const artifact = resolveArtifactFileName(file, duplicateBasenames, modeName);
|
|
@@ -48,7 +75,8 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
48
75
|
if (type == 0x02) {
|
|
49
76
|
const event = JSON.parse(payload.toString("utf8"));
|
|
50
77
|
if (String(event.kind ?? "") == "fuzz:config") {
|
|
51
|
-
|
|
78
|
+
const resolved = config;
|
|
79
|
+
respond(`${config.runs}\n${config.seed}\n${resolved.runsOverrideKind ?? 0}\n${resolved.runsOverrideValue ?? 0}`);
|
|
52
80
|
}
|
|
53
81
|
else {
|
|
54
82
|
respond("");
|
|
@@ -68,6 +96,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
68
96
|
const crash = persistCrashRecord(config.crashDir, {
|
|
69
97
|
kind: "fuzz",
|
|
70
98
|
file,
|
|
99
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
71
100
|
mode: modeName ?? "default",
|
|
72
101
|
seed: config.seed,
|
|
73
102
|
error: crashMessage,
|
|
@@ -94,6 +123,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
94
123
|
const crash = persistCrashRecord(config.crashDir, {
|
|
95
124
|
kind: "fuzz",
|
|
96
125
|
file,
|
|
126
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
97
127
|
mode: modeName ?? "default",
|
|
98
128
|
seed: config.seed,
|
|
99
129
|
error: `missing fuzz report payload from ${path.basename(file)}`,
|
|
@@ -115,13 +145,37 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
115
145
|
fuzzers: [],
|
|
116
146
|
};
|
|
117
147
|
}
|
|
148
|
+
const crashFiles = [];
|
|
149
|
+
for (const fuzzer of report.fuzzers) {
|
|
150
|
+
if (fuzzer.failed <= 0 && fuzzer.crashed <= 0)
|
|
151
|
+
continue;
|
|
152
|
+
const firstFailureSeed = typeof fuzzer.failures?.[0]?.seed == "number"
|
|
153
|
+
? fuzzer.failures[0].seed
|
|
154
|
+
: config.seed;
|
|
155
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
156
|
+
kind: "fuzz",
|
|
157
|
+
file,
|
|
158
|
+
entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
|
|
159
|
+
mode: modeName ?? "default",
|
|
160
|
+
seed: firstFailureSeed,
|
|
161
|
+
reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", 1),
|
|
162
|
+
error: fuzzer.failure?.message ||
|
|
163
|
+
`fuzz failure in ${fuzzer.name} after ${fuzzer.runs} runs`,
|
|
164
|
+
stdout: passthrough.stdout,
|
|
165
|
+
stderr: "",
|
|
166
|
+
failure: fuzzer.failure,
|
|
167
|
+
failures: fuzzer.failures,
|
|
168
|
+
});
|
|
169
|
+
crashFiles.push(crash.jsonPath);
|
|
170
|
+
fuzzer.crashFile = crash.jsonPath;
|
|
171
|
+
}
|
|
118
172
|
return {
|
|
119
173
|
file,
|
|
120
174
|
target: path.basename(file),
|
|
121
175
|
modeName: modeName ?? "default",
|
|
122
176
|
runs: report.fuzzers.reduce((sum, item) => sum + item.runs, 0),
|
|
123
177
|
crashes: report.fuzzers.reduce((sum, item) => sum + item.crashed, 0),
|
|
124
|
-
crashFiles
|
|
178
|
+
crashFiles,
|
|
125
179
|
seed: config.seed,
|
|
126
180
|
time: Date.now() - startedAt,
|
|
127
181
|
buildTime,
|
|
@@ -130,6 +184,23 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
130
184
|
fuzzers: report.fuzzers,
|
|
131
185
|
};
|
|
132
186
|
}
|
|
187
|
+
function buildFuzzReproCommand(file, seed, modeName, runs) {
|
|
188
|
+
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
189
|
+
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
190
|
+
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
191
|
+
}
|
|
192
|
+
function buildFuzzFailureEntryKey(file, name, modeName) {
|
|
193
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
|
|
194
|
+
}
|
|
195
|
+
function buildFuzzCrashEntryKey(file, modeName) {
|
|
196
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}`;
|
|
197
|
+
}
|
|
198
|
+
function sanitizeEntryName(name) {
|
|
199
|
+
return name
|
|
200
|
+
.toLowerCase()
|
|
201
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
202
|
+
.replace(/^-+|-+$/g, "") || "fuzzer";
|
|
203
|
+
}
|
|
133
204
|
function captureFrames(onFrame) {
|
|
134
205
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
135
206
|
const originalRead = typeof process.stdin.read == "function"
|
|
@@ -961,7 +961,7 @@ function buildBasicFuzzerExample() {
|
|
|
961
961
|
fuzz("basic string fuzzer", (value: string): bool => {
|
|
962
962
|
expect(value.length >= 0).toBe(true);
|
|
963
963
|
return value.length <= 24;
|
|
964
|
-
}).generate((seed: FuzzSeed, run: (value: string) => bool): void => {
|
|
964
|
+
}, 250).generate((seed: FuzzSeed, run: (value: string) => bool): void => {
|
|
965
965
|
run(
|
|
966
966
|
seed.string({
|
|
967
967
|
charset: "ascii",
|
package/bin/commands/run-core.js
CHANGED
|
@@ -10,6 +10,7 @@ import { buildWebRunnerSource } from "./web-runner-source.js";
|
|
|
10
10
|
import { createReporter as createDefaultReporter } from "../reporters/default.js";
|
|
11
11
|
import { createTapReporter } from "../reporters/tap.js";
|
|
12
12
|
import { persistCrashRecord } from "../crash-store.js";
|
|
13
|
+
import { describeCoveragePoint } from "../coverage-points.js";
|
|
13
14
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
14
15
|
class SnapshotStore {
|
|
15
16
|
constructor(specFile, snapshotDir, duplicateSpecBasenames = new Set()) {
|
|
@@ -1091,6 +1092,8 @@ function collectCoverageSummary(reports, enabled, showPoints, coverage) {
|
|
|
1091
1092
|
for (const point of report.coverage.points) {
|
|
1092
1093
|
if (isIgnoredCoverageFile(point.file, coverage))
|
|
1093
1094
|
continue;
|
|
1095
|
+
if (isIgnoredCoveragePoint(point, coverage))
|
|
1096
|
+
continue;
|
|
1094
1097
|
const key = `${point.file}::${point.hash}`;
|
|
1095
1098
|
const existing = uniquePoints.get(key);
|
|
1096
1099
|
if (!existing) {
|
|
@@ -1246,10 +1249,19 @@ function resolveCoverageOptions(raw) {
|
|
|
1246
1249
|
includeSpecs: false,
|
|
1247
1250
|
include: [],
|
|
1248
1251
|
exclude: [],
|
|
1252
|
+
ignore: {
|
|
1253
|
+
labels: [],
|
|
1254
|
+
names: [],
|
|
1255
|
+
locations: [],
|
|
1256
|
+
snippets: [],
|
|
1257
|
+
},
|
|
1249
1258
|
};
|
|
1250
1259
|
}
|
|
1251
1260
|
if (raw && typeof raw == "object") {
|
|
1252
1261
|
const obj = raw;
|
|
1262
|
+
const ignore = obj.ignore && typeof obj.ignore == "object" && !Array.isArray(obj.ignore)
|
|
1263
|
+
? obj.ignore
|
|
1264
|
+
: null;
|
|
1253
1265
|
return {
|
|
1254
1266
|
enabled: obj.enabled == null ? false : Boolean(obj.enabled),
|
|
1255
1267
|
includeSpecs: Boolean(obj.includeSpecs),
|
|
@@ -1259,6 +1271,20 @@ function resolveCoverageOptions(raw) {
|
|
|
1259
1271
|
exclude: Array.isArray(obj.exclude)
|
|
1260
1272
|
? obj.exclude.filter((item) => typeof item == "string")
|
|
1261
1273
|
: [],
|
|
1274
|
+
ignore: {
|
|
1275
|
+
labels: Array.isArray(ignore?.labels)
|
|
1276
|
+
? ignore.labels.filter((item) => typeof item == "string")
|
|
1277
|
+
: [],
|
|
1278
|
+
names: Array.isArray(ignore?.names)
|
|
1279
|
+
? ignore.names.filter((item) => typeof item == "string")
|
|
1280
|
+
: [],
|
|
1281
|
+
locations: Array.isArray(ignore?.locations)
|
|
1282
|
+
? ignore.locations.filter((item) => typeof item == "string")
|
|
1283
|
+
: [],
|
|
1284
|
+
snippets: Array.isArray(ignore?.snippets)
|
|
1285
|
+
? ignore.snippets.filter((item) => typeof item == "string")
|
|
1286
|
+
: [],
|
|
1287
|
+
},
|
|
1262
1288
|
};
|
|
1263
1289
|
}
|
|
1264
1290
|
return {
|
|
@@ -1266,8 +1292,49 @@ function resolveCoverageOptions(raw) {
|
|
|
1266
1292
|
includeSpecs: false,
|
|
1267
1293
|
include: [],
|
|
1268
1294
|
exclude: [],
|
|
1295
|
+
ignore: {
|
|
1296
|
+
labels: [],
|
|
1297
|
+
names: [],
|
|
1298
|
+
locations: [],
|
|
1299
|
+
snippets: [],
|
|
1300
|
+
},
|
|
1269
1301
|
};
|
|
1270
1302
|
}
|
|
1303
|
+
function isIgnoredCoveragePoint(point, coverage) {
|
|
1304
|
+
const ignore = coverage.ignore;
|
|
1305
|
+
if (!ignore.labels.length &&
|
|
1306
|
+
!ignore.names.length &&
|
|
1307
|
+
!ignore.locations.length &&
|
|
1308
|
+
!ignore.snippets.length) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
const info = describeCoveragePoint(point.file, point.line, point.column, point.type);
|
|
1312
|
+
const location = `${point.file.replace(/\\/g, "/")}:${point.line}:${point.column}`;
|
|
1313
|
+
const label = info.displayType.toLowerCase();
|
|
1314
|
+
const name = info.subjectName?.toLowerCase() ?? "";
|
|
1315
|
+
const snippet = info.visible.toLowerCase();
|
|
1316
|
+
if (ignore.labels.some((pattern) => matchesCoverageTextPattern(label, pattern.toLowerCase()))) {
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
if (name.length &&
|
|
1320
|
+
ignore.names.some((pattern) => matchesCoverageTextPattern(name, pattern.toLowerCase()))) {
|
|
1321
|
+
return true;
|
|
1322
|
+
}
|
|
1323
|
+
if (ignore.locations.some((pattern) => matchesCoverageTextPattern(location, pattern.replace(/\\/g, "/")))) {
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
if (snippet.length &&
|
|
1327
|
+
ignore.snippets.some((pattern) => matchesCoverageTextPattern(snippet, pattern.toLowerCase()))) {
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
function matchesCoverageTextPattern(value, pattern) {
|
|
1333
|
+
const normalized = pattern.trim();
|
|
1334
|
+
if (!normalized.length)
|
|
1335
|
+
return false;
|
|
1336
|
+
return globPatternToRegExp(normalized).test(value);
|
|
1337
|
+
}
|
|
1271
1338
|
function compareCoveragePoints(a, b) {
|
|
1272
1339
|
if (a.line !== b.line)
|
|
1273
1340
|
return a.line - b.line;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
const sourceLineCache = new Map();
|
|
4
|
+
export function describeCoveragePoint(file, line, column, fallbackType) {
|
|
5
|
+
const context = getCoverageSourceContext(file, line, column);
|
|
6
|
+
if (!context) {
|
|
7
|
+
return {
|
|
8
|
+
displayType: fallbackType,
|
|
9
|
+
subjectName: null,
|
|
10
|
+
visible: "",
|
|
11
|
+
focus: 0,
|
|
12
|
+
highlightStart: 0,
|
|
13
|
+
highlightEnd: 0,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const declaration = detectCoverageDeclaration(context.visible);
|
|
17
|
+
if (declaration) {
|
|
18
|
+
const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(context.visible, context.focus);
|
|
19
|
+
return {
|
|
20
|
+
displayType: declaration.type,
|
|
21
|
+
subjectName: declaration.name,
|
|
22
|
+
visible: context.visible,
|
|
23
|
+
focus: context.focus,
|
|
24
|
+
highlightStart,
|
|
25
|
+
highlightEnd,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const call = detectCoverageCall(context.visible, context.focus);
|
|
29
|
+
if (call) {
|
|
30
|
+
return {
|
|
31
|
+
displayType: "Call",
|
|
32
|
+
subjectName: call.name,
|
|
33
|
+
visible: context.visible,
|
|
34
|
+
focus: context.focus,
|
|
35
|
+
highlightStart: call.start,
|
|
36
|
+
highlightEnd: call.end,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(context.visible, context.focus);
|
|
40
|
+
return {
|
|
41
|
+
displayType: fallbackType,
|
|
42
|
+
subjectName: null,
|
|
43
|
+
visible: context.visible,
|
|
44
|
+
focus: context.focus,
|
|
45
|
+
highlightStart,
|
|
46
|
+
highlightEnd,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function readCoverageSourceLine(file, line) {
|
|
50
|
+
const resolved = path.resolve(process.cwd(), file);
|
|
51
|
+
let lines = sourceLineCache.get(resolved);
|
|
52
|
+
if (lines === undefined) {
|
|
53
|
+
try {
|
|
54
|
+
lines = readFileSync(resolved, "utf8").split(/\r?\n/);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
lines = null;
|
|
58
|
+
}
|
|
59
|
+
sourceLineCache.set(resolved, lines);
|
|
60
|
+
}
|
|
61
|
+
if (!lines)
|
|
62
|
+
return "";
|
|
63
|
+
return lines[line - 1] ?? "";
|
|
64
|
+
}
|
|
65
|
+
export function resolveCoverageHighlightSpan(visible, focus) {
|
|
66
|
+
if (!visible.length)
|
|
67
|
+
return [0, 0];
|
|
68
|
+
const index = Math.max(0, Math.min(visible.length - 1, focus));
|
|
69
|
+
if (isCoverageBoundary(visible.charAt(index))) {
|
|
70
|
+
return [index, Math.min(visible.length, index + 1)];
|
|
71
|
+
}
|
|
72
|
+
let start = index;
|
|
73
|
+
let end = index + 1;
|
|
74
|
+
while (start > 0 && !isCoverageBoundary(visible.charAt(start - 1)))
|
|
75
|
+
start--;
|
|
76
|
+
while (end < visible.length && !isCoverageBoundary(visible.charAt(end)))
|
|
77
|
+
end++;
|
|
78
|
+
return [start, end];
|
|
79
|
+
}
|
|
80
|
+
function getCoverageSourceContext(file, line, column) {
|
|
81
|
+
const sourceLine = readCoverageSourceLine(file, line);
|
|
82
|
+
if (!sourceLine)
|
|
83
|
+
return null;
|
|
84
|
+
const expanded = sourceLine.replace(/\t/g, " ");
|
|
85
|
+
const firstNonWhitespace = expanded.search(/\S/);
|
|
86
|
+
if (firstNonWhitespace == -1)
|
|
87
|
+
return null;
|
|
88
|
+
const visible = expanded.slice(firstNonWhitespace).trimEnd();
|
|
89
|
+
if (!visible.length)
|
|
90
|
+
return null;
|
|
91
|
+
const focus = Math.max(0, Math.min(visible.length - 1, Math.max(0, column - 1 - firstNonWhitespace)));
|
|
92
|
+
return { visible, focus };
|
|
93
|
+
}
|
|
94
|
+
function detectCoverageDeclaration(visible) {
|
|
95
|
+
const trimmed = visible.trim();
|
|
96
|
+
if (!trimmed.length)
|
|
97
|
+
return null;
|
|
98
|
+
let match = trimmed.match(/^(?:export\s+)?function\s+([A-Za-z_]\w*)(?:<[^>]+>)?\s*\(/);
|
|
99
|
+
if (match)
|
|
100
|
+
return { type: "Function", name: match[1] ?? null };
|
|
101
|
+
if (trimmed.startsWith("constructor(") ||
|
|
102
|
+
/^(?:public\s+|private\s+|protected\s+)constructor\s*\(/.test(trimmed)) {
|
|
103
|
+
return { type: "Constructor", name: "constructor" };
|
|
104
|
+
}
|
|
105
|
+
match = trimmed.match(/^(?:export\s+)?(?:public\s+|private\s+|protected\s+)?(?:static\s+)?([A-Za-z_]\w*)(?:<[^>]+>)?\([^)]*\)\s*:\s*[^{=]+[{]?$/);
|
|
106
|
+
if (match)
|
|
107
|
+
return { type: "Method", name: match[1] ?? null };
|
|
108
|
+
match = trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:readonly\s+)?([A-Za-z_]\w*)(?:<[^>]+>)?\s*:\s*[^=;{]+(?:=.*)?;?$/);
|
|
109
|
+
if (match)
|
|
110
|
+
return { type: "Property", name: match[1] ?? null };
|
|
111
|
+
if (/^(?:export\s+)?class\b/.test(trimmed)) {
|
|
112
|
+
match = trimmed.match(/^(?:export\s+)?class\s+([A-Za-z_]\w*)/);
|
|
113
|
+
return { type: "Class", name: match?.[1] ?? null };
|
|
114
|
+
}
|
|
115
|
+
if (/^(?:export\s+)?enum\b/.test(trimmed)) {
|
|
116
|
+
match = trimmed.match(/^(?:export\s+)?enum\s+([A-Za-z_]\w*)/);
|
|
117
|
+
return { type: "Enum", name: match?.[1] ?? null };
|
|
118
|
+
}
|
|
119
|
+
if (/^(?:export\s+)?interface\b/.test(trimmed)) {
|
|
120
|
+
match = trimmed.match(/^(?:export\s+)?interface\s+([A-Za-z_]\w*)/);
|
|
121
|
+
return { type: "Interface", name: match?.[1] ?? null };
|
|
122
|
+
}
|
|
123
|
+
if (/^(?:export\s+)?namespace\b/.test(trimmed)) {
|
|
124
|
+
match = trimmed.match(/^(?:export\s+)?namespace\s+([A-Za-z_]\w*)/);
|
|
125
|
+
return { type: "Namespace", name: match?.[1] ?? null };
|
|
126
|
+
}
|
|
127
|
+
if (/^(?:const|let|var)\b/.test(trimmed)) {
|
|
128
|
+
match = trimmed.match(/^(?:const|let|var)\s+([A-Za-z_]\w*)/);
|
|
129
|
+
return { type: "Variable", name: match?.[1] ?? null };
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function detectCoverageCall(visible, focus) {
|
|
134
|
+
const matches = [...visible.matchAll(/\b([A-Za-z_]\w*)(?:<[^>()]+>)?\s*\(/g)];
|
|
135
|
+
if (!matches.length)
|
|
136
|
+
return null;
|
|
137
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
138
|
+
let bestMatch = null;
|
|
139
|
+
for (const match of matches) {
|
|
140
|
+
const start = match.index ?? -1;
|
|
141
|
+
if (start == -1)
|
|
142
|
+
continue;
|
|
143
|
+
const end = start + match[0].length;
|
|
144
|
+
const distance = focus < start ? start - focus : focus >= end ? focus - end + 1 : 0;
|
|
145
|
+
if (distance < bestDistance) {
|
|
146
|
+
bestDistance = distance;
|
|
147
|
+
bestMatch = match;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!bestMatch)
|
|
151
|
+
return null;
|
|
152
|
+
const name = bestMatch[1] ?? null;
|
|
153
|
+
if (name == "if" ||
|
|
154
|
+
name == "for" ||
|
|
155
|
+
name == "while" ||
|
|
156
|
+
name == "switch" ||
|
|
157
|
+
name == "return" ||
|
|
158
|
+
name == "function") {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (bestDistance > Math.max(12, Math.floor(visible.length / 3))) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const start = bestMatch.index ?? 0;
|
|
165
|
+
return {
|
|
166
|
+
name,
|
|
167
|
+
start,
|
|
168
|
+
end: start + (name?.length ?? 1),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function isCoverageBoundary(ch) {
|
|
172
|
+
return /[\s()[\]{}.,;:+\-*/%&|^!?=<>]/.test(ch);
|
|
173
|
+
}
|
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/index.js
CHANGED
|
@@ -281,7 +281,7 @@ function printCommandHelp(command) {
|
|
|
281
281
|
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
282
282
|
process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
|
|
283
283
|
process.stdout.write(" --fuzz Run fuzz targets after the normal test pass\n");
|
|
284
|
-
process.stdout.write(" --fuzz-runs <
|
|
284
|
+
process.stdout.write(" --fuzz-runs <value> Override fuzz iteration count, e.g. 500, 1.5x, +10%, +100000\n");
|
|
285
285
|
process.stdout.write(" --fuzz-seed <n> Override fuzz seed for this run\n");
|
|
286
286
|
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
287
287
|
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
@@ -302,7 +302,7 @@ function printCommandHelp(command) {
|
|
|
302
302
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
303
303
|
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
304
304
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
305
|
-
process.stdout.write(" --runs <
|
|
305
|
+
process.stdout.write(" --runs <value> Override fuzz iteration count, e.g. 500, 1.5x, +10%, +100000\n");
|
|
306
306
|
process.stdout.write(" --seed <n> Override fuzz seed\n");
|
|
307
307
|
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
308
308
|
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
@@ -482,9 +482,10 @@ function resolveFuzzOverrides(rawArgs, command) {
|
|
|
482
482
|
runs: "--fuzz-runs",
|
|
483
483
|
seed: "--fuzz-seed",
|
|
484
484
|
};
|
|
485
|
-
const runs =
|
|
485
|
+
const runs = parseFuzzRunsFlag(rawArgs, i, direct.runs);
|
|
486
486
|
if (runs) {
|
|
487
|
-
out.runs = runs.
|
|
487
|
+
out.runs = runs.absoluteRuns;
|
|
488
|
+
out.runsOverride = runs.override;
|
|
488
489
|
if (runs.consumeNext)
|
|
489
490
|
i++;
|
|
490
491
|
continue;
|
|
@@ -499,6 +500,63 @@ function resolveFuzzOverrides(rawArgs, command) {
|
|
|
499
500
|
}
|
|
500
501
|
return out;
|
|
501
502
|
}
|
|
503
|
+
function parseFuzzRunsFlag(rawArgs, index, flag) {
|
|
504
|
+
const arg = rawArgs[index];
|
|
505
|
+
let value = "";
|
|
506
|
+
let consumeNext = false;
|
|
507
|
+
if (arg == flag) {
|
|
508
|
+
const next = rawArgs[index + 1];
|
|
509
|
+
if (!next || !next.length) {
|
|
510
|
+
throw new Error(`${flag} requires a value such as 500, 1.5x, +10%, or +100000`);
|
|
511
|
+
}
|
|
512
|
+
value = next;
|
|
513
|
+
consumeNext = true;
|
|
514
|
+
}
|
|
515
|
+
else if (arg.startsWith(`${flag}=`)) {
|
|
516
|
+
value = arg.slice(flag.length + 1);
|
|
517
|
+
if (!value.length) {
|
|
518
|
+
throw new Error(`${flag} requires a value such as 500, 1.5x, +10%, or +100000`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const parsed = parseFuzzRunsValue(flag, value.trim());
|
|
525
|
+
return {
|
|
526
|
+
key: flag,
|
|
527
|
+
absoluteRuns: parsed.kind == "set" ? parsed.value : undefined,
|
|
528
|
+
override: parsed,
|
|
529
|
+
consumeNext,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function parseFuzzRunsValue(flag, value) {
|
|
533
|
+
if (/^\d+$/.test(value)) {
|
|
534
|
+
const parsed = parseIntegerFlag(flag, value);
|
|
535
|
+
return { kind: "set", value: parsed };
|
|
536
|
+
}
|
|
537
|
+
if (/^[+-]\d+$/.test(value)) {
|
|
538
|
+
const delta = Number(value);
|
|
539
|
+
if (!Number.isFinite(delta) || !Number.isInteger(delta)) {
|
|
540
|
+
throw new Error(`${flag} additive run override must be an integer`);
|
|
541
|
+
}
|
|
542
|
+
return { kind: "add", value: delta };
|
|
543
|
+
}
|
|
544
|
+
if (/^\d+(?:\.\d+)?x$/i.test(value)) {
|
|
545
|
+
const factor = Number(value.slice(0, -1));
|
|
546
|
+
if (!Number.isFinite(factor) || factor <= 0) {
|
|
547
|
+
throw new Error(`${flag} multiplier must be greater than 0`);
|
|
548
|
+
}
|
|
549
|
+
return { kind: "scale", value: factor };
|
|
550
|
+
}
|
|
551
|
+
if (/^[+-]\d+(?:\.\d+)?%$/.test(value)) {
|
|
552
|
+
const percent = Number(value.slice(0, -1));
|
|
553
|
+
if (!Number.isFinite(percent)) {
|
|
554
|
+
throw new Error(`${flag} percentage must be numeric`);
|
|
555
|
+
}
|
|
556
|
+
return { kind: "percent-add", value: percent };
|
|
557
|
+
}
|
|
558
|
+
throw new Error(`${flag} must be a run count or expression such as 500, 1.5x, +10%, or +100000`);
|
|
559
|
+
}
|
|
502
560
|
function resolveListFlags(rawArgs, command) {
|
|
503
561
|
const out = {
|
|
504
562
|
list: false,
|
package/bin/reporters/default.js
CHANGED
|
@@ -3,6 +3,7 @@ import { diff } from "typer-diff";
|
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { formatTime } from "../util.js";
|
|
6
|
+
import { describeCoveragePoint, readCoverageSourceLine, resolveCoverageHighlightSpan, } from "../coverage-points.js";
|
|
6
7
|
export const createReporter = (context) => {
|
|
7
8
|
return new DefaultReporter(context);
|
|
8
9
|
};
|
|
@@ -360,7 +361,19 @@ function renderFailedFuzzers(results) {
|
|
|
360
361
|
console.log(chalk.dim(`Runs: ${fuzzer.passed + fuzzer.failed + fuzzer.crashed} completed (${fuzzer.passed} passed, ${fuzzer.failed} failed, ${fuzzer.crashed} crashed)`));
|
|
361
362
|
console.log(chalk.dim(`Repro: ${repro}`));
|
|
362
363
|
console.log(chalk.dim(`Seed: ${modeResult.seed}`));
|
|
363
|
-
if (
|
|
364
|
+
if (fuzzer.failures?.length) {
|
|
365
|
+
console.log(chalk.dim(`Failing seeds: ${formatFailingSeeds(fuzzer)}`));
|
|
366
|
+
for (const failure of fuzzer.failures) {
|
|
367
|
+
console.log(chalk.dim(`Repro ${failure.run + 1}: ${buildFuzzReproCommand(relativeFile, failure.seed, modeResult.modeName, 1)}`));
|
|
368
|
+
if (failure.input) {
|
|
369
|
+
console.log(chalk.dim(`Input ${failure.run + 1}: ${JSON.stringify(failure.input)}`));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (fuzzer.crashFile?.length) {
|
|
374
|
+
console.log(chalk.dim(`Crash: ${fuzzer.crashFile}`));
|
|
375
|
+
}
|
|
376
|
+
else if (modeResult.crashFiles.length) {
|
|
364
377
|
console.log(chalk.dim(`Crash: ${modeResult.crashFiles[0]}`));
|
|
365
378
|
}
|
|
366
379
|
console.log("");
|
|
@@ -392,9 +405,13 @@ function averageFuzzModeTime(results) {
|
|
|
392
405
|
return 0;
|
|
393
406
|
return results.reduce((sum, result) => sum + result.time, 0) / results.length;
|
|
394
407
|
}
|
|
395
|
-
function buildFuzzReproCommand(file, seed, modeName) {
|
|
408
|
+
function buildFuzzReproCommand(file, seed, modeName, runs) {
|
|
396
409
|
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
397
|
-
|
|
410
|
+
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
411
|
+
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
412
|
+
}
|
|
413
|
+
function formatFailingSeeds(fuzzer) {
|
|
414
|
+
return (fuzzer.failures ?? []).map((failure) => String(failure.seed)).join(", ");
|
|
398
415
|
}
|
|
399
416
|
function toRelativeResultPath(file) {
|
|
400
417
|
const relative = path.relative(process.cwd(), path.resolve(process.cwd(), file));
|
|
@@ -617,21 +634,60 @@ function renderSummaryLine(label, summary, layout = {
|
|
|
617
634
|
process.stdout.write(totalText.padStart(layout.totalWidth) + "\n");
|
|
618
635
|
}
|
|
619
636
|
function renderCoverageSummary(summary) {
|
|
637
|
+
console.log("");
|
|
638
|
+
console.log(chalk.bold("Coverage"));
|
|
639
|
+
if (!summary.files.length || summary.total <= 0) {
|
|
640
|
+
console.log(` ${chalk.dim("No eligible source files were tracked for coverage.")}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
620
643
|
const pct = summary.total
|
|
621
644
|
? ((summary.covered * 100) / summary.total).toFixed(2)
|
|
622
645
|
: "100.00";
|
|
646
|
+
const missingLabel = summary.uncovered == 1 ? "1 point missing" : `${summary.uncovered} points missing`;
|
|
647
|
+
const fileLabel = summary.files.length == 1 ? "1 file" : `${summary.files.length} files`;
|
|
623
648
|
const color = Number(pct) >= 90
|
|
624
649
|
? chalk.greenBright
|
|
625
650
|
: Number(pct) >= 75
|
|
626
651
|
? chalk.yellowBright
|
|
627
652
|
: chalk.redBright;
|
|
628
|
-
console.log("");
|
|
629
|
-
|
|
653
|
+
console.log(` ${color(pct + "%")} ${renderCoverageBar(summary.percent)} ${chalk.dim(`(${summary.covered}/${summary.total} covered, ${missingLabel}, ${fileLabel})`)}`);
|
|
654
|
+
const ranked = [...summary.files].sort((a, b) => {
|
|
655
|
+
if (a.percent != b.percent)
|
|
656
|
+
return a.percent - b.percent;
|
|
657
|
+
if (a.uncovered != b.uncovered)
|
|
658
|
+
return b.uncovered - a.uncovered;
|
|
659
|
+
return a.file.localeCompare(b.file);
|
|
660
|
+
});
|
|
661
|
+
console.log(chalk.bold(" File Breakdown"));
|
|
662
|
+
for (const file of ranked.slice(0, 8)) {
|
|
663
|
+
const filePct = file.total
|
|
664
|
+
? ((file.covered * 100) / file.total).toFixed(2)
|
|
665
|
+
: "100.00";
|
|
666
|
+
const fileColor = Number(filePct) >= 90
|
|
667
|
+
? chalk.greenBright
|
|
668
|
+
: Number(filePct) >= 75
|
|
669
|
+
? chalk.yellowBright
|
|
670
|
+
: chalk.redBright;
|
|
671
|
+
const suffix = file.uncovered > 0
|
|
672
|
+
? `${file.uncovered} missing`
|
|
673
|
+
: "fully covered";
|
|
674
|
+
console.log(` ${fileColor(filePct.padStart(6) + "%")} ${toRelativeResultPath(file.file).padEnd(36)} ${chalk.dim(`${file.covered}/${file.total} covered, ${suffix}`)}`);
|
|
675
|
+
}
|
|
676
|
+
if (ranked.length > 8) {
|
|
677
|
+
console.log(chalk.dim(` ... ${ranked.length - 8} more files`));
|
|
678
|
+
}
|
|
630
679
|
}
|
|
631
680
|
function renderCoveragePoints(files) {
|
|
632
681
|
console.log("");
|
|
633
|
-
console.log(chalk.bold("Coverage
|
|
682
|
+
console.log(chalk.bold("Coverage Gaps"));
|
|
634
683
|
const sortedFiles = [...files].sort((a, b) => a.file.localeCompare(b.file));
|
|
684
|
+
const missingPoints = sortedFiles.flatMap((file) => file.points
|
|
685
|
+
.filter((point) => !point.executed)
|
|
686
|
+
.map((point) => ({
|
|
687
|
+
...point,
|
|
688
|
+
displayType: describeCoveragePoint(point.file, point.line, point.column, point.type).displayType,
|
|
689
|
+
})));
|
|
690
|
+
const layout = createCoverageGapLayout(missingPoints);
|
|
635
691
|
for (const file of sortedFiles) {
|
|
636
692
|
const points = [...file.points].sort((a, b) => {
|
|
637
693
|
if (a.line != b.line)
|
|
@@ -640,10 +696,69 @@ function renderCoveragePoints(files) {
|
|
|
640
696
|
return a.column - b.column;
|
|
641
697
|
return a.type.localeCompare(b.type);
|
|
642
698
|
});
|
|
699
|
+
const missing = points.filter((point) => !point.executed);
|
|
700
|
+
if (!missing.length)
|
|
701
|
+
continue;
|
|
702
|
+
console.log(` ${chalk.bold(toRelativeResultPath(file.file))} ${chalk.dim(`(${missing.length} uncovered)`)}`);
|
|
643
703
|
for (const point of points) {
|
|
644
704
|
if (point.executed)
|
|
645
705
|
continue;
|
|
646
|
-
|
|
706
|
+
const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
|
|
707
|
+
const snippet = formatCoverageSnippet(point.file, point.line, point.column);
|
|
708
|
+
const typeLabel = describeCoveragePoint(point.file, point.line, point.column, point.type).displayType.padEnd(layout.typeWidth + 4);
|
|
709
|
+
const locationLabel = location.padEnd(layout.locationWidth + 4);
|
|
710
|
+
console.log(` ${chalk.red("x")} ${chalk.dim(typeLabel)}${chalk.dim(locationLabel)}${snippet}`);
|
|
647
711
|
}
|
|
648
712
|
}
|
|
649
713
|
}
|
|
714
|
+
function renderCoverageBar(percent) {
|
|
715
|
+
const slots = 12;
|
|
716
|
+
const filled = Math.max(0, Math.min(slots, Math.round((Math.max(0, Math.min(100, percent)) / 100) * slots)));
|
|
717
|
+
return `[${"=".repeat(filled)}${"-".repeat(slots - filled)}]`;
|
|
718
|
+
}
|
|
719
|
+
function createCoverageGapLayout(points) {
|
|
720
|
+
return {
|
|
721
|
+
typeWidth: Math.max(...points.map((point) => point.displayType.length), 5),
|
|
722
|
+
locationWidth: Math.max(...points.map((point) => `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`.length), 1),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function formatCoverageSnippet(file, line, column) {
|
|
726
|
+
const sourceLine = readCoverageSourceLine(file, line);
|
|
727
|
+
if (!sourceLine)
|
|
728
|
+
return "";
|
|
729
|
+
const expanded = sourceLine.replace(/\t/g, " ");
|
|
730
|
+
const firstNonWhitespace = expanded.search(/\S/);
|
|
731
|
+
if (firstNonWhitespace == -1)
|
|
732
|
+
return "";
|
|
733
|
+
const visible = expanded.slice(firstNonWhitespace).trimEnd();
|
|
734
|
+
if (!visible.length)
|
|
735
|
+
return "";
|
|
736
|
+
const maxWidth = 72;
|
|
737
|
+
const focus = Math.max(0, Math.min(visible.length - 1, Math.max(0, column - 1 - firstNonWhitespace)));
|
|
738
|
+
if (visible.length <= maxWidth) {
|
|
739
|
+
return styleCoverageSnippetWindow(visible, 0, visible.length, focus);
|
|
740
|
+
}
|
|
741
|
+
const start = Math.max(0, Math.min(visible.length - maxWidth, focus - Math.floor(maxWidth / 2)));
|
|
742
|
+
const end = Math.min(visible.length, start + maxWidth);
|
|
743
|
+
return styleCoverageSnippetWindow(visible, start, end, focus);
|
|
744
|
+
}
|
|
745
|
+
function styleCoverageSnippetWindow(visible, start, end, focus) {
|
|
746
|
+
const prefix = start > 0 ? "..." : "";
|
|
747
|
+
const suffix = end < visible.length ? "..." : "";
|
|
748
|
+
const slice = visible.slice(start, end);
|
|
749
|
+
const localFocus = Math.max(0, Math.min(slice.length - 1, focus - start));
|
|
750
|
+
const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(visible, focus);
|
|
751
|
+
const localStart = Math.max(0, Math.min(slice.length, highlightStart - start));
|
|
752
|
+
const localEnd = Math.max(localStart + 1, Math.min(slice.length, highlightEnd - start));
|
|
753
|
+
if (!slice.length)
|
|
754
|
+
return "";
|
|
755
|
+
if (localStart >= slice.length) {
|
|
756
|
+
return chalk.dim(`${prefix}${slice}${suffix}`);
|
|
757
|
+
}
|
|
758
|
+
const head = slice.slice(0, localStart);
|
|
759
|
+
const body = slice.slice(localStart, localEnd || localStart + 1);
|
|
760
|
+
const tail = slice.slice(localEnd || localStart + 1);
|
|
761
|
+
return (chalk.dim(prefix + head) +
|
|
762
|
+
chalk.dim.underline(body.length ? body : slice.charAt(localFocus)) +
|
|
763
|
+
chalk.dim(tail + suffix));
|
|
764
|
+
}
|
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}` : "";
|
package/bin/types.js
CHANGED
|
@@ -21,6 +21,15 @@ export class CoverageOptions {
|
|
|
21
21
|
this.includeSpecs = false;
|
|
22
22
|
this.include = [];
|
|
23
23
|
this.exclude = [];
|
|
24
|
+
this.ignore = new CoverageIgnoreOptions();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class CoverageIgnoreOptions {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.labels = [];
|
|
30
|
+
this.names = [];
|
|
31
|
+
this.locations = [];
|
|
32
|
+
this.snippets = [];
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
35
|
export class Suite {
|
package/bin/util.js
CHANGED
|
@@ -340,6 +340,22 @@ function validateCoverageValue(value, path, issues) {
|
|
|
340
340
|
}
|
|
341
341
|
validateStringArrayField(obj, "include", path, issues);
|
|
342
342
|
validateStringArrayField(obj, "exclude", path, issues);
|
|
343
|
+
if ("ignore" in obj && obj.ignore != undefined) {
|
|
344
|
+
if (!obj.ignore || typeof obj.ignore != "object" || Array.isArray(obj.ignore)) {
|
|
345
|
+
issues.push({
|
|
346
|
+
path: `${path}.ignore`,
|
|
347
|
+
message: "must be an object",
|
|
348
|
+
fix: 'set "ignore" to an object such as { "labels": ["Call"], "names": ["panic"] }',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
const ignore = obj.ignore;
|
|
353
|
+
validateStringArrayField(ignore, "labels", `${path}.ignore`, issues);
|
|
354
|
+
validateStringArrayField(ignore, "names", `${path}.ignore`, issues);
|
|
355
|
+
validateStringArrayField(ignore, "locations", `${path}.ignore`, issues);
|
|
356
|
+
validateStringArrayField(ignore, "snippets", `${path}.ignore`, issues);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
343
359
|
}
|
|
344
360
|
function validateStringArrayField(raw, key, pathPrefix, issues) {
|
|
345
361
|
if (!(key in raw) || raw[key] == undefined)
|