as-test 1.1.2 → 1.1.4

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,16 +1,30 @@
1
1
  # Change Log
2
2
 
3
+ ## 2026-05-14 - v1.1.4
4
+
5
+ - feat: add `coverage.mode` (`project` or `all`) plus `coverage.dependencies` package allowlisting so dependency coverage can include normal or pnpm-installed packages without raw path globs.
6
+
7
+ ## 2026-05-13 - v1.1.3
8
+
9
+ ### Coverage
10
+
11
+ - feat: make coverage gaps hierarchical and easier to scan, with parent-before-child grouping, tree-style connectors, collapsed nested gaps by default, and `--show-coverage=all` / `--verbose` expansion.
12
+ - feat: add richer coverage point names including `DefaultValue`, `Ternary`, `IfBranch`, `Assignment`, `Loop`, `Return`, and `Throw` so uncovered points describe the actual construct instead of falling back to broad labels.
13
+ - fix: make coverage snippets underline the emitted construct span instead of recomputing from the raw column, so cases like inline `if (...) ...` and assignments highlight the right text.
14
+
3
15
  ## 2026-05-13 - v1.1.2
4
16
 
5
- - update build and run faliures to provide helpful error messages and reproduction commands and instructions
6
- - remove confirmation from ast clean
17
+ ### Reporting & CLI
18
+
19
+ - fix: update build and run failures to provide clearer error messages plus reproduction commands and instructions.
20
+ - fix: remove the confirmation prompt from `ast clean`.
7
21
 
8
- ## 2026-05-12 -v1.1.1
22
+ ## 2026-05-12 - v1.1.1
9
23
 
10
24
  - add `ast clean` command to remove build outputs, coverage outputs, crash reports, and logs.
11
25
  - remove deps
12
26
 
13
- ## 2026-05-12 -v1.1.0
27
+ ## 2026-05-12 - v1.1.0
14
28
 
15
29
  ### Upgrading to 1.1.0
16
30
 
@@ -78,7 +92,7 @@
78
92
  - feat: make integer `FuzzSeed` helpers default to the full range of their target type when no options are provided, instead of collapsing to `0`.
79
93
  - perf: add unchecked full-range fast paths for default integer seed generation while keeping explicit user-provided ranges validated.
80
94
 
81
- ## 2025-05-03 - v1.0.13
95
+ ## 2026-05-03 - v1.0.13
82
96
 
83
97
  - 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.
84
98
  - feat: add `--suite` / `--suites` filtering for `ast run` and `ast test`, and print suite-specific repro commands on failing test assertions.
package/README.md CHANGED
@@ -16,12 +16,13 @@
16
16
  - [Why as-test](#why-as-test)
17
17
  - [Installation](#installation)
18
18
  - [Docs](#docs)
19
- - [Project Layout](#project-layout)
20
19
  - [Writing Tests](#writing-tests)
21
20
  - [Mocking](#mocking)
22
21
  - [Snapshots](#snapshots)
23
- - [Fuzzing](#fuzzing)
24
22
  - [Runtimes](#runtimes)
23
+ - [Modes](#modes)
24
+ - [Coverage](#coverage)
25
+ - [Fuzzing](#fuzzing)
25
26
  - [Examples](#examples)
26
27
  - [License](#license)
27
28
 
@@ -33,15 +34,6 @@ Most AssemblyScript testing tools are tied to a single runtime, usually Node.js.
33
34
  If you deploy to WASI, Wazero, or a custom runtime, you often end up mocking everything and maintaining parallel logic just for tests.
34
35
  as-test solves this by letting you run tests on your actual target runtime, while only mocking what’s necessary.
35
36
 
36
- Key benefits
37
-
38
- - Runtime-agnostic: test on WASI, bindings, or custom runners
39
- - Minimal mocking: keep real imports when possible
40
- - Production-like testing: catch runtime-specific issues early
41
- - Inline mocking and snapshots
42
- - Custom reporters and coverage
43
- - Integrated fuzzing support
44
-
45
37
  ## Installation
46
38
 
47
39
  The easiest way to start is with the project initializer:
@@ -64,50 +56,6 @@ Full documentation lives at:
64
56
 
65
57
  <https://docs.jairus.dev/as-test>
66
58
 
67
- ## Project Layout
68
-
69
- By default, `as-test` looks for:
70
-
71
- - tests in `assembly/__tests__`
72
- - fuzzers in `assembly/__fuzz__`
73
- - config in `as-test.config.json`
74
-
75
- Generated files go into `.as-test/`.
76
-
77
- Minimal `as-test.config.json`:
78
-
79
- ```json
80
- {
81
- "input": ["assembly/__tests__/*.spec.ts"],
82
- "output": ".as-test/",
83
- "buildOptions": {
84
- "target": "wasi"
85
- },
86
- "runOptions": {
87
- "runtime": {
88
- "cmd": "node .as-test/runners/default.wasi.js"
89
- }
90
- }
91
- }
92
- ```
93
-
94
- Coverage point filtering is configurable when you want to ignore known-noisy gaps:
95
-
96
- ```json
97
- {
98
- "coverage": {
99
- "enabled": true,
100
- "include": ["assembly/src/**/*.ts"],
101
- "ignore": {
102
- "labels": ["Call"],
103
- "names": ["panic", "serialize"],
104
- "locations": ["assembly/src/fuzz.ts:38:*"],
105
- "snippets": ["*message: string*"]
106
- }
107
- }
108
- }
109
- ```
110
-
111
59
  ## Writing Tests
112
60
 
113
61
  Tests usually live in `assembly/__tests__/*.spec.ts`.
@@ -119,7 +67,7 @@ import { describe, expect, test } from "as-test";
119
67
 
120
68
  describe("math", () => {
121
69
  test("adds numbers", () => {
122
- expect(1 + 2).toBe(3);
70
+ expect(1 + 2).toBe(3, "should add two numbers");
123
71
  });
124
72
  });
125
73
  ```
@@ -145,8 +93,8 @@ npx ast test math
145
93
  Re-run one suite inside a matching file:
146
94
 
147
95
  ```bash
148
- npx ast run math --suite array-check
149
- npx ast run math --suite array-manipulation/array-check
96
+ npx ast run math --suite math
97
+ npx ast run math --suite math/adds-numbers
150
98
  ```
151
99
 
152
100
  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.
@@ -232,96 +180,6 @@ If an existing snapshot legitimately changed, overwrite it with:
232
180
  npx ast test --overwrite-snapshots
233
181
  ```
234
182
 
235
- ## Fuzzing
236
-
237
- Fuzzers usually live in `assembly/__fuzz__/*.fuzz.ts`.
238
-
239
- Example:
240
-
241
- ```ts
242
- import { expect, FuzzSeed, fuzz } from "as-test";
243
-
244
- fuzz("bounded integer addition", (left: i32, right: i32): bool => {
245
- const sum = left + right;
246
- expect(sum - right).toBe(left);
247
- return sum >= i32.MIN_VALUE;
248
- }).generate((seed: FuzzSeed, run: (left: i32, right: i32) => bool): void => {
249
- run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
250
- });
251
- ```
252
-
253
- Pass a third argument to override the operation count for one target without changing the global fuzz config:
254
-
255
- ```ts
256
- fuzz(
257
- "hot path stays stable",
258
- (): void => {
259
- expect(1 + 1).toBe(2);
260
- },
261
- 250,
262
- );
263
- ```
264
-
265
- Or pass it as the second argument to `.generate(...)`:
266
-
267
- ```ts
268
- fuzz(
269
- "ascii strings survive concatenation boundaries",
270
- (input: string): bool => {
271
- expect(input.length <= 40).toBe(true);
272
- return true;
273
- },
274
- ).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
275
- run(seed.string({ charset: "ascii", min: 0, max: 40 }));
276
- }, 250);
277
- ```
278
-
279
- You can still override fuzz runs from the CLI when you want to force a different count for the current command:
280
-
281
- ```bash
282
- npx ast fuzz --runs 500
283
- npx ast fuzz --runs 1.5x
284
- npx ast fuzz --runs +10%
285
- npx ast fuzz --runs +100000
286
- ```
287
-
288
- 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:
289
-
290
- ```json
291
- {
292
- "fuzz": {
293
- "input": ["./assembly/__fuzz__/*.fuzz.ts"],
294
- "target": "bindings"
295
- }
296
- }
297
- ```
298
-
299
- `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`.
300
-
301
- By default, each fuzz run campaign picks a new random base seed. Pin a seed with `--seed <n>` (or `--fuzz-seed <n>` on `ast test`) when you want deterministic replay.
302
-
303
- 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.
304
-
305
- Run only fuzzers:
306
-
307
- ```bash
308
- npx ast fuzz
309
- ```
310
-
311
- Run one matching fuzz target:
312
-
313
- ```bash
314
- npx ast fuzz string --fuzzer ascii-strings-survive-concatenation-boundaries
315
- ```
316
-
317
- Run tests and fuzzers together:
318
-
319
- ```bash
320
- npx ast test --fuzz
321
- ```
322
-
323
- Fuzzing is there when you want broader input coverage, but it does not get in the way of the normal test flow. You can start with ordinary specs and add fuzzers later.
324
-
325
183
  ## Runtimes
326
184
 
327
185
  One of the main reasons to use `as-test` is that you are not locked into a single runtime.
@@ -350,7 +208,13 @@ Then run your tests normally:
350
208
  npx ast test
351
209
  ```
352
210
 
353
- If you want to keep more than one runtime around, use modes:
211
+ If you want to keep a single runtime, one config is enough. If you want to fan out across multiple runtimes, use modes.
212
+
213
+ ## Modes
214
+
215
+ Modes let one project keep more than one runtime or build target available at the same time.
216
+
217
+ For example:
354
218
 
355
219
  ```json
356
220
  {
@@ -389,8 +253,12 @@ Set `"default": false` on a mode when you want to keep it available for explicit
389
253
  "modes": {
390
254
  "web": {
391
255
  "default": false,
256
+ "buildOptions": {
257
+ "target": "web"
258
+ },
392
259
  "runOptions": {
393
260
  "runtime": {
261
+ "cmd": "node ./.as-test/runners/default.web.js",
394
262
  "browser": "chromium"
395
263
  }
396
264
  }
@@ -423,8 +291,12 @@ Modes can also be full config objects. That means a mode can override fuzzing, i
423
291
  "input": ["./assembly/__fuzz__/web/*.fuzz.ts"],
424
292
  "runs": 200
425
293
  },
294
+ "buildOptions": {
295
+ "target": "web"
296
+ },
426
297
  "runOptions": {
427
298
  "runtime": {
299
+ "cmd": "node ./.as-test/runners/default.web.js",
428
300
  "browser": "chromium"
429
301
  }
430
302
  }
@@ -455,6 +327,135 @@ or
455
327
  npx ast test --mode wasi,bindings
456
328
  ```
457
329
 
330
+ ## Coverage
331
+
332
+ Coverage is opt-in.
333
+
334
+ Enable it from the CLI:
335
+
336
+ ```bash
337
+ npx ast test --enable coverage
338
+ npx ast test --enable coverage --show-coverage
339
+ npx ast test --enable coverage --show-coverage=all
340
+ ```
341
+
342
+ Or from config:
343
+
344
+ ```json
345
+ {
346
+ "coverage": {
347
+ "enabled": true,
348
+ "mode": "project",
349
+ "dependencies": ["json-as"],
350
+ "includeSpecs": false
351
+ }
352
+ }
353
+ ```
354
+
355
+ Coverage modes:
356
+
357
+ - `project`
358
+ - covers project files only
359
+ - excludes dependency files by default
360
+ - `all`
361
+ - covers project files and dependency files
362
+ - still excludes AssemblyScript stdlib files
363
+
364
+ If you only want specific dependencies, keep `mode: "project"` and list package names in `dependencies`.
365
+ That works for both normal installs and `pnpm` layouts.
366
+
367
+ `--show-coverage` prints uncovered point details. `--show-coverage=all` and `--verbose` expand nested uncovered gaps instead of collapsing them.
368
+
369
+ ## Fuzzing
370
+
371
+ Fuzzers usually live in `assembly/__fuzz__/*.fuzz.ts`.
372
+
373
+ Example:
374
+
375
+ ```ts
376
+ import { expect, FuzzSeed, fuzz } from "as-test";
377
+
378
+ fuzz("bounded integer addition", (left: i32, right: i32): bool => {
379
+ const sum = left + right;
380
+ expect(sum - right).toBe(left);
381
+ return sum >= i32.MIN_VALUE;
382
+ }).generate((seed: FuzzSeed, run: (left: i32, right: i32) => bool): void => {
383
+ run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
384
+ });
385
+ ```
386
+
387
+ Pass a third argument to override the operation count for one target without changing the global fuzz config:
388
+
389
+ ```ts
390
+ fuzz(
391
+ "hot path stays stable",
392
+ (): void => {
393
+ expect(1 + 1).toBe(2);
394
+ },
395
+ 250,
396
+ );
397
+ ```
398
+
399
+ Or pass it as the second argument to `.generate(...)`:
400
+
401
+ ```ts
402
+ fuzz(
403
+ "ascii strings survive concatenation boundaries",
404
+ (input: string): bool => {
405
+ expect(input.length <= 40).toBe(true);
406
+ return true;
407
+ },
408
+ ).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
409
+ run(seed.string({ charset: "ascii", min: 0, max: 40 }));
410
+ }, 250);
411
+ ```
412
+
413
+ You can still override fuzz runs from the CLI when you want to force a different count for the current command:
414
+
415
+ ```bash
416
+ npx ast fuzz --runs 500
417
+ npx ast fuzz --runs 1.5x
418
+ npx ast fuzz --runs +10%
419
+ npx ast fuzz --runs +100000
420
+ ```
421
+
422
+ 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:
423
+
424
+ ```json
425
+ {
426
+ "fuzz": {
427
+ "input": ["./assembly/__fuzz__/*.fuzz.ts"],
428
+ "target": "bindings"
429
+ }
430
+ }
431
+ ```
432
+
433
+ `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`.
434
+
435
+ By default, each fuzz run campaign picks a new random base seed. Pin a seed with `--seed <n>` (or `--fuzz-seed <n>` on `ast test`) when you want deterministic replay.
436
+
437
+ 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.
438
+
439
+ Run only fuzzers:
440
+
441
+ ```bash
442
+ npx ast fuzz
443
+ ```
444
+
445
+ Run one matching fuzz target:
446
+
447
+ ```bash
448
+ npx ast fuzz string --fuzzer ascii-strings-survive-concatenation-boundaries
449
+ ```
450
+
451
+ Run tests and fuzzers together:
452
+
453
+ ```bash
454
+ npx ast test --fuzz
455
+ ```
456
+
457
+ Fuzzing is there when you want broader input coverage, but it does not get in the way of the normal test flow. You can start with ordinary specs and add fuzzers later.
458
+
458
459
  This is the general idea throughout the project: write tests once, then choose the runtime that matches how your code actually runs.
459
460
 
460
461
  ## Examples
@@ -101,11 +101,25 @@
101
101
  "type": "boolean",
102
102
  "default": false
103
103
  },
104
+ "mode": {
105
+ "type": "string",
106
+ "enum": ["project", "all"],
107
+ "description": "Broad coverage eligibility. \"project\" covers project sources only. \"all\" also includes dependency sources.",
108
+ "default": "project"
109
+ },
104
110
  "includeSpecs": {
105
111
  "type": "boolean",
106
112
  "description": "Include *.spec.ts files in coverage collection.",
107
113
  "default": false
108
114
  },
115
+ "dependencies": {
116
+ "type": "array",
117
+ "description": "Package-name allowlist for dependency coverage, including pnpm-installed packages.",
118
+ "items": {
119
+ "type": "string"
120
+ },
121
+ "default": []
122
+ },
109
123
  "include": {
110
124
  "type": "array",
111
125
  "description": "Only include matching source files in coverage reports when this list is not empty.",
@@ -369,8 +383,18 @@
369
383
  "enabled": {
370
384
  "type": "boolean"
371
385
  },
386
+ "mode": {
387
+ "type": "string",
388
+ "enum": ["project", "all"]
389
+ },
372
390
  "includeSpecs": {
373
391
  "type": "boolean"
392
+ },
393
+ "dependencies": {
394
+ "type": "array",
395
+ "items": {
396
+ "type": "string"
397
+ }
374
398
  }
375
399
  }
376
400
  }
@@ -4,6 +4,10 @@ export class CoverPoint {
4
4
  public line: i32 = 0;
5
5
  public column: i32 = 0;
6
6
  public type: string = "";
7
+ public parentHash: string = "";
8
+ public scopeKind: string = "";
9
+ public scopeName: string = "";
10
+ public depth: i32 = 0;
7
11
  public executed: boolean = false;
8
12
  }
9
13
 
@@ -29,6 +33,10 @@ export function __REGISTER_RAW(
29
33
  line: i32,
30
34
  column: i32,
31
35
  type: string,
36
+ parentHash: string = "",
37
+ scopeKind: string = "",
38
+ scopeName: string = "",
39
+ depth: i32 = 0,
32
40
  ): void {
33
41
  if (Coverage.SN.allIndex.has(hash)) return;
34
42
  const point = new CoverPoint();
@@ -37,6 +45,10 @@ export function __REGISTER_RAW(
37
45
  point.line = line;
38
46
  point.column = column;
39
47
  point.type = type;
48
+ point.parentHash = parentHash;
49
+ point.scopeKind = scopeKind;
50
+ point.scopeName = scopeName;
51
+ point.depth = depth;
40
52
  Coverage.SN.points++;
41
53
  Coverage.SN.allIndex.set(hash, Coverage.SN.all.length);
42
54
  Coverage.SN.all.push(point);
package/assembly/index.ts CHANGED
@@ -548,6 +548,10 @@ class CoveragePointReport {
548
548
  column: i32 = 0;
549
549
  type: string = "";
550
550
  executed: bool = false;
551
+ parentHash: string = "";
552
+ scopeKind: string = "";
553
+ scopeName: string = "";
554
+ depth: i32 = 0;
551
555
 
552
556
  serialize(): string {
553
557
  return (
@@ -563,6 +567,14 @@ class CoveragePointReport {
563
567
  quote(this.type) +
564
568
  ',"executed":' +
565
569
  (this.executed ? "true" : "false") +
570
+ ',"parentHash":' +
571
+ quote(this.parentHash) +
572
+ ',"scopeKind":' +
573
+ quote(this.scopeKind) +
574
+ ',"scopeName":' +
575
+ quote(this.scopeName) +
576
+ ',"depth":' +
577
+ this.depth.toString() +
566
578
  "}"
567
579
  );
568
580
  }
@@ -632,6 +644,10 @@ function toCoveragePointReport(point: CoverPoint): CoveragePointReport {
632
644
  out.column = point.column;
633
645
  out.type = point.type;
634
646
  out.executed = point.executed;
647
+ out.parentHash = point.parentHash;
648
+ out.scopeKind = point.scopeKind;
649
+ out.scopeName = point.scopeName;
650
+ out.depth = point.depth;
635
651
  return out;
636
652
  }
637
653
 
@@ -131,6 +131,8 @@ export class Suite {
131
131
  this.verdict = "ok";
132
132
  } else if (hasSkip) {
133
133
  this.verdict = "skip";
134
+ } else if (isTestCase) {
135
+ this.verdict = "ok";
134
136
  } else {
135
137
  this.verdict = "none";
136
138
  }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "as-test";
2
+ // Import JSON and bs directly from json-as.
3
+ // bs import prevents json-as transform from adding broken pnpm paths.
4
+ import { JSON } from "json-as/assembly";
5
+
6
+
7
+ @json
8
+ class SimpleData {
9
+ name: string = "";
10
+ count: i32 = 0;
11
+ }
12
+
13
+
14
+ @json
15
+ class NestedData {
16
+ id: string = "";
17
+ items: string[] = [];
18
+ }
19
+
20
+ describe("JSON", () => {
21
+ describe("stringify", () => {
22
+ it("should serialize a simple class", () => {
23
+ const data = new SimpleData();
24
+ data.name = "test";
25
+ data.count = 42;
26
+
27
+ const json = JSON.stringify(data);
28
+
29
+ expect(json).toBe('{"name":"test","count":42}');
30
+ });
31
+
32
+ it("should serialize a class with arrays", () => {
33
+ const data = new NestedData();
34
+ data.id = "abc123";
35
+ data.items = ["item1", "item2"];
36
+
37
+ const json = JSON.stringify(data);
38
+
39
+ expect(json).toBe('{"id":"abc123","items":["item1","item2"]}');
40
+ });
41
+
42
+ it("should serialize primitive types", () => {
43
+ expect(JSON.stringify<i32>(42)).toBe("42");
44
+ expect(JSON.stringify<bool>(true)).toBe("true");
45
+ expect(JSON.stringify<string>("hello")).toBe('"hello"');
46
+ });
47
+
48
+ it("should serialize arrays", () => {
49
+ const arr: i32[] = [1, 2, 3];
50
+ expect(JSON.stringify(arr)).toBe("[1,2,3]");
51
+ });
52
+ });
53
+
54
+ describe("parse", () => {
55
+ it("should deserialize a simple class", () => {
56
+ const json = '{"name":"test","count":42}';
57
+ const data = JSON.parse<SimpleData>(json);
58
+
59
+ expect(data.name).toBe("test");
60
+ expect(data.count).toBe(42);
61
+ });
62
+
63
+ it("should deserialize a class with arrays", () => {
64
+ const json = '{"id":"abc123","items":["item1","item2"]}';
65
+ const data = JSON.parse<NestedData>(json);
66
+
67
+ expect(data.id).toBe("abc123");
68
+ expect(data.items.length).toBe(2);
69
+ expect(data.items[0]).toBe("item1");
70
+ expect(data.items[1]).toBe("item2");
71
+ });
72
+
73
+ it("should round-trip data correctly", () => {
74
+ const original = new SimpleData();
75
+ original.name = "round-trip";
76
+ original.count = 123;
77
+
78
+ const json = JSON.stringify(original);
79
+ const restored = JSON.parse<SimpleData>(json);
80
+
81
+ expect(restored.name).toBe(original.name);
82
+ expect(restored.count).toBe(original.count);
83
+ });
84
+ });
85
+ });