as-test 1.3.0 → 1.4.0
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 +41 -0
- package/README.md +1 -4
- package/assembly/index.ts +66 -47
- package/assembly/src/expectation.ts +44 -86
- package/assembly/src/fuzz.ts +10 -10
- package/assembly/src/log.ts +3 -3
- package/assembly/src/reflect.ts +122 -0
- package/assembly/src/stringify.ts +240 -0
- package/assembly/src/suite.ts +48 -27
- package/assembly/src/tests.ts +7 -7
- package/assembly/util/wipc.ts +2 -2
- package/bin/build-worker-pool.js +9 -0
- package/bin/build-worker.js +27 -3
- package/bin/commands/build-core.js +144 -82
- package/bin/commands/init-core.js +0 -3
- package/bin/commands/run-core.js +165 -41
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +2 -1
- package/bin/dependency-graph.js +0 -0
- package/bin/index.js +534 -79
- package/bin/reporters/default.js +34 -0
- package/bin/util.js +9 -0
- package/package.json +3 -7
- package/transform/lib/equals.js +388 -0
- package/transform/lib/index.js +2 -0
- package/transform/lib/log.js +3 -7
- package/transform/lib/types.js +4 -2
- package/transform/lib/transform.js +0 -502
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 2026-05-26 - v1.4.0
|
|
4
|
+
|
|
5
|
+
### Dependency-free value serialization (json-as removed)
|
|
6
|
+
|
|
7
|
+
- feat: as-test no longer depends on or auto-includes `json-as`. Value serialization for assertion reports, snapshots, and `log()` is now handled by a small in-tree stringifier (`assembly/src/stringify.ts`), so `npm install --save-dev as-test` is all you need — `json-as` is no longer a peer dependency.
|
|
8
|
+
- feat: `stringify<T>` renders a broad set of built-in types as JSON: booleans, integers, floats, strings (RFC 8259 escaping, including UTF-16 surrogate handling), `null`, `Date` (quoted ISO-8601), `ArrayBuffer` (array of unsigned byte values), typed arrays / `ArrayBufferView` (element array), `Array`, `StaticArray`, `Set` (value array), and `Map` (JSON object; non-string keys are coerced to quoted strings). Classes render via a transform-generated `toJSON()` or a `"<TypeName>"` placeholder.
|
|
9
|
+
- chore: removed the json-as peer-advisor (`transform/lib/peer-advisor.js`) and the json-as transform-passthrough integration test. Classes decorated `@json`/`@serializable` are skipped by the toJSON injector, so users who want json-as serialization can still add their own `toJSON()` and wire up `--transform json-as`.
|
|
10
|
+
|
|
11
|
+
### Structural deep equality for matchers
|
|
12
|
+
|
|
13
|
+
- feat: `.toBe()`, `.toEqual()`, and `.toStrictEqual()` now compare by structure rather than by reference. The EqualsTransform (`transform/lib/equals.js`) synthesises an `__as_test_equals(...)` method for every class that appears as an `expect()`/matcher operand, including nested classes reachable through their fields; the runtime entry point (`assembly/src/reflect.ts`'s `reflectEquals`) handles primitives, nullables, arrays, managed dispatch, and cycle detection, with a strict mode that also checks runtime type ids.
|
|
14
|
+
- feat: hand-written `__as_test_equals` methods are left untouched by the transform, and inheritance chains are supported via a super-call ignore-list pattern.
|
|
15
|
+
|
|
16
|
+
### Surfacing `log()` output
|
|
17
|
+
|
|
18
|
+
- feat: after a run, as-test now reports how many `log()` lines were captured and writes them to a single aggregated `.as-test/logs/latest.log`, e.g. `19 logs captured → .as-test/logs/latest.log`. The file groups logs by spec and de-duplicates identical output across modes, tagging each block with the modes that produced it: `[LOG] log.spec.ts (node:bindings, node:wasi):`.
|
|
19
|
+
- feat: `ast test --show-logs` (also on `ast run`) prints the captured logs as a clean grouped block at the end of the run instead of pointing at the file. In a normal run logs stay quiet (just the hint line); `--verbose` and non-TTY output still stream them inline as before.
|
|
20
|
+
- fix: the per-spec readable log's `Log:` section was always empty — it read a `value`/`message` field that never existed on log entries (the field is `text`). It now contains the captured logs.
|
|
21
|
+
|
|
22
|
+
### Suite / test counting
|
|
23
|
+
|
|
24
|
+
- change: every grouping block — `describe`, `test`, `it`, `only` (and their skip variants) — now counts as a **suite**, and each `expect()` assertion counts as a **test**. Previously `test`/`it`/`only` weren't counted as suites and an empty one was tallied as a single test, so an `it()` that contained assertions reported `Suites: 0`. As a result a top-level `it()`/`test()` failure now also appears in the end-of-run failure summary (with its location), instead of only the inline assertion line.
|
|
25
|
+
- fix: nested grouping blocks now actually nest. A `describe`/`it`/`test` declared inside another block is parented to the block whose callback is running (`current_suite`) rather than to a stale depth-indexed stack, so `describe`-in-`describe` no longer flattens (the inner block's children were previously attached to the outer block, leaving the inner one empty). The unused `suites`/`depth` registration globals were removed.
|
|
26
|
+
|
|
27
|
+
### Scoped beforeEach / afterEach
|
|
28
|
+
|
|
29
|
+
- feat: `beforeEach` and `afterEach` take an optional second argument listing the suite kinds they fire around — `beforeEach(() => {}, ["describe", "test"])`. With no argument the behavior is unchanged: hooks run around test cases (`test` / `it` / `only` and skip variants) and not around grouping blocks like `describe`.
|
|
30
|
+
|
|
31
|
+
### Watch mode + dependency graph
|
|
32
|
+
|
|
33
|
+
- feat: `ast test --watch` (`-w`) tracks a per-spec dependency graph (`cli/dependency-graph.ts`) built from the files `asc` actually loads during each build, so editing a shared helper re-runs only the specs that depend on it instead of the whole suite. `asc`'s bundled stdlib and the on-disk package are excluded from the graph to keep it small.
|
|
34
|
+
- feat: press `w` in watch mode to toggle auto-run off (manual invocation) and back on. While paused, edits are remembered but not run — invoke runs yourself with `a` (all) or `space` (retry failing); the footer shows how many changes are pending. Resuming re-runs everything if anything changed while paused.
|
|
35
|
+
|
|
36
|
+
### Breaking
|
|
37
|
+
|
|
38
|
+
- chore: `json-as` is no longer installed or auto-included by as-test. Projects that relied on as-test pulling in `json-as`, or on json-as-shaped serialization output in reports/snapshots, should install `json-as` themselves and add a `toJSON()` to the relevant classes. Existing snapshots whose serialized form changed will need `--overwrite-snapshots` once.
|
|
39
|
+
|
|
40
|
+
### Tooling
|
|
41
|
+
|
|
42
|
+
- ci: integration tests updated for the new serialization/equality paths (`tests/coverage-points.test.mjs`, new `tests/try-as-dedupe.test.mjs` and `tests/dependency-graph.test.mjs`).
|
|
43
|
+
|
|
3
44
|
## 2026-05-22 - v1.3.0
|
|
4
45
|
|
|
5
46
|
### `features` config array + arbitrary `--enable` passthrough
|
package/README.md
CHANGED
|
@@ -39,12 +39,9 @@ That gives you a basic config file, a sample test, and optionally a sample fuzze
|
|
|
39
39
|
If you already have a project and just want the package:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
npm install --save-dev as-test
|
|
42
|
+
npm install --save-dev as-test
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
`json-as` is a required peer dependency (used for value serialization in
|
|
46
|
-
assertions, snapshots, and `log()`)
|
|
47
|
-
|
|
48
45
|
## Docs
|
|
49
46
|
|
|
50
47
|
Full documentation lives at:
|
package/assembly/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
sendFileStart,
|
|
15
15
|
sendReport,
|
|
16
16
|
} from "./util/wipc";
|
|
17
|
-
import {
|
|
17
|
+
import { escape, stringify } from "./src/stringify";
|
|
18
18
|
import { bold, green, red } from "./util/format";
|
|
19
19
|
import {
|
|
20
20
|
createFuzzer,
|
|
@@ -36,7 +36,8 @@ export {
|
|
|
36
36
|
IntegerOptions,
|
|
37
37
|
StringOptions,
|
|
38
38
|
} from "./src/fuzz";
|
|
39
|
-
export {
|
|
39
|
+
export { reflectEquals } from "./src/reflect";
|
|
40
|
+
export { stringify as __as_test_stringify } from "./src/stringify";
|
|
40
41
|
|
|
41
42
|
let entrySuites: Suite[] = [];
|
|
42
43
|
let entryFuzzers: FuzzerBase[] = [];
|
|
@@ -56,10 +57,6 @@ const FILE = isDefined(ENTRY_FILE) ? ENTRY_FILE : "unknown";
|
|
|
56
57
|
string
|
|
57
58
|
>();
|
|
58
59
|
// @ts-ignore
|
|
59
|
-
@global let suites: Suite[] = [];
|
|
60
|
-
// @ts-ignore
|
|
61
|
-
@global let depth: i32 = -1;
|
|
62
|
-
// @ts-ignore
|
|
63
60
|
@global let current_suite: Suite | null = null;
|
|
64
61
|
// @ts-ignore
|
|
65
62
|
let before_all_callback: (() => void) | null = null;
|
|
@@ -68,6 +65,11 @@ let after_all_callback: (() => void) | null = null;
|
|
|
68
65
|
|
|
69
66
|
export let before_each_callback: (() => void) | null = null;
|
|
70
67
|
export let after_each_callback: (() => void) | null = null;
|
|
68
|
+
// Suite kinds each hook fires before/after. `null` = the default set (test
|
|
69
|
+
// cases: test / it / only and their skip variants), excluding grouping blocks
|
|
70
|
+
// like `describe`. A caller-supplied list overrides this.
|
|
71
|
+
export let before_each_kinds: string[] | null = null;
|
|
72
|
+
export let after_each_kinds: string[] | null = null;
|
|
71
73
|
let __test_options!: RunOptions;
|
|
72
74
|
|
|
73
75
|
/**
|
|
@@ -240,7 +242,7 @@ export function xexpect<T>(
|
|
|
240
242
|
*/
|
|
241
243
|
export function log<T>(data: T): void {
|
|
242
244
|
if (!__as_test_log_is_enabled()) return;
|
|
243
|
-
__as_test_log_serialized(
|
|
245
|
+
__as_test_log_serialized(stringify<T>(data));
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
export function __as_test_log_is_enabled(): bool {
|
|
@@ -276,21 +278,43 @@ export function afterAll(callback: () => void): void {
|
|
|
276
278
|
}
|
|
277
279
|
|
|
278
280
|
/**
|
|
279
|
-
* Registers a callback
|
|
281
|
+
* Registers a callback to run before each matching block.
|
|
282
|
+
*
|
|
283
|
+
* By default it runs before each test case (`test` / `it` / `only`, plus their
|
|
284
|
+
* skip variants) and NOT before grouping blocks like `describe`. Pass `kinds`
|
|
285
|
+
* to run before exactly the listed suite kinds instead, e.g.
|
|
286
|
+
* `beforeEach(() => {}, ["describe", "test"])`.
|
|
280
287
|
*
|
|
281
|
-
* @param {() => void} callback - The function to
|
|
288
|
+
* @param {() => void} callback - The function to run.
|
|
289
|
+
* @param {string[] | null} kinds - Suite kinds to run before, or `null` for the
|
|
290
|
+
* default test-case kinds.
|
|
282
291
|
*/
|
|
283
|
-
export function beforeEach(
|
|
292
|
+
export function beforeEach(
|
|
293
|
+
callback: () => void,
|
|
294
|
+
kinds: string[] | null = null,
|
|
295
|
+
): void {
|
|
284
296
|
before_each_callback = callback;
|
|
297
|
+
before_each_kinds = kinds;
|
|
285
298
|
}
|
|
286
299
|
|
|
287
300
|
/**
|
|
288
|
-
* Registers a callback
|
|
301
|
+
* Registers a callback to run after each matching block.
|
|
289
302
|
*
|
|
290
|
-
*
|
|
303
|
+
* By default it runs after each test case (`test` / `it` / `only`, plus their
|
|
304
|
+
* skip variants) and NOT after grouping blocks like `describe`. Pass `kinds`
|
|
305
|
+
* to run after exactly the listed suite kinds instead, e.g.
|
|
306
|
+
* `afterEach(() => {}, ["describe", "test"])`.
|
|
307
|
+
*
|
|
308
|
+
* @param {() => void} callback - The function to run.
|
|
309
|
+
* @param {string[] | null} kinds - Suite kinds to run after, or `null` for the
|
|
310
|
+
* default test-case kinds.
|
|
291
311
|
*/
|
|
292
|
-
export function afterEach(
|
|
312
|
+
export function afterEach(
|
|
313
|
+
callback: () => void,
|
|
314
|
+
kinds: string[] | null = null,
|
|
315
|
+
): void {
|
|
293
316
|
after_each_callback = callback;
|
|
317
|
+
after_each_kinds = kinds;
|
|
294
318
|
}
|
|
295
319
|
|
|
296
320
|
/**
|
|
@@ -374,10 +398,7 @@ export function run(options: RunOptions = new RunOptions()): void {
|
|
|
374
398
|
for (let i = 0; i < entrySuites.length; i++) {
|
|
375
399
|
// @ts-ignore
|
|
376
400
|
const suite = unchecked(entrySuites[i]);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
current_suite = suite;
|
|
380
|
-
depth = -1;
|
|
401
|
+
// @ts-ignore: current_suite is a @global; null between top-level suites
|
|
381
402
|
current_suite = null;
|
|
382
403
|
|
|
383
404
|
if (hasTopLevelOnly && suite.kind != "only") {
|
|
@@ -393,8 +414,7 @@ export function run(options: RunOptions = new RunOptions()): void {
|
|
|
393
414
|
fileVerdict = "skip";
|
|
394
415
|
}
|
|
395
416
|
|
|
396
|
-
|
|
397
|
-
depth = -1;
|
|
417
|
+
// @ts-ignore: current_suite is a @global
|
|
398
418
|
current_suite = null;
|
|
399
419
|
}
|
|
400
420
|
time.end = performance.now();
|
|
@@ -402,7 +422,7 @@ export function run(options: RunOptions = new RunOptions()): void {
|
|
|
402
422
|
const report = new FileReport();
|
|
403
423
|
report.suites = entrySuites;
|
|
404
424
|
report.coverage = collectCoverage();
|
|
405
|
-
sendReport(report.
|
|
425
|
+
sendReport(report.toJSON());
|
|
406
426
|
}
|
|
407
427
|
|
|
408
428
|
function containsOnlySuites(values: Suite[]): bool {
|
|
@@ -422,11 +442,11 @@ class FuzzConfig {
|
|
|
422
442
|
class FuzzReport {
|
|
423
443
|
fuzzers: FuzzerResult[] = [];
|
|
424
444
|
|
|
425
|
-
|
|
445
|
+
toJSON(): string {
|
|
426
446
|
let out = '{"fuzzers":[';
|
|
427
447
|
for (let i = 0; i < this.fuzzers.length; i++) {
|
|
428
448
|
if (i) out += ",";
|
|
429
|
-
out += unchecked(this.fuzzers[i]).
|
|
449
|
+
out += unchecked(this.fuzzers[i]).toJSON();
|
|
430
450
|
}
|
|
431
451
|
out += "]}";
|
|
432
452
|
return out;
|
|
@@ -443,7 +463,7 @@ function runFuzzers(): void {
|
|
|
443
463
|
const result = fuzzer.run(config.seed, resolveFuzzerRuns(fuzzer, config));
|
|
444
464
|
report.fuzzers.push(result);
|
|
445
465
|
}
|
|
446
|
-
sendReport(report.
|
|
466
|
+
sendReport(report.toJSON());
|
|
447
467
|
}
|
|
448
468
|
|
|
449
469
|
function requestFuzzConfig(): FuzzConfig {
|
|
@@ -482,20 +502,19 @@ function registerSuite(
|
|
|
482
502
|
kind: string,
|
|
483
503
|
): void {
|
|
484
504
|
const suite = new Suite(description, callback, kind);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
505
|
+
// Callbacks run lazily during the run phase, and `current_suite` is always
|
|
506
|
+
// the suite whose callback is currently executing (the same reference
|
|
507
|
+
// `expect()`/`log()` resolve against). So a describe/test/it registered from
|
|
508
|
+
// inside another block nests under it — including describe-in-describe.
|
|
509
|
+
// `current_suite` is null only at collection time, i.e. a top-level suite.
|
|
510
|
+
const parent = current_suite;
|
|
511
|
+
if (parent !== null) {
|
|
512
|
+
parent.addSuite(suite);
|
|
493
513
|
return;
|
|
494
514
|
}
|
|
495
515
|
|
|
496
516
|
suite.file = FILE;
|
|
497
517
|
entrySuites.push(suite);
|
|
498
|
-
suites.push(suite);
|
|
499
518
|
}
|
|
500
519
|
|
|
501
520
|
function resolveExpectationSuite(): Suite {
|
|
@@ -519,7 +538,7 @@ class CoverageReport {
|
|
|
519
538
|
percent: f64 = 100.0;
|
|
520
539
|
points: CoveragePointReport[] = [];
|
|
521
540
|
|
|
522
|
-
|
|
541
|
+
toJSON(): string {
|
|
523
542
|
return (
|
|
524
543
|
'{"total":' +
|
|
525
544
|
this.total.toString() +
|
|
@@ -548,26 +567,26 @@ class CoveragePointReport {
|
|
|
548
567
|
scopeName: string = "";
|
|
549
568
|
depth: i32 = 0;
|
|
550
569
|
|
|
551
|
-
|
|
570
|
+
toJSON(): string {
|
|
552
571
|
return (
|
|
553
572
|
'{"hash":' +
|
|
554
|
-
|
|
573
|
+
escape(this.hash) +
|
|
555
574
|
',"file":' +
|
|
556
|
-
|
|
575
|
+
escape(this.file) +
|
|
557
576
|
',"line":' +
|
|
558
577
|
this.line.toString() +
|
|
559
578
|
',"column":' +
|
|
560
579
|
this.column.toString() +
|
|
561
580
|
',"type":' +
|
|
562
|
-
|
|
581
|
+
escape(this.type) +
|
|
563
582
|
',"executed":' +
|
|
564
583
|
(this.executed ? "true" : "false") +
|
|
565
584
|
',"parentHash":' +
|
|
566
|
-
|
|
585
|
+
escape(this.parentHash) +
|
|
567
586
|
',"scopeKind":' +
|
|
568
|
-
|
|
587
|
+
escape(this.scopeKind) +
|
|
569
588
|
',"scopeName":' +
|
|
570
|
-
|
|
589
|
+
escape(this.scopeName) +
|
|
571
590
|
',"depth":' +
|
|
572
591
|
this.depth.toString() +
|
|
573
592
|
"}"
|
|
@@ -579,12 +598,12 @@ class FileReport {
|
|
|
579
598
|
suites: Suite[] = [];
|
|
580
599
|
coverage: CoverageReport = new CoverageReport();
|
|
581
600
|
|
|
582
|
-
|
|
601
|
+
toJSON(): string {
|
|
583
602
|
return (
|
|
584
603
|
'{"suites":' +
|
|
585
604
|
serializeSuites(this.suites) +
|
|
586
605
|
',"coverage":' +
|
|
587
|
-
this.coverage.
|
|
606
|
+
this.coverage.toJSON() +
|
|
588
607
|
"}"
|
|
589
608
|
);
|
|
590
609
|
}
|
|
@@ -595,7 +614,7 @@ function serializeSuites(values: Suite[]): string {
|
|
|
595
614
|
let out = "[";
|
|
596
615
|
for (let i = 0; i < values.length; i++) {
|
|
597
616
|
if (i) out += ",";
|
|
598
|
-
out += unchecked(values[i]).
|
|
617
|
+
out += unchecked(values[i]).toJSON();
|
|
599
618
|
}
|
|
600
619
|
out += "]";
|
|
601
620
|
return out;
|
|
@@ -606,7 +625,7 @@ function serializeCoveragePoints(values: CoveragePointReport[]): string {
|
|
|
606
625
|
let out = "[";
|
|
607
626
|
for (let i = 0; i < values.length; i++) {
|
|
608
627
|
if (i) out += ",";
|
|
609
|
-
out += unchecked(values[i]).
|
|
628
|
+
out += unchecked(values[i]).toJSON();
|
|
610
629
|
}
|
|
611
630
|
out += "]";
|
|
612
631
|
return out;
|
|
@@ -690,10 +709,10 @@ export class Result {
|
|
|
690
709
|
out += ` ${this.arg1 + this.arg2} total\n`;
|
|
691
710
|
return out;
|
|
692
711
|
}
|
|
693
|
-
|
|
712
|
+
toJSON(): string {
|
|
694
713
|
return (
|
|
695
714
|
'{"name":' +
|
|
696
|
-
|
|
715
|
+
escape(this.name) +
|
|
697
716
|
',"arg1":' +
|
|
698
717
|
this.arg1.toString() +
|
|
699
718
|
',"arg2":' +
|
|
@@ -710,7 +729,7 @@ export class Time {
|
|
|
710
729
|
return formatTime(this.end - this.start);
|
|
711
730
|
}
|
|
712
731
|
|
|
713
|
-
|
|
732
|
+
toJSON(): string {
|
|
714
733
|
return (
|
|
715
734
|
'{"start":' +
|
|
716
735
|
this.start.toString() +
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { visualize } from "../util/helpers";
|
|
2
2
|
import { Tests } from "./tests";
|
|
3
|
-
import { JSON } from "json-as/assembly";
|
|
4
3
|
import { namedSnapshotKey, nextUnnamedSnapshotKey } from "..";
|
|
5
4
|
import {
|
|
6
5
|
sendAssertionFailure,
|
|
7
6
|
sendWarning,
|
|
8
7
|
snapshotAssert,
|
|
9
8
|
} from "../util/wipc";
|
|
10
|
-
import {
|
|
9
|
+
import { reflectEquals } from "./reflect";
|
|
10
|
+
import { stringify, escape } from "./stringify";
|
|
11
11
|
|
|
12
12
|
let warnedToThrowDisabled = false;
|
|
13
|
+
let warnedSafeStringifyMissing = false;
|
|
14
|
+
|
|
15
|
+
function safeStringify<T>(value: T): string {
|
|
16
|
+
if (
|
|
17
|
+
isManaged<T>() &&
|
|
18
|
+
!warnedSafeStringifyMissing &&
|
|
19
|
+
changetype<usize>(value) != 0 &&
|
|
20
|
+
!isString<T>() &&
|
|
21
|
+
!isArray<T>()
|
|
22
|
+
) {
|
|
23
|
+
// @ts-expect-error: optional user-supplied serializer
|
|
24
|
+
if (!isDefined(value.toJSON)) {
|
|
25
|
+
sendWarning(
|
|
26
|
+
"Class " +
|
|
27
|
+
nameof<T>() +
|
|
28
|
+
" has no toJSON(): string method. Report values render as a `<" +
|
|
29
|
+
nameof<T>() +
|
|
30
|
+
">` placeholder. Add toJSON() returning a JSON string to serialize.",
|
|
31
|
+
);
|
|
32
|
+
warnedSafeStringifyMissing = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return stringify<T>(value);
|
|
36
|
+
}
|
|
13
37
|
|
|
14
38
|
export class Expectation<T> extends Tests {
|
|
15
39
|
public verdict: string = "none";
|
|
@@ -18,13 +42,11 @@ export class Expectation<T> extends Tests {
|
|
|
18
42
|
|
|
19
43
|
private _left: T;
|
|
20
44
|
|
|
21
|
-
// @ts-
|
|
45
|
+
// @ts-expect-error: used internally
|
|
22
46
|
private _right: u64 = 0;
|
|
23
47
|
|
|
24
|
-
// @ts-ignore
|
|
25
48
|
private _not: boolean = false;
|
|
26
49
|
|
|
27
|
-
// @ts-ignore
|
|
28
50
|
private _skip: boolean = false;
|
|
29
51
|
|
|
30
52
|
private _message: string = "";
|
|
@@ -492,8 +514,8 @@ export class Expectation<T> extends Tests {
|
|
|
492
514
|
this._resolve(
|
|
493
515
|
passed,
|
|
494
516
|
"toContain",
|
|
495
|
-
|
|
496
|
-
|
|
517
|
+
safeStringify<T>(this._left),
|
|
518
|
+
safeStringify<valueof<T>>(value),
|
|
497
519
|
message,
|
|
498
520
|
);
|
|
499
521
|
return this;
|
|
@@ -519,7 +541,7 @@ export class Expectation<T> extends Tests {
|
|
|
519
541
|
? namedSnapshotKey(this._snapshotKey, name)
|
|
520
542
|
: nextUnnamedSnapshotKey(this._snapshotKey);
|
|
521
543
|
|
|
522
|
-
const actual =
|
|
544
|
+
const actual = safeStringify<T>(this._left);
|
|
523
545
|
const res = snapshotAssert(key, actual);
|
|
524
546
|
this._resolve(res.ok, "toMatchSnapshot", actual, res.expected, message);
|
|
525
547
|
return this;
|
|
@@ -591,108 +613,44 @@ export class Expectation<T> extends Tests {
|
|
|
591
613
|
* Tests for equality
|
|
592
614
|
*/
|
|
593
615
|
toBe(equals: T, message: string = ""): Expectation<T> {
|
|
594
|
-
|
|
595
|
-
|
|
616
|
+
// Deep structural equality for managed values; `===` semantics for
|
|
617
|
+
// primitives and strings (reflectEquals does the dispatch).
|
|
618
|
+
// `toEqual` is kept below as a Jest-familiarity alias.
|
|
619
|
+
const passed = reflectEquals<T>(this._left, equals, [], false);
|
|
596
620
|
this._resolve(
|
|
597
621
|
passed,
|
|
598
622
|
"toBe",
|
|
599
|
-
|
|
600
|
-
|
|
623
|
+
safeStringify<T>(this._left),
|
|
624
|
+
safeStringify<T>(equals),
|
|
601
625
|
message,
|
|
602
626
|
);
|
|
603
627
|
return this;
|
|
604
628
|
}
|
|
605
629
|
|
|
606
630
|
/**
|
|
607
|
-
*
|
|
631
|
+
* Alias of `toBe` retained for Jest familiarity.
|
|
608
632
|
*/
|
|
609
633
|
toEqual(equals: T, message: string = ""): Expectation<T> {
|
|
610
|
-
|
|
611
|
-
this._resolve(
|
|
612
|
-
passed,
|
|
613
|
-
"toEqual",
|
|
614
|
-
JSON.stringify<T>(this._left),
|
|
615
|
-
JSON.stringify<T>(equals),
|
|
616
|
-
message,
|
|
617
|
-
);
|
|
618
|
-
return this;
|
|
634
|
+
return this.toBe(equals, message);
|
|
619
635
|
}
|
|
620
636
|
|
|
621
637
|
/**
|
|
622
|
-
*
|
|
638
|
+
* Like `toBe` but also requires the runtime type (rtId) of the
|
|
639
|
+
* operands to match for managed values.
|
|
623
640
|
*/
|
|
624
641
|
toStrictEqual(equals: T, message: string = ""): Expectation<T> {
|
|
625
|
-
const passed =
|
|
642
|
+
const passed = reflectEquals<T>(this._left, equals, [], true);
|
|
626
643
|
this._resolve(
|
|
627
644
|
passed,
|
|
628
645
|
"toStrictEqual",
|
|
629
|
-
|
|
630
|
-
|
|
646
|
+
safeStringify<T>(this._left),
|
|
647
|
+
safeStringify<T>(equals),
|
|
631
648
|
message,
|
|
632
649
|
);
|
|
633
650
|
return this;
|
|
634
651
|
}
|
|
635
652
|
}
|
|
636
653
|
|
|
637
|
-
function arrayEquals<T>(a: T[], b: T[], strict: bool): boolean {
|
|
638
|
-
if (a.length != b.length) return false;
|
|
639
|
-
for (let i = 0; i < a.length; i++) {
|
|
640
|
-
if (!valueEquals<T>(unchecked(a[i]), unchecked(b[i]), strict)) {
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
return true;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function valueEquals<T>(left: T, right: T, strict: bool): bool {
|
|
648
|
-
if (isBoolean<T>() || isString<T>() || isInteger<T>() || isFloat<T>()) {
|
|
649
|
-
return left === right;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (isNullable<T>()) {
|
|
653
|
-
const leftPtr = changetype<usize>(left);
|
|
654
|
-
const rightPtr = changetype<usize>(right);
|
|
655
|
-
if (leftPtr == 0 || rightPtr == 0) return leftPtr == rightPtr;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
if (isArray<T>()) {
|
|
659
|
-
return arrayEquals<valueof<T>>(
|
|
660
|
-
changetype<valueof<T>[]>(left),
|
|
661
|
-
changetype<valueof<T>[]>(right),
|
|
662
|
-
strict,
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (isManaged<T>()) {
|
|
667
|
-
return managedEquals<T>(left, right, strict);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
abort(
|
|
671
|
-
`Unsupported equality matcher for ${nameof<T>()}. Use toBe() for identity or compare fields explicitly.`,
|
|
672
|
-
);
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
export function __as_test_deep_equal<T>(left: T, right: T, strict: bool): bool {
|
|
677
|
-
return valueEquals<T>(left, right, strict);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function managedEquals<T>(left: T, right: T, strict: bool): bool {
|
|
681
|
-
const leftPtr = changetype<usize>(left);
|
|
682
|
-
const rightPtr = changetype<usize>(right);
|
|
683
|
-
if (leftPtr == rightPtr) return true;
|
|
684
|
-
if (leftPtr == 0 || rightPtr == 0) return false;
|
|
685
|
-
|
|
686
|
-
if (strict) {
|
|
687
|
-
const leftObject = changetype<OBJECT>(leftPtr - TOTAL_OVERHEAD);
|
|
688
|
-
const rightObject = changetype<OBJECT>(rightPtr - TOTAL_OVERHEAD);
|
|
689
|
-
if (leftObject.rtId != rightObject.rtId) return false;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// @ts-ignore
|
|
693
|
-
return left.__as_test_equals(right, strict);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
654
|
function isTruthy<T>(value: T): bool {
|
|
697
655
|
if (isBoolean<T>()) {
|
|
698
656
|
return value as bool;
|
|
@@ -714,5 +672,5 @@ function isTruthy<T>(value: T): bool {
|
|
|
714
672
|
}
|
|
715
673
|
|
|
716
674
|
function q(value: string): string {
|
|
717
|
-
return
|
|
675
|
+
return escape(value);
|
|
718
676
|
}
|
package/assembly/src/fuzz.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escape, stringify } from "./stringify";
|
|
2
2
|
|
|
3
3
|
function quote(s: string): string {
|
|
4
|
-
return
|
|
4
|
+
return escape(s);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export class StringOptions {
|
|
@@ -615,7 +615,7 @@ export class FuzzerResult {
|
|
|
615
615
|
public failureMessage: string = "";
|
|
616
616
|
public failures: FuzzFailure[] = [];
|
|
617
617
|
|
|
618
|
-
|
|
618
|
+
toJSON(): string {
|
|
619
619
|
return (
|
|
620
620
|
'{"name":"' +
|
|
621
621
|
this.name +
|
|
@@ -653,7 +653,7 @@ export class FuzzFailure {
|
|
|
653
653
|
public seed: u64 = 0;
|
|
654
654
|
public input: string = "";
|
|
655
655
|
|
|
656
|
-
|
|
656
|
+
toJSON(): string {
|
|
657
657
|
return (
|
|
658
658
|
'{"run":' +
|
|
659
659
|
this.run.toString() +
|
|
@@ -881,7 +881,7 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
881
881
|
);
|
|
882
882
|
} else {
|
|
883
883
|
this.generator(seed, (a: A): R => {
|
|
884
|
-
__as_test_fuzz_input = "[" +
|
|
884
|
+
__as_test_fuzz_input = "[" + stringify<A>(a) + "]";
|
|
885
885
|
return changetype<(a: A) => R>(__fuzz_run1)(a);
|
|
886
886
|
});
|
|
887
887
|
}
|
|
@@ -952,7 +952,7 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
952
952
|
} else {
|
|
953
953
|
this.generator(seed, (a: A, b: B): R => {
|
|
954
954
|
__as_test_fuzz_input =
|
|
955
|
-
"[" +
|
|
955
|
+
"[" + stringify<A>(a) + "," + stringify<B>(b) + "]";
|
|
956
956
|
return changetype<(a: A, b: B) => R>(__fuzz_run2)(a, b);
|
|
957
957
|
});
|
|
958
958
|
}
|
|
@@ -1026,11 +1026,11 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
1026
1026
|
)(seed, (a: A, b: B, c: C): R => {
|
|
1027
1027
|
__as_test_fuzz_input =
|
|
1028
1028
|
"[" +
|
|
1029
|
-
|
|
1029
|
+
stringify<A>(a) +
|
|
1030
1030
|
"," +
|
|
1031
|
-
|
|
1031
|
+
stringify<B>(b) +
|
|
1032
1032
|
"," +
|
|
1033
|
-
|
|
1033
|
+
stringify<C>(c) +
|
|
1034
1034
|
"]";
|
|
1035
1035
|
return changetype<(a: A, b: B, c: C) => R>(__fuzz_run3)(a, b, c);
|
|
1036
1036
|
});
|
|
@@ -1208,7 +1208,7 @@ function serializeFuzzFailures(values: FuzzFailure[]): string {
|
|
|
1208
1208
|
let out = "[";
|
|
1209
1209
|
for (let i = 0; i < values.length; i++) {
|
|
1210
1210
|
if (i) out += ",";
|
|
1211
|
-
out += unchecked(values[i]).
|
|
1211
|
+
out += unchecked(values[i]).toJSON();
|
|
1212
1212
|
}
|
|
1213
1213
|
out += "]";
|
|
1214
1214
|
return out;
|
package/assembly/src/log.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { escape, stringify } from "./stringify";
|
|
2
2
|
|
|
3
3
|
import { sendLog } from "../util/wipc";
|
|
4
4
|
|
|
@@ -14,14 +14,14 @@ export class Log {
|
|
|
14
14
|
sendLog(this.file, this.depth, this.text);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
toJSON(): string {
|
|
18
18
|
return (
|
|
19
19
|
'{"order":' +
|
|
20
20
|
this.order.toString() +
|
|
21
21
|
',"depth":' +
|
|
22
22
|
this.depth.toString() +
|
|
23
23
|
',"text":' +
|
|
24
|
-
|
|
24
|
+
escape(this.text) +
|
|
25
25
|
"}"
|
|
26
26
|
);
|
|
27
27
|
}
|