as-test 1.0.6 → 1.0.9

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
+ ## 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
+
3
16
  ## 2026-03-31 - v1.0.6
4
17
 
5
18
  ### Fuzzing
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,34 @@ 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
+ Or pass it as the second argument to `.generate(...)`:
247
+
248
+ ```ts
249
+ fuzz("ascii strings survive concatenation boundaries", (input: string): bool => {
250
+ expect(input.length <= 40).toBe(true);
251
+ return true;
252
+ }).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
253
+ run(seed.string({ charset: "ascii", min: 0, max: 40 }));
254
+ }, 250);
255
+ ```
256
+
257
+ You can still override fuzz runs from the CLI when you want to force a different count for the current command:
258
+
259
+ ```bash
260
+ npx ast fuzz --runs 500
261
+ npx ast fuzz --runs 1.5x
262
+ npx ast fuzz --runs +10%
263
+ npx ast fuzz --runs +100000
264
+ ```
265
+
221
266
  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
267
 
223
268
  ```json
@@ -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
  }
@@ -18,4 +18,4 @@ fuzz(
18
18
  exclude: [0x00, 0x0a, 0x0d],
19
19
  }),
20
20
  );
21
- });
21
+ }, 250);
@@ -0,0 +1,60 @@
1
+ export {};
2
+
3
+ declare module "as-test" {
4
+ export interface IntellisenseIntegerOptions {
5
+ min?: number;
6
+ max?: number;
7
+ exclude?: number[];
8
+ }
9
+
10
+ export interface IntellisenseFloatOptions {
11
+ min?: number;
12
+ max?: number;
13
+ exclude?: number[];
14
+ }
15
+
16
+ export interface IntellisenseBytesOptions {
17
+ min?: number;
18
+ max?: number;
19
+ include?: number[];
20
+ exclude?: number[];
21
+ }
22
+
23
+ export interface IntellisenseStringOptions {
24
+ charset?:
25
+ | "ascii"
26
+ | "alpha"
27
+ | "alnum"
28
+ | "digit"
29
+ | "hex"
30
+ | "base64"
31
+ | "identifier"
32
+ | "whitespace"
33
+ | "custom";
34
+ min?: number;
35
+ max?: number;
36
+ include?: number[];
37
+ exclude?: number[];
38
+ prefix?: string;
39
+ suffix?: string;
40
+ }
41
+
42
+ export interface IntellisenseArrayOptions {
43
+ min?: number;
44
+ max?: number;
45
+ }
46
+
47
+ export interface FuzzSeed {
48
+ i32(options?: IntellisenseIntegerOptions): number;
49
+ u32(options?: IntellisenseIntegerOptions): number;
50
+ f32(options?: IntellisenseFloatOptions): number;
51
+ f64(options?: IntellisenseFloatOptions): number;
52
+ bytes(options?: IntellisenseBytesOptions): Uint8Array;
53
+ buffer(options?: IntellisenseBytesOptions): ArrayBuffer;
54
+ string(options?: IntellisenseStringOptions): string;
55
+ array<T>(
56
+ item: (seed: FuzzSeed) => T,
57
+ options?: IntellisenseArrayOptions,
58
+ ): Array<T>;
59
+ }
60
+ }
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(config.seed, config.runs);
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,
@@ -186,15 +186,22 @@ export class FuzzSeed {
186
186
  export abstract class FuzzerBase {
187
187
  public name: string;
188
188
  public skipped: bool;
189
- constructor(name: string, skipped: bool = false) {
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
- generate<T extends Function>(_generator: T): this {
196
+ generate<T extends Function>(_generator: T, operations: i32 = 0): this {
197
+ if (operations > 0) this.operations = operations;
195
198
  return this;
196
199
  }
197
200
 
201
+ runsOr(defaultRuns: i32): i32 {
202
+ return this.operations > 0 ? this.operations : defaultRuns;
203
+ }
204
+
198
205
  abstract run(seed: u64, runs: i32): FuzzerResult;
199
206
  }
200
207
 
@@ -377,19 +384,25 @@ export class Fuzzer0<R> extends FuzzerBase {
377
384
  name: string,
378
385
  private callback: () => R,
379
386
  skipped: bool = false,
387
+ operations: i32 = 0,
380
388
  ) {
381
- super(name, skipped);
389
+ super(name, skipped, operations);
382
390
  this.returnsBool = !isVoid<R>();
383
391
  }
384
392
 
385
- generate<T extends Function>(generator: T): this {
393
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
386
394
  this.generator =
387
395
  changetype<(seed: FuzzSeed, run: () => R) => void>(generator);
396
+ if (operations > 0) this.operations = operations;
388
397
  return this;
389
398
  }
390
399
 
391
- generateTyped(generator: (seed: FuzzSeed, run: () => R) => void): this {
400
+ generateTyped(
401
+ generator: (seed: FuzzSeed, run: () => R) => void,
402
+ operations: i32 = 0,
403
+ ): this {
392
404
  this.generator = generator;
405
+ if (operations > 0) this.operations = operations;
393
406
  return this;
394
407
  }
395
408
 
@@ -431,19 +444,25 @@ export class Fuzzer1<A, R> extends FuzzerBase {
431
444
  name: string,
432
445
  private callback: (a: A) => R,
433
446
  skipped: bool = false,
447
+ operations: i32 = 0,
434
448
  ) {
435
- super(name, skipped);
449
+ super(name, skipped, operations);
436
450
  this.returnsBool = !isVoid<R>();
437
451
  }
438
452
 
439
- generate<T extends Function>(generator: T): this {
453
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
440
454
  this.generator =
441
455
  changetype<(seed: FuzzSeed, run: (a: A) => R) => void>(generator);
456
+ if (operations > 0) this.operations = operations;
442
457
  return this;
443
458
  }
444
459
 
445
- generateTyped(generator: (seed: FuzzSeed, run: (a: A) => R) => void): this {
460
+ generateTyped(
461
+ generator: (seed: FuzzSeed, run: (a: A) => R) => void,
462
+ operations: i32 = 0,
463
+ ): this {
446
464
  this.generator = generator;
465
+ if (operations > 0) this.operations = operations;
447
466
  return this;
448
467
  }
449
468
 
@@ -494,21 +513,25 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
494
513
  name: string,
495
514
  private callback: (a: A, b: B) => R,
496
515
  skipped: bool = false,
516
+ operations: i32 = 0,
497
517
  ) {
498
- super(name, skipped);
518
+ super(name, skipped, operations);
499
519
  this.returnsBool = !isVoid<R>();
500
520
  }
501
521
 
502
- generate<T extends Function>(generator: T): this {
522
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
503
523
  this.generator =
504
524
  changetype<(seed: FuzzSeed, run: (a: A, b: B) => R) => void>(generator);
525
+ if (operations > 0) this.operations = operations;
505
526
  return this;
506
527
  }
507
528
 
508
529
  generateTyped(
509
530
  generator: (seed: FuzzSeed, run: (a: A, b: B) => R) => void,
531
+ operations: i32 = 0,
510
532
  ): this {
511
533
  this.generator = generator;
534
+ if (operations > 0) this.operations = operations;
512
535
  return this;
513
536
  }
514
537
 
@@ -559,20 +582,24 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
559
582
  name: string,
560
583
  private callback: (a: A, b: B, c: C) => R,
561
584
  skipped: bool = false,
585
+ operations: i32 = 0,
562
586
  ) {
563
- super(name, skipped);
587
+ super(name, skipped, operations);
564
588
  this.returnsBool = !isVoid<R>();
565
589
  }
566
590
 
567
- generate<T extends Function>(generator: T): this {
591
+ generate<T extends Function>(generator: T, operations: i32 = 0): this {
568
592
  this.generator = changetype<usize>(generator);
593
+ if (operations > 0) this.operations = operations;
569
594
  return this;
570
595
  }
571
596
 
572
597
  generateTyped(
573
598
  generator: (seed: FuzzSeed, run: (a: A, b: B, c: C) => R) => void,
599
+ operations: i32 = 0,
574
600
  ): this {
575
601
  this.generator = changetype<usize>(generator);
602
+ if (operations > 0) this.operations = operations;
576
603
  return this;
577
604
  }
578
605
 
@@ -629,16 +656,23 @@ export function createFuzzer<T extends Function>(
629
656
  name: string,
630
657
  callback: T,
631
658
  skipped: bool = false,
659
+ operations: i32 = 0,
632
660
  ): FuzzerBase {
633
661
  const length = callback.length;
634
662
  if (length == 0) {
635
- return new Fuzzer0<usize>(name, changetype<() => usize>(callback), skipped);
663
+ return new Fuzzer0<usize>(
664
+ name,
665
+ changetype<() => usize>(callback),
666
+ skipped,
667
+ operations,
668
+ );
636
669
  }
637
670
  if (length == 1) {
638
671
  return new Fuzzer1<usize, usize>(
639
672
  name,
640
673
  changetype<(a: usize) => usize>(callback),
641
674
  skipped,
675
+ operations,
642
676
  );
643
677
  }
644
678
  if (length == 2) {
@@ -646,6 +680,7 @@ export function createFuzzer<T extends Function>(
646
680
  name,
647
681
  changetype<(a: usize, b: usize) => usize>(callback),
648
682
  skipped,
683
+ operations,
649
684
  );
650
685
  }
651
686
  if (length == 3) {
@@ -653,10 +688,16 @@ export function createFuzzer<T extends Function>(
653
688
  name,
654
689
  changetype<(a: usize, b: usize, c: usize) => usize>(callback),
655
690
  skipped,
691
+ operations,
656
692
  );
657
693
  }
658
694
  panic();
659
- return new Fuzzer0<usize>(name, changetype<() => usize>(callback), skipped);
695
+ return new Fuzzer0<usize>(
696
+ name,
697
+ changetype<() => usize>(callback),
698
+ skipped,
699
+ operations,
700
+ );
660
701
  }
661
702
 
662
703
  function buildAlphabet(options: StringOptions): i32[] {
@@ -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 sep = body.indexOf("\n");
145
- if (sep < 0) return new FuzzConfigReply();
146
+ const first = body.indexOf("\n");
147
+ if (first < 0) return new FuzzConfigReply();
146
148
  const reply = new FuzzConfigReply();
147
- const runs = body.slice(0, sep);
148
- const seed = body.slice(sep + 1);
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, overrides);
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
- respond(`${config.runs}\n${config.seed}`);
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("");