as-test 1.0.4 → 1.0.6
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 +18 -0
- package/README.md +19 -0
- package/assembly/src/fuzz.ts +68 -10
- package/bin/commands/build-core.js +50 -0
- package/bin/commands/build.js +1 -1
- package/bin/commands/fuzz-core.js +44 -1
- package/bin/commands/run-core.js +130 -27
- package/bin/commands/run.js +1 -0
- package/bin/commands/test.js +1 -0
- package/bin/crash-store.js +12 -1
- package/bin/index.js +150 -42
- package/bin/reporters/default.js +41 -4
- package/bin/reporters/tap.js +5 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 2026-03-31 - v1.0.6
|
|
4
|
+
|
|
5
|
+
### Fuzzing
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
### Reporting
|
|
10
|
+
|
|
11
|
+
- fix: correct reporter override output behavior.
|
|
12
|
+
|
|
13
|
+
## 2026-03-30 - v1.0.5
|
|
14
|
+
|
|
15
|
+
### CLI
|
|
16
|
+
|
|
17
|
+
- fix: preserve selectors passed after `--parallel` so commands like `ast test --parallel math` still target the requested suite.
|
|
18
|
+
- fix: treat uncaught runtime stderr / missing report payloads as normal failed test results instead of transport-level crashes, with cleaner default reporter output.
|
|
19
|
+
- perf: reuse build artifacts across modes when the resolved non-custom build invocation and build environment are identical, copying the first artifact instead of recompiling.
|
|
20
|
+
|
|
3
21
|
## 2026-03-27 - v1.0.4
|
|
4
22
|
|
|
5
23
|
### Build Command
|
package/README.md
CHANGED
|
@@ -66,6 +66,23 @@ By default, `as-test` looks for:
|
|
|
66
66
|
|
|
67
67
|
Generated files go into `.as-test/`.
|
|
68
68
|
|
|
69
|
+
Minimal `as-test.config.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"input": ["assembly/__tests__/*.spec.ts"],
|
|
74
|
+
"output": ".as-test/",
|
|
75
|
+
"buildOptions": {
|
|
76
|
+
"target": "wasi"
|
|
77
|
+
},
|
|
78
|
+
"runOptions": {
|
|
79
|
+
"runtime": {
|
|
80
|
+
"cmd": "node .as-test/runners/default.wasi.js <file>"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
69
86
|
## Writing Tests
|
|
70
87
|
|
|
71
88
|
Tests usually live in `assembly/__tests__/*.spec.ts`.
|
|
@@ -214,6 +231,8 @@ If you used `npx ast init` with a fuzzer example, the config is already there. O
|
|
|
214
231
|
|
|
215
232
|
`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`.
|
|
216
233
|
|
|
234
|
+
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.
|
|
235
|
+
|
|
217
236
|
Run only fuzzers:
|
|
218
237
|
|
|
219
238
|
```bash
|
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";
|
|
@@ -211,6 +211,7 @@ export class FuzzerResult {
|
|
|
211
211
|
public failureLeft: string = "";
|
|
212
212
|
public failureRight: string = "";
|
|
213
213
|
public failureMessage: string = "";
|
|
214
|
+
public failures: FuzzFailure[] = [];
|
|
214
215
|
|
|
215
216
|
serialize(): string {
|
|
216
217
|
return (
|
|
@@ -238,7 +239,27 @@ export class FuzzerResult {
|
|
|
238
239
|
quote(this.failureRight) +
|
|
239
240
|
',"message":' +
|
|
240
241
|
quote(this.failureMessage) +
|
|
241
|
-
|
|
242
|
+
'},"failures":' +
|
|
243
|
+
serializeFuzzFailures(this.failures) +
|
|
244
|
+
"}"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export class FuzzFailure {
|
|
250
|
+
public run: i32 = 0;
|
|
251
|
+
public seed: u64 = 0;
|
|
252
|
+
public input: string = "";
|
|
253
|
+
|
|
254
|
+
serialize(): string {
|
|
255
|
+
return (
|
|
256
|
+
'{"run":' +
|
|
257
|
+
this.run.toString() +
|
|
258
|
+
',"seed":' +
|
|
259
|
+
this.seed.toString() +
|
|
260
|
+
',"input":' +
|
|
261
|
+
rawOrNull(this.input) +
|
|
262
|
+
"}"
|
|
242
263
|
);
|
|
243
264
|
}
|
|
244
265
|
}
|
|
@@ -329,7 +350,7 @@ function createSkippedResult(name: string): FuzzerResult {
|
|
|
329
350
|
return result;
|
|
330
351
|
}
|
|
331
352
|
|
|
332
|
-
function recordResult(result: FuzzerResult): void {
|
|
353
|
+
function recordResult(result: FuzzerResult, run: i32, seed: u64): void {
|
|
333
354
|
if (__as_test_fuzz_failed) {
|
|
334
355
|
result.failed++;
|
|
335
356
|
if (!result.failureInstr.length && __as_test_fuzz_failure_instr.length) {
|
|
@@ -338,6 +359,11 @@ function recordResult(result: FuzzerResult): void {
|
|
|
338
359
|
result.failureRight = __as_test_fuzz_failure_right;
|
|
339
360
|
result.failureMessage = __as_test_fuzz_failure_message;
|
|
340
361
|
}
|
|
362
|
+
const failure = new FuzzFailure();
|
|
363
|
+
failure.run = run;
|
|
364
|
+
failure.seed = seed;
|
|
365
|
+
failure.input = __as_test_fuzz_input;
|
|
366
|
+
result.failures.push(failure);
|
|
341
367
|
} else {
|
|
342
368
|
result.passed++;
|
|
343
369
|
}
|
|
@@ -389,7 +415,7 @@ export class Fuzzer0<R> extends FuzzerBase {
|
|
|
389
415
|
"fuzz generator must call run() exactly once",
|
|
390
416
|
);
|
|
391
417
|
}
|
|
392
|
-
recordResult(result);
|
|
418
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
393
419
|
}
|
|
394
420
|
__fuzz_callback0 = null;
|
|
395
421
|
result.timeEnd = performance.now();
|
|
@@ -438,7 +464,10 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
438
464
|
"fuzzers with arguments must call .generate(...)",
|
|
439
465
|
);
|
|
440
466
|
} else {
|
|
441
|
-
this.generator(seed,
|
|
467
|
+
this.generator(seed, (a: A): R => {
|
|
468
|
+
__as_test_fuzz_input = "[" + stringifyValue<A>(a) + "]";
|
|
469
|
+
return changetype<(a: A) => R>(__fuzz_run1)(a);
|
|
470
|
+
});
|
|
442
471
|
}
|
|
443
472
|
if (__fuzz_calls != 1) {
|
|
444
473
|
failFuzzIteration(
|
|
@@ -448,7 +477,7 @@ export class Fuzzer1<A, R> extends FuzzerBase {
|
|
|
448
477
|
"fuzz generator must call run() exactly once",
|
|
449
478
|
);
|
|
450
479
|
}
|
|
451
|
-
recordResult(result);
|
|
480
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
452
481
|
}
|
|
453
482
|
__fuzz_callback1 = null;
|
|
454
483
|
result.timeEnd = performance.now();
|
|
@@ -500,7 +529,11 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
500
529
|
"fuzzers with arguments must call .generate(...)",
|
|
501
530
|
);
|
|
502
531
|
} else {
|
|
503
|
-
this.generator(seed,
|
|
532
|
+
this.generator(seed, (a: A, b: B): R => {
|
|
533
|
+
__as_test_fuzz_input =
|
|
534
|
+
"[" + stringifyValue<A>(a) + "," + stringifyValue<B>(b) + "]";
|
|
535
|
+
return changetype<(a: A, b: B) => R>(__fuzz_run2)(a, b);
|
|
536
|
+
});
|
|
504
537
|
}
|
|
505
538
|
if (__fuzz_calls != 1) {
|
|
506
539
|
failFuzzIteration(
|
|
@@ -510,7 +543,7 @@ export class Fuzzer2<A, B, R> extends FuzzerBase {
|
|
|
510
543
|
"fuzz generator must call run() exactly once",
|
|
511
544
|
);
|
|
512
545
|
}
|
|
513
|
-
recordResult(result);
|
|
546
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
514
547
|
}
|
|
515
548
|
__fuzz_callback2 = null;
|
|
516
549
|
result.timeEnd = performance.now();
|
|
@@ -564,7 +597,17 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
564
597
|
} else {
|
|
565
598
|
changetype<(seed: FuzzSeed, run: (a: A, b: B, c: C) => R) => void>(
|
|
566
599
|
this.generator,
|
|
567
|
-
)(seed,
|
|
600
|
+
)(seed, (a: A, b: B, c: C): R => {
|
|
601
|
+
__as_test_fuzz_input =
|
|
602
|
+
"[" +
|
|
603
|
+
stringifyValue<A>(a) +
|
|
604
|
+
"," +
|
|
605
|
+
stringifyValue<B>(b) +
|
|
606
|
+
"," +
|
|
607
|
+
stringifyValue<C>(c) +
|
|
608
|
+
"]";
|
|
609
|
+
return changetype<(a: A, b: B, c: C) => R>(__fuzz_run3)(a, b, c);
|
|
610
|
+
});
|
|
568
611
|
}
|
|
569
612
|
if (__fuzz_calls != 1) {
|
|
570
613
|
failFuzzIteration(
|
|
@@ -574,7 +617,7 @@ export class Fuzzer3<A, B, C, R> extends FuzzerBase {
|
|
|
574
617
|
"fuzz generator must call run() exactly once",
|
|
575
618
|
);
|
|
576
619
|
}
|
|
577
|
-
recordResult(result);
|
|
620
|
+
recordResult(result, i, seedBase + <u64>i);
|
|
578
621
|
}
|
|
579
622
|
__fuzz_callback3 = null;
|
|
580
623
|
result.timeEnd = performance.now();
|
|
@@ -694,6 +737,8 @@ function containsFloatValue<T>(values: T[], needle: T): bool {
|
|
|
694
737
|
@global export let __as_test_fuzz_failure_right: string = "";
|
|
695
738
|
// @ts-ignore
|
|
696
739
|
@global export let __as_test_fuzz_failure_message: string = "";
|
|
740
|
+
// @ts-ignore
|
|
741
|
+
@global export let __as_test_fuzz_input: string = "";
|
|
697
742
|
|
|
698
743
|
export function prepareFuzzIteration(): void {
|
|
699
744
|
__as_test_fuzz_failed = false;
|
|
@@ -701,6 +746,7 @@ export function prepareFuzzIteration(): void {
|
|
|
701
746
|
__as_test_fuzz_failure_left = "";
|
|
702
747
|
__as_test_fuzz_failure_right = "";
|
|
703
748
|
__as_test_fuzz_failure_message = "";
|
|
749
|
+
__as_test_fuzz_input = "[]";
|
|
704
750
|
}
|
|
705
751
|
|
|
706
752
|
export function failFuzzIteration(
|
|
@@ -721,3 +767,15 @@ export function failFuzzIteration(
|
|
|
721
767
|
function panic(): void {
|
|
722
768
|
unreachable();
|
|
723
769
|
}
|
|
770
|
+
|
|
771
|
+
function serializeFuzzFailures(values: FuzzFailure[]): string {
|
|
772
|
+
if (!values.length) return "[]";
|
|
773
|
+
|
|
774
|
+
let out = "[";
|
|
775
|
+
for (let i = 0; i < values.length; i++) {
|
|
776
|
+
if (i) out += ",";
|
|
777
|
+
out += unchecked(values[i]).serialize();
|
|
778
|
+
}
|
|
779
|
+
out += "]";
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
@@ -124,6 +124,39 @@ export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH
|
|
|
124
124
|
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
|
|
125
125
|
return getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
|
|
126
126
|
}
|
|
127
|
+
export async function getBuildReuseInfo(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
|
|
128
|
+
const loadedConfig = loadConfig(configPath, false);
|
|
129
|
+
const mode = applyMode(loadedConfig, modeName);
|
|
130
|
+
const config = Object.assign(Object.create(Object.getPrototypeOf(mode.config)), mode.config);
|
|
131
|
+
config.buildOptions = Object.assign(Object.create(Object.getPrototypeOf(mode.config.buildOptions)), mode.config.buildOptions);
|
|
132
|
+
if (overrides.target) {
|
|
133
|
+
config.buildOptions.target = overrides.target;
|
|
134
|
+
}
|
|
135
|
+
if (overrides.args?.length) {
|
|
136
|
+
config.buildOptions.args = [...config.buildOptions.args, ...overrides.args];
|
|
137
|
+
}
|
|
138
|
+
if (hasCustomBuildCommand(config)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const duplicateSpecBasenames = resolveDuplicateBasenames([file]);
|
|
142
|
+
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
|
|
143
|
+
const invocation = getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
|
|
144
|
+
const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
|
|
145
|
+
const buildEnv = {
|
|
146
|
+
...mode.env,
|
|
147
|
+
...config.buildOptions.env,
|
|
148
|
+
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
signature: JSON.stringify({
|
|
152
|
+
command: invocation.command,
|
|
153
|
+
args: stripOutputArgs(invocation.args),
|
|
154
|
+
apiArgs: invocation.apiArgs ? stripOutputArgs(invocation.apiArgs) : [],
|
|
155
|
+
env: sortRecord(buildEnv),
|
|
156
|
+
}),
|
|
157
|
+
outFile,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
127
160
|
function hasCustomBuildCommand(config) {
|
|
128
161
|
return !!config.buildOptions.cmd.trim().length;
|
|
129
162
|
}
|
|
@@ -176,6 +209,23 @@ function getUserBuildArgs(config) {
|
|
|
176
209
|
}
|
|
177
210
|
return args;
|
|
178
211
|
}
|
|
212
|
+
function stripOutputArgs(args) {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (let i = 0; i < args.length; i++) {
|
|
215
|
+
const arg = args[i];
|
|
216
|
+
if (arg == "-o" || arg == "--outFile") {
|
|
217
|
+
i++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
out.push(arg);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function sortRecord(record) {
|
|
225
|
+
return Object.fromEntries(Object.entries(record)
|
|
226
|
+
.filter((entry) => typeof entry[1] == "string")
|
|
227
|
+
.sort((a, b) => a[0].localeCompare(b[0])));
|
|
228
|
+
}
|
|
179
229
|
function expandBuildCommand(template, file, outFile, target, modeName) {
|
|
180
230
|
const name = path
|
|
181
231
|
.basename(file)
|
package/bin/commands/build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { closeSerialBuildWorkerPool, } from "./build-core.js";
|
|
2
2
|
export { build } from "./build-core.js";
|
|
3
|
-
export { formatInvocation, getBuildInvocationPreview } from "./build-core.js";
|
|
3
|
+
export { formatInvocation, getBuildInvocationPreview, getBuildReuseInfo, } from "./build-core.js";
|
|
4
4
|
export async function executeBuildCommand(rawArgs, configPath, selectedModes, deps) {
|
|
5
5
|
const commandArgs = deps.resolveCommandArgs(rawArgs, "build");
|
|
6
6
|
const listFlags = deps.resolveListFlags(rawArgs, "build");
|
|
@@ -68,6 +68,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
68
68
|
const crash = persistCrashRecord(config.crashDir, {
|
|
69
69
|
kind: "fuzz",
|
|
70
70
|
file,
|
|
71
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
71
72
|
mode: modeName ?? "default",
|
|
72
73
|
seed: config.seed,
|
|
73
74
|
error: crashMessage,
|
|
@@ -94,6 +95,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
94
95
|
const crash = persistCrashRecord(config.crashDir, {
|
|
95
96
|
kind: "fuzz",
|
|
96
97
|
file,
|
|
98
|
+
entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
|
|
97
99
|
mode: modeName ?? "default",
|
|
98
100
|
seed: config.seed,
|
|
99
101
|
error: `missing fuzz report payload from ${path.basename(file)}`,
|
|
@@ -115,13 +117,37 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
115
117
|
fuzzers: [],
|
|
116
118
|
};
|
|
117
119
|
}
|
|
120
|
+
const crashFiles = [];
|
|
121
|
+
for (const fuzzer of report.fuzzers) {
|
|
122
|
+
if (fuzzer.failed <= 0 && fuzzer.crashed <= 0)
|
|
123
|
+
continue;
|
|
124
|
+
const firstFailureSeed = typeof fuzzer.failures?.[0]?.seed == "number"
|
|
125
|
+
? fuzzer.failures[0].seed
|
|
126
|
+
: config.seed;
|
|
127
|
+
const crash = persistCrashRecord(config.crashDir, {
|
|
128
|
+
kind: "fuzz",
|
|
129
|
+
file,
|
|
130
|
+
entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
|
|
131
|
+
mode: modeName ?? "default",
|
|
132
|
+
seed: firstFailureSeed,
|
|
133
|
+
reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", 1),
|
|
134
|
+
error: fuzzer.failure?.message ||
|
|
135
|
+
`fuzz failure in ${fuzzer.name} after ${fuzzer.runs} runs`,
|
|
136
|
+
stdout: passthrough.stdout,
|
|
137
|
+
stderr: "",
|
|
138
|
+
failure: fuzzer.failure,
|
|
139
|
+
failures: fuzzer.failures,
|
|
140
|
+
});
|
|
141
|
+
crashFiles.push(crash.jsonPath);
|
|
142
|
+
fuzzer.crashFile = crash.jsonPath;
|
|
143
|
+
}
|
|
118
144
|
return {
|
|
119
145
|
file,
|
|
120
146
|
target: path.basename(file),
|
|
121
147
|
modeName: modeName ?? "default",
|
|
122
148
|
runs: report.fuzzers.reduce((sum, item) => sum + item.runs, 0),
|
|
123
149
|
crashes: report.fuzzers.reduce((sum, item) => sum + item.crashed, 0),
|
|
124
|
-
crashFiles
|
|
150
|
+
crashFiles,
|
|
125
151
|
seed: config.seed,
|
|
126
152
|
time: Date.now() - startedAt,
|
|
127
153
|
buildTime,
|
|
@@ -130,6 +156,23 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, buildStar
|
|
|
130
156
|
fuzzers: report.fuzzers,
|
|
131
157
|
};
|
|
132
158
|
}
|
|
159
|
+
function buildFuzzReproCommand(file, seed, modeName, runs) {
|
|
160
|
+
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
161
|
+
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
162
|
+
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
163
|
+
}
|
|
164
|
+
function buildFuzzFailureEntryKey(file, name, modeName) {
|
|
165
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
|
|
166
|
+
}
|
|
167
|
+
function buildFuzzCrashEntryKey(file, modeName) {
|
|
168
|
+
return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}`;
|
|
169
|
+
}
|
|
170
|
+
function sanitizeEntryName(name) {
|
|
171
|
+
return name
|
|
172
|
+
.toLowerCase()
|
|
173
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
174
|
+
.replace(/^-+|-+$/g, "") || "fuzzer";
|
|
175
|
+
}
|
|
133
176
|
function captureFrames(onFrame) {
|
|
134
177
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
135
178
|
const originalRead = typeof process.stdin.read == "function"
|
package/bin/commands/run-core.js
CHANGED
|
@@ -1286,26 +1286,22 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1286
1286
|
let report = null;
|
|
1287
1287
|
let parseError = null;
|
|
1288
1288
|
let stderrBuffer = "";
|
|
1289
|
+
let stderrPendingLine = "";
|
|
1289
1290
|
let stdoutBuffer = "";
|
|
1290
|
-
let suppressTraceWarningLine = false;
|
|
1291
1291
|
let spawnError = null;
|
|
1292
1292
|
child.on("error", (error) => {
|
|
1293
1293
|
spawnError = error;
|
|
1294
1294
|
});
|
|
1295
1295
|
child.stderr.on("data", (chunk) => {
|
|
1296
|
-
|
|
1297
|
-
let newline =
|
|
1296
|
+
stderrPendingLine += chunk.toString("utf8");
|
|
1297
|
+
let newline = stderrPendingLine.indexOf("\n");
|
|
1298
1298
|
while (newline >= 0) {
|
|
1299
|
-
const line =
|
|
1300
|
-
|
|
1301
|
-
if (shouldSuppressWasiWarningLine(line
|
|
1302
|
-
|
|
1299
|
+
const line = stderrPendingLine.slice(0, newline + 1);
|
|
1300
|
+
stderrPendingLine = stderrPendingLine.slice(newline + 1);
|
|
1301
|
+
if (!shouldSuppressWasiWarningLine(line)) {
|
|
1302
|
+
stderrBuffer += line;
|
|
1303
1303
|
}
|
|
1304
|
-
|
|
1305
|
-
suppressTraceWarningLine = false;
|
|
1306
|
-
process.stderr.write(line);
|
|
1307
|
-
}
|
|
1308
|
-
newline = stderrBuffer.indexOf("\n");
|
|
1304
|
+
newline = stderrPendingLine.indexOf("\n");
|
|
1309
1305
|
}
|
|
1310
1306
|
});
|
|
1311
1307
|
class TestChannel extends Channel {
|
|
@@ -1409,55 +1405,138 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1409
1405
|
const code = await new Promise((resolve) => {
|
|
1410
1406
|
child.on("close", (exitCode) => resolve(exitCode ?? 1));
|
|
1411
1407
|
});
|
|
1412
|
-
if (
|
|
1413
|
-
|
|
1414
|
-
process.stderr.write(stderrBuffer);
|
|
1415
|
-
}
|
|
1408
|
+
if (stderrPendingLine.length && !shouldSuppressWasiWarningLine(stderrPendingLine)) {
|
|
1409
|
+
stderrBuffer += stderrPendingLine;
|
|
1416
1410
|
}
|
|
1417
1411
|
if (spawnError) {
|
|
1412
|
+
const errorText = spawnError.stack ?? spawnError.message;
|
|
1418
1413
|
persistCrashRecord(crashDir, {
|
|
1419
1414
|
kind: "test",
|
|
1420
1415
|
file: specFile,
|
|
1421
1416
|
mode: modeName ?? "default",
|
|
1422
|
-
error:
|
|
1417
|
+
error: errorText,
|
|
1423
1418
|
stdout: stdoutBuffer,
|
|
1424
1419
|
stderr: stderrBuffer,
|
|
1425
1420
|
});
|
|
1426
|
-
|
|
1421
|
+
return createRuntimeFailureReport(specFile, modeName, "failed to start test runtime", errorText, stdoutBuffer, stderrBuffer);
|
|
1427
1422
|
}
|
|
1428
1423
|
if (parseError) {
|
|
1424
|
+
const errorText = `could not parse report payload: ${parseError}`;
|
|
1429
1425
|
persistCrashRecord(crashDir, {
|
|
1430
1426
|
kind: "test",
|
|
1431
1427
|
file: specFile,
|
|
1432
1428
|
mode: modeName ?? "default",
|
|
1433
|
-
error:
|
|
1429
|
+
error: errorText,
|
|
1434
1430
|
stdout: stdoutBuffer,
|
|
1435
1431
|
stderr: stderrBuffer,
|
|
1436
1432
|
});
|
|
1437
|
-
|
|
1433
|
+
return createRuntimeFailureReport(specFile, modeName, "runtime returned an invalid report payload", errorText, stdoutBuffer, stderrBuffer);
|
|
1438
1434
|
}
|
|
1439
1435
|
if (!report) {
|
|
1436
|
+
const errorText = "missing report payload from test runtime";
|
|
1440
1437
|
persistCrashRecord(crashDir, {
|
|
1441
1438
|
kind: "test",
|
|
1442
1439
|
file: specFile,
|
|
1443
1440
|
mode: modeName ?? "default",
|
|
1444
|
-
error:
|
|
1441
|
+
error: errorText,
|
|
1445
1442
|
stdout: stdoutBuffer,
|
|
1446
1443
|
stderr: stderrBuffer,
|
|
1447
1444
|
});
|
|
1448
|
-
|
|
1445
|
+
return createRuntimeFailureReport(specFile, modeName, "test runtime exited without sending a report", errorText, stdoutBuffer, stderrBuffer);
|
|
1449
1446
|
}
|
|
1450
|
-
if (code !== 0) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1447
|
+
if (code !== 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
|
|
1448
|
+
const errorParts = [];
|
|
1449
|
+
if (code !== 0) {
|
|
1450
|
+
errorParts.push(`child process exited with code ${code}`);
|
|
1451
|
+
}
|
|
1452
|
+
const stderrText = normalizeRuntimeOutput(stderrBuffer);
|
|
1453
|
+
if (stderrText.length) {
|
|
1454
|
+
errorParts.push(stderrText);
|
|
1455
|
+
}
|
|
1456
|
+
const errorText = errorParts.join("\n\n");
|
|
1457
|
+
persistCrashRecord(crashDir, {
|
|
1458
|
+
kind: "test",
|
|
1459
|
+
file: specFile,
|
|
1460
|
+
mode: modeName ?? "default",
|
|
1461
|
+
error: errorText || "runtime reported an unknown error",
|
|
1462
|
+
stdout: stdoutBuffer,
|
|
1463
|
+
stderr: stderrBuffer,
|
|
1464
|
+
});
|
|
1465
|
+
return appendRuntimeFailureReport(report, specFile, modeName, code !== 0
|
|
1466
|
+
? `test runtime failed with exit code ${code}`
|
|
1467
|
+
: "test runtime wrote to stderr", errorText, stdoutBuffer, stderrBuffer);
|
|
1453
1468
|
}
|
|
1454
1469
|
return report;
|
|
1455
1470
|
}
|
|
1456
|
-
function
|
|
1471
|
+
function createRuntimeFailureReport(specFile, modeName, title, details, stdout, stderr) {
|
|
1472
|
+
return appendRuntimeFailureReport({
|
|
1473
|
+
suites: [],
|
|
1474
|
+
coverage: {
|
|
1475
|
+
total: 0,
|
|
1476
|
+
covered: 0,
|
|
1477
|
+
uncovered: 0,
|
|
1478
|
+
percent: 100,
|
|
1479
|
+
points: [],
|
|
1480
|
+
},
|
|
1481
|
+
}, specFile, modeName, title, details, stdout, stderr);
|
|
1482
|
+
}
|
|
1483
|
+
function appendRuntimeFailureReport(report, specFile, modeName, title, details, stdout, stderr) {
|
|
1484
|
+
const suites = Array.isArray(report?.suites) ? report.suites : [];
|
|
1485
|
+
suites.push({
|
|
1486
|
+
file: specFile,
|
|
1487
|
+
description: path.basename(specFile),
|
|
1488
|
+
depth: 0,
|
|
1489
|
+
kind: "runtime-error",
|
|
1490
|
+
verdict: "fail",
|
|
1491
|
+
time: {
|
|
1492
|
+
start: 0,
|
|
1493
|
+
end: 0,
|
|
1494
|
+
},
|
|
1495
|
+
suites: [],
|
|
1496
|
+
logs: [],
|
|
1497
|
+
tests: [
|
|
1498
|
+
{
|
|
1499
|
+
order: 0,
|
|
1500
|
+
type: "runtime-error",
|
|
1501
|
+
verdict: "fail",
|
|
1502
|
+
left: null,
|
|
1503
|
+
right: null,
|
|
1504
|
+
instr: title,
|
|
1505
|
+
message: formatRuntimeFailureMessage(details, stdout, stderr),
|
|
1506
|
+
location: "",
|
|
1507
|
+
},
|
|
1508
|
+
],
|
|
1509
|
+
modeName: modeName ?? "default",
|
|
1510
|
+
});
|
|
1511
|
+
report.suites = suites;
|
|
1512
|
+
return report;
|
|
1513
|
+
}
|
|
1514
|
+
function formatRuntimeFailureMessage(details, stdout, stderr) {
|
|
1515
|
+
const parts = [];
|
|
1516
|
+
const normalizedDetails = normalizeRuntimeOutput(details);
|
|
1517
|
+
const normalizedStderr = normalizeRuntimeOutput(stderr);
|
|
1518
|
+
const normalizedStdout = normalizeRuntimeOutput(stdout);
|
|
1519
|
+
if (normalizedDetails.length)
|
|
1520
|
+
parts.push(normalizedDetails);
|
|
1521
|
+
if (normalizedStderr.length && normalizedStderr !== normalizedDetails) {
|
|
1522
|
+
parts.push(`stderr:\n${normalizedStderr}`);
|
|
1523
|
+
}
|
|
1524
|
+
if (normalizedStdout.length) {
|
|
1525
|
+
parts.push(`stdout:\n${normalizedStdout}`);
|
|
1526
|
+
}
|
|
1527
|
+
return parts.join("\n\n");
|
|
1528
|
+
}
|
|
1529
|
+
function normalizeRuntimeOutput(value) {
|
|
1530
|
+
return value.replace(/\r\n/g, "\n").trim();
|
|
1531
|
+
}
|
|
1532
|
+
function hasMeaningfulRuntimeOutput(value) {
|
|
1533
|
+
return normalizeRuntimeOutput(value).length > 0;
|
|
1534
|
+
}
|
|
1535
|
+
function shouldSuppressWasiWarningLine(line) {
|
|
1457
1536
|
if (line.includes("ExperimentalWarning: WASI is an experimental feature")) {
|
|
1458
1537
|
return true;
|
|
1459
1538
|
}
|
|
1460
|
-
if (
|
|
1539
|
+
if (line.includes("--trace-warnings")) {
|
|
1461
1540
|
return true;
|
|
1462
1541
|
}
|
|
1463
1542
|
return false;
|
|
@@ -1506,6 +1585,7 @@ function readFileReport(stats, fileReport) {
|
|
|
1506
1585
|
}
|
|
1507
1586
|
function readSuite(stats, suite, file, modeName) {
|
|
1508
1587
|
const suiteAny = suite;
|
|
1588
|
+
const kind = String(suiteAny.kind ?? "");
|
|
1509
1589
|
let verdict = normalizeVerdict(suiteAny.verdict);
|
|
1510
1590
|
const time = suiteAny.time;
|
|
1511
1591
|
const start = Number(time?.start ?? 0);
|
|
@@ -1533,6 +1613,20 @@ function readSuite(stats, suite, file, modeName) {
|
|
|
1533
1613
|
stats.skippedTests++;
|
|
1534
1614
|
}
|
|
1535
1615
|
}
|
|
1616
|
+
if (isTestCaseSuiteKind(kind)) {
|
|
1617
|
+
if (!subSuites.length && !tests.length) {
|
|
1618
|
+
if (verdict == "fail") {
|
|
1619
|
+
stats.failedTests++;
|
|
1620
|
+
}
|
|
1621
|
+
else if (verdict == "ok") {
|
|
1622
|
+
stats.passedTests++;
|
|
1623
|
+
}
|
|
1624
|
+
else if (verdict == "skip") {
|
|
1625
|
+
stats.skippedTests++;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return verdict;
|
|
1629
|
+
}
|
|
1536
1630
|
if (verdict == "fail") {
|
|
1537
1631
|
stats.failedSuites++;
|
|
1538
1632
|
stats.failedEntries.push({
|
|
@@ -1549,6 +1643,15 @@ function readSuite(stats, suite, file, modeName) {
|
|
|
1549
1643
|
}
|
|
1550
1644
|
return verdict;
|
|
1551
1645
|
}
|
|
1646
|
+
function isTestCaseSuiteKind(kind) {
|
|
1647
|
+
return (kind == "test" ||
|
|
1648
|
+
kind == "it" ||
|
|
1649
|
+
kind == "only" ||
|
|
1650
|
+
kind == "xtest" ||
|
|
1651
|
+
kind == "xit" ||
|
|
1652
|
+
kind == "xonly" ||
|
|
1653
|
+
kind == "todo");
|
|
1654
|
+
}
|
|
1552
1655
|
function normalizeVerdict(value) {
|
|
1553
1656
|
const verdict = String(value ?? "none");
|
|
1554
1657
|
if (verdict == "fail")
|
package/bin/commands/run.js
CHANGED
|
@@ -13,6 +13,7 @@ export async function executeRunCommand(rawArgs, flags, configPath, selectedMode
|
|
|
13
13
|
...deps.resolveParallelJobs(rawArgs, "run"),
|
|
14
14
|
coverage: featureToggles.coverage,
|
|
15
15
|
browser: deps.resolveBrowserOverride(rawArgs, "run"),
|
|
16
|
+
reporterPath: deps.resolveReporterOverride(rawArgs, "run"),
|
|
16
17
|
};
|
|
17
18
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
18
19
|
if (listFlags.list || listFlags.listModes) {
|
package/bin/commands/test.js
CHANGED
|
@@ -16,6 +16,7 @@ export async function executeTestCommand(rawArgs, flags, configPath, selectedMod
|
|
|
16
16
|
...deps.resolveParallelJobs(rawArgs, "test"),
|
|
17
17
|
coverage: featureToggles.coverage,
|
|
18
18
|
browser: deps.resolveBrowserOverride(rawArgs, "test"),
|
|
19
|
+
reporterPath: deps.resolveReporterOverride(rawArgs, "test"),
|
|
19
20
|
};
|
|
20
21
|
const fuzzEnabled = flags.includes("--fuzz");
|
|
21
22
|
const fuzzOverrides = deps.resolveFuzzOverrides(rawArgs, "test");
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, } from "./commands/build.js";
|
|
3
|
+
import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, getBuildReuseInfo, } from "./commands/build.js";
|
|
4
4
|
import { createRunReporter, run } from "./commands/run.js";
|
|
5
5
|
import { executeBuildCommand } from "./commands/build.js";
|
|
6
6
|
import { executeRunCommand } from "./commands/run.js";
|
|
@@ -14,7 +14,7 @@ import * as path from "path";
|
|
|
14
14
|
import { spawnSync } from "child_process";
|
|
15
15
|
import { glob } from "glob";
|
|
16
16
|
import { createInterface } from "readline";
|
|
17
|
-
import { existsSync } from "fs";
|
|
17
|
+
import { copyFileSync, existsSync, mkdirSync } from "fs";
|
|
18
18
|
import { availableParallelism, cpus } from "os";
|
|
19
19
|
import { BuildWorkerPool } from "./build-worker-pool.js";
|
|
20
20
|
const _args = process.argv.slice(2);
|
|
@@ -66,6 +66,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
66
66
|
resolveFeatureToggles,
|
|
67
67
|
resolveParallelJobs,
|
|
68
68
|
resolveBrowserOverride,
|
|
69
|
+
resolveReporterOverride,
|
|
69
70
|
resolveExecutionModes,
|
|
70
71
|
listExecutionPlan,
|
|
71
72
|
runRuntimeModes,
|
|
@@ -81,6 +82,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
81
82
|
resolveFeatureToggles,
|
|
82
83
|
resolveParallelJobs,
|
|
83
84
|
resolveBrowserOverride,
|
|
85
|
+
resolveReporterOverride,
|
|
84
86
|
resolveFuzzOverrides,
|
|
85
87
|
resolveExecutionModes,
|
|
86
88
|
listExecutionPlan,
|
|
@@ -400,7 +402,6 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
400
402
|
}
|
|
401
403
|
if (arg == "--runs" ||
|
|
402
404
|
arg == "--seed" ||
|
|
403
|
-
arg == "--parallel" ||
|
|
404
405
|
arg == "--jobs" ||
|
|
405
406
|
arg == "--build-jobs" ||
|
|
406
407
|
arg == "--run-jobs" ||
|
|
@@ -410,6 +411,9 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
410
411
|
i++;
|
|
411
412
|
continue;
|
|
412
413
|
}
|
|
414
|
+
if (arg == "--parallel") {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
413
417
|
if (arg.startsWith("--runs=") ||
|
|
414
418
|
arg.startsWith("--seed=") ||
|
|
415
419
|
arg.startsWith("--jobs=") ||
|
|
@@ -577,6 +581,32 @@ function resolveBrowserOverride(rawArgs, command) {
|
|
|
577
581
|
}
|
|
578
582
|
return undefined;
|
|
579
583
|
}
|
|
584
|
+
function resolveReporterOverride(rawArgs, command) {
|
|
585
|
+
let seenCommand = false;
|
|
586
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
587
|
+
const arg = rawArgs[i];
|
|
588
|
+
if (!seenCommand) {
|
|
589
|
+
if (arg == command)
|
|
590
|
+
seenCommand = true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (arg == "--reporter") {
|
|
594
|
+
const next = rawArgs[i + 1];
|
|
595
|
+
if (next && !next.startsWith("-")) {
|
|
596
|
+
return next;
|
|
597
|
+
}
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
if (arg.startsWith("--reporter=")) {
|
|
601
|
+
const value = arg.slice("--reporter=".length);
|
|
602
|
+
return value.length ? value : undefined;
|
|
603
|
+
}
|
|
604
|
+
if (arg == "--tap") {
|
|
605
|
+
return "tap";
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
580
610
|
function resolveJobs(rawArgs, command) {
|
|
581
611
|
let seenCommand = false;
|
|
582
612
|
let parallel = false;
|
|
@@ -731,9 +761,9 @@ function createBufferedStream() {
|
|
|
731
761
|
},
|
|
732
762
|
};
|
|
733
763
|
}
|
|
734
|
-
async function createBufferedReporter(configPath, modeName) {
|
|
764
|
+
async function createBufferedReporter(configPath, reporterPath, modeName) {
|
|
735
765
|
const stream = createBufferedStream();
|
|
736
|
-
const session = await createRunReporter(configPath,
|
|
766
|
+
const session = await createRunReporter(configPath, reporterPath, modeName, {
|
|
737
767
|
stdout: stream,
|
|
738
768
|
stderr: stream,
|
|
739
769
|
});
|
|
@@ -882,6 +912,32 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
882
912
|
}
|
|
883
913
|
return values;
|
|
884
914
|
}
|
|
915
|
+
async function buildFileForMode(cache, args) {
|
|
916
|
+
const reuse = await getBuildReuseInfo(args.configPath, args.file, args.modeName, args.buildFeatureToggles);
|
|
917
|
+
if (reuse) {
|
|
918
|
+
const source = cache.get(reuse.signature);
|
|
919
|
+
if (source && source != reuse.outFile && existsSync(source)) {
|
|
920
|
+
mkdirSync(path.dirname(reuse.outFile), { recursive: true });
|
|
921
|
+
copyFileSync(source, reuse.outFile);
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (args.buildPool) {
|
|
926
|
+
await args.buildPool.buildFileMode({
|
|
927
|
+
configPath: args.configPath,
|
|
928
|
+
file: args.file,
|
|
929
|
+
modeName: args.modeName,
|
|
930
|
+
featureToggles: args.buildFeatureToggles,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
await build(args.configPath, [args.file], args.modeName, args.buildFeatureToggles);
|
|
935
|
+
}
|
|
936
|
+
if (reuse) {
|
|
937
|
+
cache.set(reuse.signature, reuse.outFile);
|
|
938
|
+
}
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
885
941
|
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
|
|
886
942
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
887
943
|
if (!files.length) {
|
|
@@ -889,7 +945,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
889
945
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
890
946
|
}
|
|
891
947
|
}
|
|
892
|
-
const reporterSession = await createRunReporter(configPath,
|
|
948
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
|
|
893
949
|
const reporter = reporterOverride ?? reporterSession.reporter;
|
|
894
950
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
895
951
|
reporter.onRunStart?.({
|
|
@@ -963,17 +1019,19 @@ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles,
|
|
|
963
1019
|
const loadedConfig = loadConfig(resolvedConfigPath, true);
|
|
964
1020
|
const allStartedAt = Date.now();
|
|
965
1021
|
let builtCount = 0;
|
|
1022
|
+
const buildReuseCache = new Map();
|
|
966
1023
|
for (const modeName of modes) {
|
|
967
1024
|
const startedAt = Date.now();
|
|
968
1025
|
if (effective.buildJobs > 1) {
|
|
969
1026
|
const pool = new BuildWorkerPool(effective.buildJobs);
|
|
970
1027
|
try {
|
|
971
1028
|
await runOrderedPool(files, effective.buildJobs, async (file) => {
|
|
972
|
-
await
|
|
1029
|
+
await buildFileForMode(buildReuseCache, {
|
|
973
1030
|
configPath,
|
|
974
1031
|
file,
|
|
975
1032
|
modeName,
|
|
976
|
-
|
|
1033
|
+
buildFeatureToggles,
|
|
1034
|
+
buildPool: pool,
|
|
977
1035
|
});
|
|
978
1036
|
});
|
|
979
1037
|
}
|
|
@@ -982,7 +1040,14 @@ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles,
|
|
|
982
1040
|
}
|
|
983
1041
|
}
|
|
984
1042
|
else {
|
|
985
|
-
|
|
1043
|
+
for (const file of files) {
|
|
1044
|
+
await buildFileForMode(buildReuseCache, {
|
|
1045
|
+
configPath,
|
|
1046
|
+
file,
|
|
1047
|
+
modeName,
|
|
1048
|
+
buildFeatureToggles,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
986
1051
|
}
|
|
987
1052
|
builtCount += files.length;
|
|
988
1053
|
const active = applyMode(loadedConfig, modeName).config;
|
|
@@ -1022,6 +1087,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
|
1022
1087
|
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modes[0], {});
|
|
1023
1088
|
for (const modeName of modes) {
|
|
1024
1089
|
const result = await run(effectiveRunFlags, configPath, selectors, false, {
|
|
1090
|
+
reporterPath: effectiveRunFlags.reporterPath,
|
|
1025
1091
|
modeName,
|
|
1026
1092
|
modeSummaryTotal,
|
|
1027
1093
|
modeSummaryExecuted: 1,
|
|
@@ -1063,11 +1129,13 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
1063
1129
|
}));
|
|
1064
1130
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1065
1131
|
const buildIntervals = [];
|
|
1132
|
+
const buildReuseCache = new Map();
|
|
1066
1133
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
1067
1134
|
const file = files[fileIndex];
|
|
1068
1135
|
const fileName = path.basename(file);
|
|
1069
1136
|
const fileResults = [];
|
|
1070
1137
|
const modeTimes = modes.map(() => "...");
|
|
1138
|
+
const buildReuseCache = new Map();
|
|
1071
1139
|
if (liveMatrix) {
|
|
1072
1140
|
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
1073
1141
|
}
|
|
@@ -1104,7 +1172,9 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
1104
1172
|
throw error;
|
|
1105
1173
|
}
|
|
1106
1174
|
}
|
|
1107
|
-
|
|
1175
|
+
if (reporterSession.reporterKind == "default") {
|
|
1176
|
+
renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
|
|
1177
|
+
}
|
|
1108
1178
|
const verdict = resolveMatrixVerdict(fileResults);
|
|
1109
1179
|
if (verdict == "fail") {
|
|
1110
1180
|
fileState[fileIndex].failed = true;
|
|
@@ -1158,7 +1228,7 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
|
|
|
1158
1228
|
}
|
|
1159
1229
|
let failed = false;
|
|
1160
1230
|
for (const modeName of modes) {
|
|
1161
|
-
const reporterSession = await createRunReporter(configPath,
|
|
1231
|
+
const reporterSession = await createRunReporter(configPath, effectiveRunFlags.reporterPath, modeName);
|
|
1162
1232
|
const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
|
|
1163
1233
|
if (modeResult.failed)
|
|
1164
1234
|
failed = true;
|
|
@@ -1228,6 +1298,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1228
1298
|
const fileName = path.basename(file);
|
|
1229
1299
|
const fileResults = [];
|
|
1230
1300
|
const modeTimes = modes.map(() => "...");
|
|
1301
|
+
const buildReuseCache = new Map();
|
|
1231
1302
|
if (liveMatrix) {
|
|
1232
1303
|
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
1233
1304
|
}
|
|
@@ -1235,7 +1306,12 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1235
1306
|
const modeName = modes[i];
|
|
1236
1307
|
try {
|
|
1237
1308
|
const buildStartedAt = Date.now();
|
|
1238
|
-
await
|
|
1309
|
+
await buildFileForMode(buildReuseCache, {
|
|
1310
|
+
configPath,
|
|
1311
|
+
file,
|
|
1312
|
+
modeName,
|
|
1313
|
+
buildFeatureToggles,
|
|
1314
|
+
});
|
|
1239
1315
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1240
1316
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1241
1317
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
@@ -1267,7 +1343,9 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1267
1343
|
throw error;
|
|
1268
1344
|
}
|
|
1269
1345
|
}
|
|
1270
|
-
|
|
1346
|
+
if (reporterSession.reporterKind == "default") {
|
|
1347
|
+
renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
|
|
1348
|
+
}
|
|
1271
1349
|
const verdict = resolveMatrixVerdict(fileResults);
|
|
1272
1350
|
if (verdict == "fail") {
|
|
1273
1351
|
fileState[fileIndex].failed = true;
|
|
@@ -1330,7 +1408,7 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1330
1408
|
if (!files.length) {
|
|
1331
1409
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1332
1410
|
}
|
|
1333
|
-
const reporterSession = await createRunReporter(configPath,
|
|
1411
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
|
|
1334
1412
|
const reporter = reporterSession.reporter;
|
|
1335
1413
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1336
1414
|
reporter.onRunStart?.({
|
|
@@ -1342,15 +1420,20 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1342
1420
|
});
|
|
1343
1421
|
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modeName, {});
|
|
1344
1422
|
const results = new Array(files.length);
|
|
1345
|
-
const
|
|
1423
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1424
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1346
1425
|
const runLimit = createAsyncLimiter(runFlags.runJobs);
|
|
1347
1426
|
const poolWidth = Math.max(runFlags.buildJobs, runFlags.runJobs);
|
|
1348
1427
|
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1349
|
-
const token =
|
|
1350
|
-
|
|
1428
|
+
const token = useQueueDisplay
|
|
1429
|
+
? renderQueuedFileStart(queueDisplay, path.basename(file))
|
|
1430
|
+
: null;
|
|
1431
|
+
const buffered = useQueueDisplay
|
|
1432
|
+
? await createBufferedReporter(configPath, runFlags.reporterPath, modeName)
|
|
1433
|
+
: null;
|
|
1351
1434
|
const result = await runLimit(() => run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1352
|
-
reporter: buffered
|
|
1353
|
-
reporterKind: buffered
|
|
1435
|
+
reporter: buffered?.reporter,
|
|
1436
|
+
reporterKind: buffered?.reporterKind,
|
|
1354
1437
|
modeName,
|
|
1355
1438
|
emitRunComplete: false,
|
|
1356
1439
|
fileSummaryTotal: 1,
|
|
@@ -1358,9 +1441,11 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1358
1441
|
modeSummaryExecuted: 1,
|
|
1359
1442
|
buildCommandsByFile: { [file]: buildCommandsByFile[file] ?? "" },
|
|
1360
1443
|
}));
|
|
1361
|
-
buffered
|
|
1444
|
+
buffered?.reporter.flush?.();
|
|
1362
1445
|
results[index] = result;
|
|
1363
|
-
|
|
1446
|
+
if (buffered && token != null) {
|
|
1447
|
+
queueDisplay.complete(token, buffered.output());
|
|
1448
|
+
}
|
|
1364
1449
|
});
|
|
1365
1450
|
queueDisplay.flush();
|
|
1366
1451
|
const summary = aggregateRunResults(results);
|
|
@@ -1384,7 +1469,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1384
1469
|
if (!files.length) {
|
|
1385
1470
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1386
1471
|
}
|
|
1387
|
-
const reporterSession = await createRunReporter(configPath);
|
|
1472
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath);
|
|
1388
1473
|
const reporter = reporterSession.reporter;
|
|
1389
1474
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1390
1475
|
reporter.onRunStart?.({
|
|
@@ -1399,23 +1484,29 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1399
1484
|
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1400
1485
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1401
1486
|
const ordered = new Array(files.length);
|
|
1402
|
-
const
|
|
1487
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1488
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1403
1489
|
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1404
1490
|
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1405
1491
|
const buildIntervals = [];
|
|
1406
1492
|
try {
|
|
1407
1493
|
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1408
1494
|
const fileName = path.basename(file);
|
|
1409
|
-
const token =
|
|
1495
|
+
const token = useQueueDisplay
|
|
1496
|
+
? renderQueuedFileStart(queueDisplay, fileName)
|
|
1497
|
+
: null;
|
|
1410
1498
|
const fileResults = [];
|
|
1411
1499
|
const modeTimes = modes.map(() => "...");
|
|
1500
|
+
const buildReuseCache = new Map();
|
|
1412
1501
|
for (let i = 0; i < modes.length; i++) {
|
|
1413
1502
|
const modeName = modes[i];
|
|
1414
1503
|
const buildStartedAt = Date.now();
|
|
1415
|
-
await
|
|
1504
|
+
await buildFileForMode(buildReuseCache, {
|
|
1416
1505
|
configPath,
|
|
1417
1506
|
file,
|
|
1418
1507
|
modeName,
|
|
1508
|
+
buildFeatureToggles: {},
|
|
1509
|
+
buildPool,
|
|
1419
1510
|
});
|
|
1420
1511
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1421
1512
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
@@ -1434,7 +1525,9 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1434
1525
|
fileResults.push(result);
|
|
1435
1526
|
}
|
|
1436
1527
|
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1437
|
-
|
|
1528
|
+
if (token != null) {
|
|
1529
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1530
|
+
}
|
|
1438
1531
|
});
|
|
1439
1532
|
}
|
|
1440
1533
|
finally {
|
|
@@ -1481,7 +1574,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
|
|
|
1481
1574
|
if (!files.length && !fuzzEnabled) {
|
|
1482
1575
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1483
1576
|
}
|
|
1484
|
-
const reporterSession = await createRunReporter(configPath,
|
|
1577
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
|
|
1485
1578
|
const reporter = reporterSession.reporter;
|
|
1486
1579
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1487
1580
|
reporter.onRunStart?.({
|
|
@@ -1493,37 +1586,45 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
|
|
|
1493
1586
|
});
|
|
1494
1587
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1495
1588
|
const results = new Array(files.length);
|
|
1496
|
-
const
|
|
1589
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1590
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1497
1591
|
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1498
1592
|
const buildIntervals = [];
|
|
1499
1593
|
if (files.length) {
|
|
1500
1594
|
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1501
1595
|
try {
|
|
1502
1596
|
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1503
|
-
const token =
|
|
1597
|
+
const token = useQueueDisplay
|
|
1598
|
+
? renderQueuedFileStart(queueDisplay, path.basename(file))
|
|
1599
|
+
: null;
|
|
1504
1600
|
const buildStartedAt = Date.now();
|
|
1505
|
-
await
|
|
1601
|
+
await buildFileForMode(new Map(), {
|
|
1506
1602
|
configPath,
|
|
1507
1603
|
file,
|
|
1508
1604
|
modeName,
|
|
1509
|
-
|
|
1605
|
+
buildFeatureToggles,
|
|
1606
|
+
buildPool,
|
|
1510
1607
|
});
|
|
1511
1608
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1512
1609
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1513
1610
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1514
|
-
const buffered =
|
|
1611
|
+
const buffered = useQueueDisplay
|
|
1612
|
+
? await createBufferedReporter(configPath, runFlags.reporterPath, modeName)
|
|
1613
|
+
: null;
|
|
1515
1614
|
const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1516
|
-
reporter: buffered
|
|
1517
|
-
reporterKind: buffered
|
|
1615
|
+
reporter: buffered?.reporter,
|
|
1616
|
+
reporterKind: buffered?.reporterKind,
|
|
1518
1617
|
emitRunComplete: false,
|
|
1519
1618
|
logFileName: `test.${artifactKey}.log.json`,
|
|
1520
1619
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1521
1620
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1522
1621
|
modeName,
|
|
1523
1622
|
});
|
|
1524
|
-
buffered
|
|
1623
|
+
buffered?.reporter.flush?.();
|
|
1525
1624
|
results[index] = result;
|
|
1526
|
-
|
|
1625
|
+
if (buffered && token != null) {
|
|
1626
|
+
queueDisplay.complete(token, buffered.output());
|
|
1627
|
+
}
|
|
1527
1628
|
});
|
|
1528
1629
|
}
|
|
1529
1630
|
finally {
|
|
@@ -1572,7 +1673,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1572
1673
|
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1573
1674
|
}
|
|
1574
1675
|
}
|
|
1575
|
-
const reporterSession = await createRunReporter(configPath);
|
|
1676
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath);
|
|
1576
1677
|
const reporter = reporterSession.reporter;
|
|
1577
1678
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1578
1679
|
reporter.onRunStart?.({
|
|
@@ -1587,24 +1688,29 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1587
1688
|
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1588
1689
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1589
1690
|
const ordered = new Array(files.length);
|
|
1590
|
-
const
|
|
1691
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1692
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1591
1693
|
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1592
1694
|
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1593
1695
|
const buildIntervals = [];
|
|
1594
1696
|
try {
|
|
1595
1697
|
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1596
1698
|
const fileName = path.basename(file);
|
|
1597
|
-
const token =
|
|
1699
|
+
const token = useQueueDisplay
|
|
1700
|
+
? renderQueuedFileStart(queueDisplay, fileName)
|
|
1701
|
+
: null;
|
|
1598
1702
|
const fileResults = [];
|
|
1599
1703
|
const modeTimes = modes.map(() => "...");
|
|
1704
|
+
const buildReuseCache = new Map();
|
|
1600
1705
|
for (let i = 0; i < modes.length; i++) {
|
|
1601
1706
|
const modeName = modes[i];
|
|
1602
1707
|
const buildStartedAt = Date.now();
|
|
1603
|
-
await
|
|
1708
|
+
await buildFileForMode(buildReuseCache, {
|
|
1604
1709
|
configPath,
|
|
1605
1710
|
file,
|
|
1606
1711
|
modeName,
|
|
1607
|
-
|
|
1712
|
+
buildFeatureToggles,
|
|
1713
|
+
buildPool,
|
|
1608
1714
|
});
|
|
1609
1715
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1610
1716
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
@@ -1623,7 +1729,9 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1623
1729
|
fileResults.push(result);
|
|
1624
1730
|
}
|
|
1625
1731
|
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1626
|
-
|
|
1732
|
+
if (token != null) {
|
|
1733
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1734
|
+
}
|
|
1627
1735
|
});
|
|
1628
1736
|
}
|
|
1629
1737
|
finally {
|
package/bin/reporters/default.js
CHANGED
|
@@ -360,7 +360,19 @@ function renderFailedFuzzers(results) {
|
|
|
360
360
|
console.log(chalk.dim(`Runs: ${fuzzer.passed + fuzzer.failed + fuzzer.crashed} completed (${fuzzer.passed} passed, ${fuzzer.failed} failed, ${fuzzer.crashed} crashed)`));
|
|
361
361
|
console.log(chalk.dim(`Repro: ${repro}`));
|
|
362
362
|
console.log(chalk.dim(`Seed: ${modeResult.seed}`));
|
|
363
|
-
if (
|
|
363
|
+
if (fuzzer.failures?.length) {
|
|
364
|
+
console.log(chalk.dim(`Failing seeds: ${formatFailingSeeds(fuzzer)}`));
|
|
365
|
+
for (const failure of fuzzer.failures) {
|
|
366
|
+
console.log(chalk.dim(`Repro ${failure.run + 1}: ${buildFuzzReproCommand(relativeFile, failure.seed, modeResult.modeName, 1)}`));
|
|
367
|
+
if (failure.input) {
|
|
368
|
+
console.log(chalk.dim(`Input ${failure.run + 1}: ${JSON.stringify(failure.input)}`));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (fuzzer.crashFile?.length) {
|
|
373
|
+
console.log(chalk.dim(`Crash: ${fuzzer.crashFile}`));
|
|
374
|
+
}
|
|
375
|
+
else if (modeResult.crashFiles.length) {
|
|
364
376
|
console.log(chalk.dim(`Crash: ${modeResult.crashFiles[0]}`));
|
|
365
377
|
}
|
|
366
378
|
console.log("");
|
|
@@ -392,9 +404,13 @@ function averageFuzzModeTime(results) {
|
|
|
392
404
|
return 0;
|
|
393
405
|
return results.reduce((sum, result) => sum + result.time, 0) / results.length;
|
|
394
406
|
}
|
|
395
|
-
function buildFuzzReproCommand(file, seed, modeName) {
|
|
407
|
+
function buildFuzzReproCommand(file, seed, modeName, runs) {
|
|
396
408
|
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
397
|
-
|
|
409
|
+
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
410
|
+
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
411
|
+
}
|
|
412
|
+
function formatFailingSeeds(fuzzer) {
|
|
413
|
+
return (fuzzer.failures ?? []).map((failure) => String(failure.seed)).join(", ");
|
|
398
414
|
}
|
|
399
415
|
function toRelativeResultPath(file) {
|
|
400
416
|
const relative = path.relative(process.cwd(), path.resolve(process.cwd(), file));
|
|
@@ -465,12 +481,30 @@ function collectSuiteFailures(suite, file, path, printed) {
|
|
|
465
481
|
const loc = String(test.location ?? "");
|
|
466
482
|
const where = loc.length ? `${file}:${loc}` : file;
|
|
467
483
|
const modeName = String(suiteAny.modeName ?? "");
|
|
468
|
-
const
|
|
484
|
+
const message = String(test.message ?? "");
|
|
485
|
+
const dedupeKey = `${file}::${modeName}::${title}::${String(test.left)}::${String(test.right)}::${message}`;
|
|
469
486
|
if (printed.has(dedupeKey))
|
|
470
487
|
continue;
|
|
471
488
|
printed.add(dedupeKey);
|
|
472
489
|
const left = JSON.stringify(test.left);
|
|
473
490
|
const right = JSON.stringify(test.right);
|
|
491
|
+
if (left == "null" && right == "null") {
|
|
492
|
+
console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(title)} ${chalk.dim("(" + where + ")")}`);
|
|
493
|
+
if (modeName.length) {
|
|
494
|
+
console.log(chalk.dim(`Mode: ${modeName}`));
|
|
495
|
+
}
|
|
496
|
+
const normalizedMessage = normalizeFailureMessage(message);
|
|
497
|
+
if (normalizedMessage.length) {
|
|
498
|
+
for (const line of normalizedMessage.split("\n")) {
|
|
499
|
+
console.log(chalk.dim(line));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
console.log(chalk.dim("runtime error"));
|
|
504
|
+
}
|
|
505
|
+
console.log("");
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
474
508
|
const diffResult = diff(left, right);
|
|
475
509
|
let expected = "";
|
|
476
510
|
for (const res of diffResult.diff) {
|
|
@@ -506,6 +540,9 @@ function collectSuiteFailures(suite, file, path, printed) {
|
|
|
506
540
|
collectSuiteFailures(sub, file, nextPath, printed);
|
|
507
541
|
}
|
|
508
542
|
}
|
|
543
|
+
function normalizeFailureMessage(message) {
|
|
544
|
+
return message.replace(/\r\n/g, "\n").trim();
|
|
545
|
+
}
|
|
509
546
|
function renderSnapshotSummary(snapshotSummary, leadingGap = true) {
|
|
510
547
|
if (leadingGap) {
|
|
511
548
|
console.log("");
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "as-test",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"author": "Jairus Tanaka",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"access": "public"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
-
"test": "node ./bin/index.js test",
|
|
67
|
+
"test": "node ./bin/index.js test --parallel",
|
|
68
|
+
"test:ci": "node ./bin/index.js test --parallel --tap --config ./as-test.ci.config.json",
|
|
68
69
|
"fuzz": "node ./bin/index.js fuzz",
|
|
69
|
-
"test:ci": "node ./bin/index.js test --tap --config ./as-test.ci.config.json",
|
|
70
70
|
"test:examples": "npm --prefix ./examples run test",
|
|
71
71
|
"ci:act": "bash ./tools/act.sh push",
|
|
72
72
|
"ci:act:pr": "bash ./tools/act.sh pull_request",
|