as-test 1.1.1 → 1.1.3
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 +11 -3
- package/README.md +14 -7
- package/as-test.config.schema.json +142 -142
- package/assembly/__fuzz__/math.fuzz.ts +17 -14
- package/assembly/__fuzz__/seed-perf.fuzz.ts +4 -2
- package/assembly/coverage.ts +12 -0
- package/assembly/index.ts +17 -4
- package/assembly/src/expectation.ts +7 -1
- package/assembly/src/fuzz.ts +44 -23
- package/assembly/src/suite.ts +2 -0
- package/assembly/test.ts +85 -0
- package/assembly/util/format.ts +10 -1
- package/assembly/util/wipc.ts +1 -2
- package/bin/build-worker-pool.js +7 -1
- package/bin/commands/build-core.js +6 -1
- package/bin/commands/build.js +1 -1
- package/bin/commands/clean-core.js +3 -1
- package/bin/commands/clean.js +0 -37
- package/bin/commands/fuzz-core.js +2 -2
- package/bin/commands/run-core.js +45 -24
- package/bin/commands/run.js +3 -1
- package/bin/commands/test.js +3 -1
- package/bin/commands/web-runner-source.js +14 -14
- package/bin/commands/web-session.js +6 -1
- package/bin/coverage-points.js +207 -2
- package/bin/crash-store.js +3 -1
- package/bin/index.js +357 -125
- package/bin/reporters/default.js +295 -64
- package/bin/util.js +36 -11
- package/lib/build/index.js +74 -24
- package/lib/src/index.ts +96 -35
- package/package.json +2 -1
- package/transform/lib/coverage.js +222 -71
package/bin/reporters/default.js
CHANGED
|
@@ -2,8 +2,8 @@ import chalk from "chalk";
|
|
|
2
2
|
import { diff } from "typer-diff";
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import { formatTime } from "../util.js";
|
|
6
|
-
import { describeCoveragePoint
|
|
5
|
+
import { formatSpecDisplayPath, formatTime } from "../util.js";
|
|
6
|
+
import { describeCoveragePoint } from "../coverage-points.js";
|
|
7
7
|
export const createReporter = (context) => {
|
|
8
8
|
return new DefaultReporter(context);
|
|
9
9
|
};
|
|
@@ -55,7 +55,9 @@ class DefaultReporter {
|
|
|
55
55
|
renderLiveState() {
|
|
56
56
|
if (!this.canRewriteLine() || !this.currentFile)
|
|
57
57
|
return;
|
|
58
|
-
const lines = [
|
|
58
|
+
const lines = [
|
|
59
|
+
`${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
|
|
60
|
+
];
|
|
59
61
|
for (const suite of this.openSuites) {
|
|
60
62
|
lines.push(`${" ".repeat(suite.depth + 1)}${this.badgeRunning()} ${suite.description}`);
|
|
61
63
|
}
|
|
@@ -67,7 +69,7 @@ class DefaultReporter {
|
|
|
67
69
|
const lines = [
|
|
68
70
|
fileEnd
|
|
69
71
|
? this.renderFileResult(fileEnd)
|
|
70
|
-
: `${this.badgeRunning()} ${this.currentFile}`,
|
|
72
|
+
: `${this.badgeRunning()} ${formatSpecDisplayPath(this.currentFile)}`,
|
|
71
73
|
];
|
|
72
74
|
for (const suite of this.verboseSuites) {
|
|
73
75
|
const badge = suite.verdict == "running"
|
|
@@ -112,13 +114,14 @@ class DefaultReporter {
|
|
|
112
114
|
renderFileResult(event) {
|
|
113
115
|
const verdict = event.verdict ?? "none";
|
|
114
116
|
const time = event.time ? ` ${chalk.dim(event.time)}` : "";
|
|
117
|
+
const file = formatSpecDisplayPath(event.file);
|
|
115
118
|
if (verdict == "fail")
|
|
116
|
-
return `${chalk.bgRed.white(" FAIL ")} ${
|
|
119
|
+
return `${chalk.bgRed.white(" FAIL ")} ${file}${time}`;
|
|
117
120
|
if (this.fileHasWarning)
|
|
118
|
-
return `${chalk.bgYellow.black(" WARN ")} ${
|
|
121
|
+
return `${chalk.bgYellow.black(" WARN ")} ${file}${time}`;
|
|
119
122
|
if (verdict == "ok")
|
|
120
|
-
return `${chalk.bgGreenBright.black(" PASS ")} ${
|
|
121
|
-
return `${chalk.bgBlackBright.white(" SKIP ")} ${
|
|
123
|
+
return `${chalk.bgGreenBright.black(" PASS ")} ${file}${time}`;
|
|
124
|
+
return `${chalk.bgBlackBright.white(" SKIP ")} ${file}${time}`;
|
|
122
125
|
}
|
|
123
126
|
onRunStart(event) {
|
|
124
127
|
this.verboseMode = Boolean(event.verbose);
|
|
@@ -139,16 +142,16 @@ class DefaultReporter {
|
|
|
139
142
|
}
|
|
140
143
|
if (!this.verboseMode) {
|
|
141
144
|
if (!this.canRewriteLine()) {
|
|
142
|
-
this.context.stdout.write(`${this.badgeRunning()} ${event.file}\n`);
|
|
145
|
+
this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`);
|
|
143
146
|
return;
|
|
144
147
|
}
|
|
145
148
|
this.clearRenderedBlock();
|
|
146
|
-
this.context.stdout.write(`${this.badgeRunning()} ${event.file}`);
|
|
149
|
+
this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}`);
|
|
147
150
|
this.renderedLines = 1;
|
|
148
151
|
return;
|
|
149
152
|
}
|
|
150
153
|
if (!this.canRewriteLine()) {
|
|
151
|
-
this.context.stdout.write(`${this.badgeRunning()} ${event.file}\n`);
|
|
154
|
+
this.context.stdout.write(`${this.badgeRunning()} ${formatSpecDisplayPath(event.file)}\n`);
|
|
152
155
|
return;
|
|
153
156
|
}
|
|
154
157
|
this.renderLiveState();
|
|
@@ -285,7 +288,7 @@ class DefaultReporter {
|
|
|
285
288
|
if (event.coverageSummary.enabled) {
|
|
286
289
|
renderCoverageSummary(event.coverageSummary, event.showCoverage);
|
|
287
290
|
if (event.showCoverage && event.coverageSummary.uncovered) {
|
|
288
|
-
renderCoveragePoints(event.coverageSummary.files);
|
|
291
|
+
renderCoveragePoints(event.coverageSummary.files, Boolean(event.verbose || event.showCoverageAll));
|
|
289
292
|
}
|
|
290
293
|
}
|
|
291
294
|
renderTotals(event.stats, event);
|
|
@@ -315,7 +318,7 @@ function renderFuzzFileSummary(context, results) {
|
|
|
315
318
|
const detail = formatTime(averageFuzzModeTime(results));
|
|
316
319
|
const crashFile = firstFuzzCrashFile(results);
|
|
317
320
|
const crashSuffix = crashFile != null ? ` ${chalk.dim(`-> ${crashFile}`)}` : "";
|
|
318
|
-
context.stdout.write(`${itemBadge} ${
|
|
321
|
+
context.stdout.write(`${itemBadge} ${formatSpecDisplayPath(file)} ${chalk.dim(detail)}${crashSuffix}\n`);
|
|
319
322
|
renderFailedFuzzers(groupFuzzResultsByFile(results));
|
|
320
323
|
}
|
|
321
324
|
function renderFuzzSummary(context, event, hasRenderedTestFiles) {
|
|
@@ -335,7 +338,7 @@ function renderFailedFuzzers(results) {
|
|
|
335
338
|
console.log("");
|
|
336
339
|
rendered = true;
|
|
337
340
|
}
|
|
338
|
-
console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(
|
|
341
|
+
console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(formatSpecDisplayPath(modeResult.file))} ${chalk.dim("(crash)")}`);
|
|
339
342
|
console.log(chalk.dim(`Mode: ${modeResult.modeName}`));
|
|
340
343
|
console.log(chalk.dim(`Runs: ${modeResult.runs} configured`));
|
|
341
344
|
console.log(chalk.dim(`Repro: ${repro}`));
|
|
@@ -413,7 +416,9 @@ function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
|
|
|
413
416
|
return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
|
|
414
417
|
}
|
|
415
418
|
function formatFailingSeeds(fuzzer) {
|
|
416
|
-
return (fuzzer.failures ?? [])
|
|
419
|
+
return (fuzzer.failures ?? [])
|
|
420
|
+
.map((failure) => String(failure.seed))
|
|
421
|
+
.join(", ");
|
|
417
422
|
}
|
|
418
423
|
function toRelativeResultPath(file) {
|
|
419
424
|
const relative = path.relative(process.cwd(), path.resolve(process.cwd(), file));
|
|
@@ -422,8 +427,8 @@ function toRelativeResultPath(file) {
|
|
|
422
427
|
function formatFuzzFailureTitle(file, name) {
|
|
423
428
|
const location = findFuzzLocation(file, name);
|
|
424
429
|
const suffix = location
|
|
425
|
-
? ` (${
|
|
426
|
-
: ` (${
|
|
430
|
+
? ` (${formatSpecDisplayPath(file)}:${location})`
|
|
431
|
+
: ` (${formatSpecDisplayPath(file)})`;
|
|
427
432
|
return `${chalk.dim(name)}${chalk.dim(suffix)}`;
|
|
428
433
|
}
|
|
429
434
|
function findFuzzLocation(file, name) {
|
|
@@ -460,19 +465,24 @@ function renderFailedSuites(failedEntries) {
|
|
|
460
465
|
if (!failedEntries.length)
|
|
461
466
|
return;
|
|
462
467
|
console.log("");
|
|
463
|
-
const
|
|
468
|
+
const grouped = new Map();
|
|
464
469
|
for (const failed of failedEntries) {
|
|
465
470
|
const failedAny = failed;
|
|
466
471
|
if (!failedAny?.file)
|
|
467
472
|
continue;
|
|
468
473
|
const file = String(failedAny.file);
|
|
469
|
-
collectSuiteFailures(failed, file, [],
|
|
474
|
+
collectSuiteFailures(failed, file, [], grouped);
|
|
475
|
+
}
|
|
476
|
+
for (const failure of grouped.values()) {
|
|
477
|
+
renderCollectedFailure(failure);
|
|
470
478
|
}
|
|
471
479
|
}
|
|
472
|
-
function collectSuiteFailures(suite, file, path,
|
|
480
|
+
function collectSuiteFailures(suite, file, path, grouped, inheritedModeName = "") {
|
|
473
481
|
const suiteAny = suite;
|
|
474
482
|
const nextPath = [...path, String(suiteAny.description ?? "unknown")];
|
|
475
483
|
const modeName = String(suiteAny.modeName ?? inheritedModeName);
|
|
484
|
+
const isRuntimeErrorSuite = String(suiteAny.kind ?? "") == "runtime-error";
|
|
485
|
+
const isBuildErrorSuite = String(suiteAny.kind ?? "") == "build-error";
|
|
476
486
|
const tests = Array.isArray(suiteAny.tests)
|
|
477
487
|
? suiteAny.tests
|
|
478
488
|
: [];
|
|
@@ -486,30 +496,159 @@ function collectSuiteFailures(suite, file, path, printed, inheritedModeName = ""
|
|
|
486
496
|
const where = loc.length ? `${file}:${loc}` : file;
|
|
487
497
|
const suitePath = String(suiteAny.path ?? "");
|
|
488
498
|
const message = String(test.message ?? "");
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
499
|
+
const left = test.left;
|
|
500
|
+
const right = test.right;
|
|
501
|
+
const dedupeKey = `${file}::${title}::${String(left)}::${String(right)}::${message}`;
|
|
502
|
+
let failure = grouped.get(dedupeKey);
|
|
503
|
+
if (!failure) {
|
|
504
|
+
failure = {
|
|
505
|
+
title,
|
|
506
|
+
where,
|
|
507
|
+
file,
|
|
508
|
+
suitePath,
|
|
509
|
+
left,
|
|
510
|
+
right,
|
|
511
|
+
message,
|
|
512
|
+
isRuntimeError: isRuntimeErrorSuite || String(test.type ?? "") == "runtime-error",
|
|
513
|
+
isBuildError: isBuildErrorSuite || String(test.type ?? "") == "build-error",
|
|
514
|
+
modes: new Set(),
|
|
515
|
+
runCommands: new Map(),
|
|
516
|
+
buildCommands: new Map(),
|
|
517
|
+
};
|
|
518
|
+
grouped.set(dedupeKey, failure);
|
|
519
|
+
}
|
|
494
520
|
if (modeName.length) {
|
|
495
|
-
|
|
521
|
+
failure.modes.add(modeName);
|
|
522
|
+
}
|
|
523
|
+
const runCommand = String(suiteAny.runCommand ?? "");
|
|
524
|
+
if (modeName.length && runCommand.length) {
|
|
525
|
+
failure.runCommands.set(modeName, runCommand);
|
|
496
526
|
}
|
|
497
|
-
|
|
498
|
-
|
|
527
|
+
const buildCommand = String(suiteAny.buildCommand ?? "");
|
|
528
|
+
if (modeName.length && buildCommand.length) {
|
|
529
|
+
failure.buildCommands.set(modeName, buildCommand);
|
|
499
530
|
}
|
|
500
|
-
renderAssertionFailureDetails(test.left, test.right, message);
|
|
501
531
|
}
|
|
502
532
|
const suites = Array.isArray(suiteAny.suites)
|
|
503
533
|
? suiteAny.suites
|
|
504
534
|
: [];
|
|
505
535
|
for (const sub of suites) {
|
|
506
|
-
collectSuiteFailures(sub, file, nextPath,
|
|
536
|
+
collectSuiteFailures(sub, file, nextPath, grouped, modeName);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function renderCollectedFailure(failure) {
|
|
540
|
+
console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(failure.title)} ${chalk.dim("(" + failure.where + ")")}`);
|
|
541
|
+
const modes = [...failure.modes].filter(Boolean).sort();
|
|
542
|
+
if (failure.isBuildError) {
|
|
543
|
+
renderBuildFailureDetails(failure, modes);
|
|
544
|
+
}
|
|
545
|
+
else if (failure.isRuntimeError) {
|
|
546
|
+
renderRuntimeFailureDetails(failure, modes);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
if (modes.length == 1) {
|
|
550
|
+
console.log(chalk.dim(`Mode: ${modes[0]}`));
|
|
551
|
+
}
|
|
552
|
+
else if (modes.length > 1) {
|
|
553
|
+
console.log(chalk.dim(`Modes: ${modes.join(", ")}`));
|
|
554
|
+
}
|
|
555
|
+
const relativeFile = toRelativeResultPath(failure.file);
|
|
556
|
+
const repro = failure.suitePath.length && modes.length == 1
|
|
557
|
+
? buildSuiteReproCommand(relativeFile, failure.suitePath, modes[0])
|
|
558
|
+
: buildFileReproCommand(relativeFile, modes);
|
|
559
|
+
console.log(chalk.dim(`Repro: ${repro}`));
|
|
560
|
+
renderModeCommands("Build", failure.buildCommands, modes);
|
|
561
|
+
renderModeCommands("Run", failure.runCommands, modes);
|
|
562
|
+
}
|
|
563
|
+
renderAssertionFailureDetails(failure.left, failure.right, failure.message);
|
|
564
|
+
}
|
|
565
|
+
function renderBuildFailureDetails(failure, modes) {
|
|
566
|
+
console.log("");
|
|
567
|
+
console.log(chalk.bold(" Oops! Looks like the test failed to build!"));
|
|
568
|
+
console.log(chalk.dim(" Here's some details and reproduction instructions if that helps:"));
|
|
569
|
+
console.log("");
|
|
570
|
+
console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
|
|
571
|
+
console.log("");
|
|
572
|
+
console.log(chalk.dim(" To reproduce, run the following commands:"));
|
|
573
|
+
for (const mode of modes.length ? modes : ["default"]) {
|
|
574
|
+
console.log(chalk.dim(` Mode: ${mode}`));
|
|
575
|
+
const buildCommand = failure.buildCommands.get(mode);
|
|
576
|
+
if (buildCommand?.length) {
|
|
577
|
+
console.log(chalk.dim(` Build: ${buildCommand}`));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
console.log("");
|
|
581
|
+
console.log(chalk.dim(" Here's a log dump too:"));
|
|
582
|
+
}
|
|
583
|
+
function renderRuntimeFailureDetails(failure, modes) {
|
|
584
|
+
console.log("");
|
|
585
|
+
console.log(chalk.bold(" Oops! Looks like the runtime crashed!"));
|
|
586
|
+
console.log(chalk.dim(" Here's some details and reproduction instructions if that helps:"));
|
|
587
|
+
console.log("");
|
|
588
|
+
console.log(chalk.dim(` Mode(s): ${modes.join(", ") || "default"}`));
|
|
589
|
+
console.log("");
|
|
590
|
+
console.log(chalk.dim(" To reproduce, run the following commands:"));
|
|
591
|
+
for (const mode of modes.length ? modes : ["default"]) {
|
|
592
|
+
console.log(chalk.dim(` Mode: ${mode}`));
|
|
593
|
+
const buildCommand = failure.buildCommands.get(mode);
|
|
594
|
+
if (buildCommand?.length) {
|
|
595
|
+
console.log(chalk.dim(` Build: ${buildCommand}`));
|
|
596
|
+
}
|
|
597
|
+
const runCommand = buildRuntimeReproRunCommand(failure.runCommands.get(mode) ?? "", buildCommand ?? "");
|
|
598
|
+
if (runCommand.length) {
|
|
599
|
+
console.log(chalk.dim(` Run: ${runCommand}`));
|
|
600
|
+
}
|
|
507
601
|
}
|
|
602
|
+
console.log("");
|
|
603
|
+
console.log(chalk.dim(" Here's a log dump too:"));
|
|
508
604
|
}
|
|
509
605
|
function buildSuiteReproCommand(file, suitePath, modeName) {
|
|
510
606
|
const modeArg = modeName && modeName != "default" ? ` --mode ${modeName}` : "";
|
|
511
607
|
return `ast run ${file}${modeArg} --suite ${suitePath}`;
|
|
512
608
|
}
|
|
609
|
+
function buildFileReproCommand(file, modes) {
|
|
610
|
+
const normalizedModes = modes.filter(Boolean).sort();
|
|
611
|
+
if (normalizedModes.length == 1 && normalizedModes[0] != "default") {
|
|
612
|
+
return `ast run ${file} --mode ${normalizedModes[0]}`;
|
|
613
|
+
}
|
|
614
|
+
if (normalizedModes.length > 1 &&
|
|
615
|
+
normalizedModes.every((mode) => mode != "default")) {
|
|
616
|
+
return `ast run ${file} --mode ${normalizedModes.join(",")}`;
|
|
617
|
+
}
|
|
618
|
+
return `ast run ${file}`;
|
|
619
|
+
}
|
|
620
|
+
function renderModeCommands(label, commands, modes) {
|
|
621
|
+
if (!commands.size)
|
|
622
|
+
return;
|
|
623
|
+
const uniqueCommands = new Set([...commands.values()].filter(Boolean));
|
|
624
|
+
if (uniqueCommands.size == 1) {
|
|
625
|
+
console.log(chalk.dim(`${label}: ${[...uniqueCommands][0]}`));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
console.log(chalk.dim(`${label} commands:`));
|
|
629
|
+
for (const mode of modes) {
|
|
630
|
+
const command = commands.get(mode);
|
|
631
|
+
if (!command)
|
|
632
|
+
continue;
|
|
633
|
+
console.log(chalk.dim(` [${mode}] ${command}`));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function buildRuntimeReproRunCommand(runCommand, buildCommand) {
|
|
637
|
+
if (!runCommand.length)
|
|
638
|
+
return "";
|
|
639
|
+
const artifactPath = extractBuildArtifactPath(buildCommand);
|
|
640
|
+
if (!artifactPath) {
|
|
641
|
+
return runCommand;
|
|
642
|
+
}
|
|
643
|
+
if (runCommand.includes(".as-test/runners/default.")) {
|
|
644
|
+
return `${runCommand} ${artifactPath}`;
|
|
645
|
+
}
|
|
646
|
+
return runCommand;
|
|
647
|
+
}
|
|
648
|
+
function extractBuildArtifactPath(buildCommand) {
|
|
649
|
+
const outMatch = buildCommand.match(/(?:^|\s)(?:-o|--outFile)\s+(?:"([^"]+)"|'([^']+)'|(\S+))/);
|
|
650
|
+
return outMatch?.[1] ?? outMatch?.[2] ?? outMatch?.[3] ?? null;
|
|
651
|
+
}
|
|
513
652
|
function normalizeFailureMessage(message) {
|
|
514
653
|
return message.replace(/\r\n/g, "\n").trim();
|
|
515
654
|
}
|
|
@@ -520,11 +659,13 @@ function renderAssertionFailureDetails(leftRaw, rightRaw, messageRaw) {
|
|
|
520
659
|
if (left == "null" && right == "null") {
|
|
521
660
|
const normalizedMessage = normalizeFailureMessage(message);
|
|
522
661
|
if (normalizedMessage.length) {
|
|
662
|
+
console.log("");
|
|
523
663
|
for (const line of normalizedMessage.split("\n")) {
|
|
524
664
|
console.log(chalk.dim(line));
|
|
525
665
|
}
|
|
526
666
|
}
|
|
527
667
|
else {
|
|
668
|
+
console.log("");
|
|
528
669
|
console.log(chalk.dim("runtime error"));
|
|
529
670
|
}
|
|
530
671
|
return;
|
|
@@ -656,7 +797,9 @@ function renderCoverageSummary(summary, showCoverage) {
|
|
|
656
797
|
const pct = summary.total
|
|
657
798
|
? ((summary.covered * 100) / summary.total).toFixed(2)
|
|
658
799
|
: "100.00";
|
|
659
|
-
const missingLabel = summary.uncovered == 1
|
|
800
|
+
const missingLabel = summary.uncovered == 1
|
|
801
|
+
? "1 point missing"
|
|
802
|
+
: `${summary.uncovered} points missing`;
|
|
660
803
|
const fileLabel = summary.files.length == 1 ? "1 file" : `${summary.files.length} files`;
|
|
661
804
|
const color = Number(pct) >= 90
|
|
662
805
|
? chalk.greenBright
|
|
@@ -681,16 +824,14 @@ function renderCoverageSummary(summary, showCoverage) {
|
|
|
681
824
|
: Number(filePct) >= 75
|
|
682
825
|
? chalk.yellowBright
|
|
683
826
|
: chalk.redBright;
|
|
684
|
-
const suffix = file.uncovered > 0
|
|
685
|
-
? `${file.uncovered} missing`
|
|
686
|
-
: "fully covered";
|
|
827
|
+
const suffix = file.uncovered > 0 ? `${file.uncovered} missing` : "fully covered";
|
|
687
828
|
console.log(` ${fileColor(filePct.padStart(6) + "%")} ${toRelativeResultPath(file.file).padEnd(36)} ${chalk.dim(`${file.covered}/${file.total} covered, ${suffix}`)}`);
|
|
688
829
|
}
|
|
689
830
|
if (ranked.length > 8) {
|
|
690
831
|
console.log(chalk.dim(` ... ${ranked.length - 8} more files`));
|
|
691
832
|
}
|
|
692
833
|
}
|
|
693
|
-
function renderCoveragePoints(files) {
|
|
834
|
+
function renderCoveragePoints(files, expandNested) {
|
|
694
835
|
console.log("");
|
|
695
836
|
console.log(chalk.bold("Coverage Gaps"));
|
|
696
837
|
const sortedFiles = [...files].sort((a, b) => a.file.localeCompare(b.file));
|
|
@@ -701,28 +842,124 @@ function renderCoveragePoints(files) {
|
|
|
701
842
|
displayType: describeCoveragePoint(point.file, point.line, point.column, point.type).displayType,
|
|
702
843
|
})));
|
|
703
844
|
const layout = createCoverageGapLayout(missingPoints);
|
|
845
|
+
let renderedFileCount = 0;
|
|
846
|
+
let collapsedNestedPoints = 0;
|
|
704
847
|
for (const file of sortedFiles) {
|
|
705
|
-
const points = [...file.points].sort(
|
|
706
|
-
if (a.line != b.line)
|
|
707
|
-
return a.line - b.line;
|
|
708
|
-
if (a.column != b.column)
|
|
709
|
-
return a.column - b.column;
|
|
710
|
-
return a.type.localeCompare(b.type);
|
|
711
|
-
});
|
|
848
|
+
const points = [...file.points].sort(compareCoverageGapPoints);
|
|
712
849
|
const missing = points.filter((point) => !point.executed);
|
|
713
850
|
if (!missing.length)
|
|
714
851
|
continue;
|
|
852
|
+
if (renderedFileCount > 0) {
|
|
853
|
+
console.log("");
|
|
854
|
+
}
|
|
715
855
|
console.log(` ${chalk.bold(toRelativeResultPath(file.file))} ${chalk.dim(`(${missing.length} uncovered)`)}`);
|
|
856
|
+
const pointsByHash = new Map(points.map((point) => [point.hash, point]));
|
|
857
|
+
const childrenByParent = new Map();
|
|
858
|
+
const roots = [];
|
|
716
859
|
for (const point of points) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
860
|
+
const parentHash = point.parentHash ?? "";
|
|
861
|
+
if (parentHash.length && pointsByHash.has(parentHash)) {
|
|
862
|
+
const children = childrenByParent.get(parentHash) ?? [];
|
|
863
|
+
children.push(point);
|
|
864
|
+
childrenByParent.set(parentHash, children);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
roots.push(point);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const visibleRoots = roots.filter((point) => shouldRenderCoveragePoint(point, childrenByParent));
|
|
871
|
+
for (let i = 0; i < visibleRoots.length; i++) {
|
|
872
|
+
collapsedNestedPoints += renderCoveragePointTree(visibleRoots[i], childrenByParent, layout, [], i == visibleRoots.length - 1, expandNested);
|
|
873
|
+
}
|
|
874
|
+
renderedFileCount++;
|
|
875
|
+
}
|
|
876
|
+
if (!expandNested && collapsedNestedPoints > 0) {
|
|
877
|
+
console.log("");
|
|
878
|
+
console.log(chalk.dim(" Run with --show-coverage=all or --verbose to expand nested coverage gaps."));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function renderCoveragePointTree(point, childrenByParent, layout, ancestorHasNext, isLast, expandNested) {
|
|
882
|
+
const visibleChildren = [...(childrenByParent.get(point.hash) ?? [])]
|
|
883
|
+
.filter((child) => shouldRenderCoveragePoint(child, childrenByParent))
|
|
884
|
+
.sort(compareCoverageGapPoints);
|
|
885
|
+
const nestedUncoveredCount = countNestedUncoveredPoints(visibleChildren, childrenByParent);
|
|
886
|
+
if (!point.executed) {
|
|
887
|
+
renderCoverageGapLine(point, layout, ancestorHasNext, isLast);
|
|
888
|
+
if (nestedUncoveredCount > 0) {
|
|
889
|
+
if (expandNested) {
|
|
890
|
+
let rendered = 0;
|
|
891
|
+
for (let i = 0; i < visibleChildren.length; i++) {
|
|
892
|
+
rendered += renderCoveragePointTree(visibleChildren[i], childrenByParent, layout, [...ancestorHasNext, !isLast], i == visibleChildren.length - 1, expandNested);
|
|
893
|
+
}
|
|
894
|
+
return 1 + rendered;
|
|
895
|
+
}
|
|
896
|
+
const treePrefix = buildCoverageTreePrefix([...ancestorHasNext, !isLast], true);
|
|
897
|
+
console.log(` ${treePrefix}${chalk.dim(`(+${nestedUncoveredCount} nested uncovered point${nestedUncoveredCount == 1 ? "" : "s"})`)}`);
|
|
898
|
+
return nestedUncoveredCount;
|
|
724
899
|
}
|
|
900
|
+
return 0;
|
|
901
|
+
}
|
|
902
|
+
if (nestedUncoveredCount <= 0)
|
|
903
|
+
return 0;
|
|
904
|
+
renderCoverageScopeHeader(point, layout, ancestorHasNext, isLast);
|
|
905
|
+
let rendered = 0;
|
|
906
|
+
for (let i = 0; i < visibleChildren.length; i++) {
|
|
907
|
+
rendered += renderCoveragePointTree(visibleChildren[i], childrenByParent, layout, [...ancestorHasNext, !isLast], i == visibleChildren.length - 1, expandNested);
|
|
908
|
+
}
|
|
909
|
+
return rendered;
|
|
910
|
+
}
|
|
911
|
+
function shouldRenderCoveragePoint(point, childrenByParent) {
|
|
912
|
+
if (!point.executed)
|
|
913
|
+
return true;
|
|
914
|
+
return (countNestedUncoveredPoints(childrenByParent.get(point.hash) ?? [], childrenByParent) > 0);
|
|
915
|
+
}
|
|
916
|
+
function countNestedUncoveredPoints(points, childrenByParent) {
|
|
917
|
+
let count = 0;
|
|
918
|
+
for (const point of points) {
|
|
919
|
+
if (!point.executed)
|
|
920
|
+
count++;
|
|
921
|
+
count += countNestedUncoveredPoints(childrenByParent.get(point.hash) ?? [], childrenByParent);
|
|
922
|
+
}
|
|
923
|
+
return count;
|
|
924
|
+
}
|
|
925
|
+
function renderCoverageGapLine(point, layout, ancestorHasNext, isLast) {
|
|
926
|
+
const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
|
|
927
|
+
const snippet = formatCoverageSnippet(point.file, point.line, point.column, point.type, ancestorHasNext.length);
|
|
928
|
+
const typeLabel = describeCoveragePoint(point.file, point.line, point.column, point.type).displayType.padEnd(layout.typeWidth + 6);
|
|
929
|
+
const locationLabel = location.padEnd(layout.locationWidth + 6);
|
|
930
|
+
const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
|
|
931
|
+
const meta = `${typeLabel}${locationLabel}`;
|
|
932
|
+
console.log(` ${treePrefix}${chalk.dim(meta)} ${snippet}`);
|
|
933
|
+
}
|
|
934
|
+
function renderCoverageScopeHeader(point, layout, ancestorHasNext, isLast) {
|
|
935
|
+
const descriptor = describeCoveragePoint(point.file, point.line, point.column, point.type);
|
|
936
|
+
const label = point.scopeKind || descriptor.displayType;
|
|
937
|
+
const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
|
|
938
|
+
const locationLabel = location.padEnd(layout.locationWidth + 6);
|
|
939
|
+
const typeLabel = label.padEnd(layout.typeWidth + 6);
|
|
940
|
+
const snippet = formatCoverageSnippet(point.file, point.line, point.column, point.type, ancestorHasNext.length);
|
|
941
|
+
const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
|
|
942
|
+
const meta = `${typeLabel}${locationLabel}`;
|
|
943
|
+
console.log(` ${treePrefix}${chalk.dim(meta)} ${chalk.dim(snippet)}`);
|
|
944
|
+
}
|
|
945
|
+
function buildCoverageTreePrefix(ancestorHasNext, isLast) {
|
|
946
|
+
let out = "";
|
|
947
|
+
for (const hasNext of ancestorHasNext) {
|
|
948
|
+
out += hasNext ? "│ " : " ";
|
|
725
949
|
}
|
|
950
|
+
out += isLast ? "└─" : "├─";
|
|
951
|
+
return chalk.dim(out);
|
|
952
|
+
}
|
|
953
|
+
function compareCoverageGapPoints(a, b) {
|
|
954
|
+
if (a.line != b.line)
|
|
955
|
+
return a.line - b.line;
|
|
956
|
+
if (a.column != b.column)
|
|
957
|
+
return a.column - b.column;
|
|
958
|
+
if ((a.depth ?? 0) != (b.depth ?? 0))
|
|
959
|
+
return (a.depth ?? 0) - (b.depth ?? 0);
|
|
960
|
+
if (a.type != b.type)
|
|
961
|
+
return a.type.localeCompare(b.type);
|
|
962
|
+
return a.hash.localeCompare(b.hash);
|
|
726
963
|
}
|
|
727
964
|
function renderCoverageBar(percent) {
|
|
728
965
|
const slots = 12;
|
|
@@ -732,35 +969,29 @@ function renderCoverageBar(percent) {
|
|
|
732
969
|
function createCoverageGapLayout(points) {
|
|
733
970
|
return {
|
|
734
971
|
typeWidth: Math.max(...points.map((point) => point.displayType.length), 5),
|
|
735
|
-
locationWidth: Math.max(...points.map((point) => `${toRelativeResultPath(point.file)}:${point.line}:${point.column}
|
|
972
|
+
locationWidth: Math.max(...points.map((point) => `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`
|
|
973
|
+
.length), 1),
|
|
736
974
|
};
|
|
737
975
|
}
|
|
738
|
-
function formatCoverageSnippet(file, line, column) {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
return "";
|
|
742
|
-
const expanded = sourceLine.replace(/\t/g, " ");
|
|
743
|
-
const firstNonWhitespace = expanded.search(/\S/);
|
|
744
|
-
if (firstNonWhitespace == -1)
|
|
745
|
-
return "";
|
|
746
|
-
const visible = expanded.slice(firstNonWhitespace).trimEnd();
|
|
976
|
+
function formatCoverageSnippet(file, line, column, fallbackType, _depth) {
|
|
977
|
+
const descriptor = describeCoveragePoint(file, line, column, fallbackType);
|
|
978
|
+
const visible = descriptor.visible;
|
|
747
979
|
if (!visible.length)
|
|
748
980
|
return "";
|
|
749
981
|
const maxWidth = 72;
|
|
750
|
-
const focus = Math.max(0, Math.min(visible.length - 1,
|
|
982
|
+
const focus = Math.max(0, Math.min(visible.length - 1, descriptor.focus));
|
|
751
983
|
if (visible.length <= maxWidth) {
|
|
752
|
-
return styleCoverageSnippetWindow(visible, 0, visible.length, focus);
|
|
984
|
+
return styleCoverageSnippetWindow(visible, 0, visible.length, focus, descriptor.highlightStart, descriptor.highlightEnd);
|
|
753
985
|
}
|
|
754
986
|
const start = Math.max(0, Math.min(visible.length - maxWidth, focus - Math.floor(maxWidth / 2)));
|
|
755
987
|
const end = Math.min(visible.length, start + maxWidth);
|
|
756
|
-
return styleCoverageSnippetWindow(visible, start, end, focus);
|
|
988
|
+
return styleCoverageSnippetWindow(visible, start, end, focus, descriptor.highlightStart, descriptor.highlightEnd);
|
|
757
989
|
}
|
|
758
|
-
function styleCoverageSnippetWindow(visible, start, end, focus) {
|
|
990
|
+
function styleCoverageSnippetWindow(visible, start, end, focus, highlightStart, highlightEnd) {
|
|
759
991
|
const prefix = start > 0 ? "..." : "";
|
|
760
992
|
const suffix = end < visible.length ? "..." : "";
|
|
761
993
|
const slice = visible.slice(start, end);
|
|
762
994
|
const localFocus = Math.max(0, Math.min(slice.length - 1, focus - start));
|
|
763
|
-
const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(visible, focus);
|
|
764
995
|
const localStart = Math.max(0, Math.min(slice.length, highlightStart - start));
|
|
765
996
|
const localEnd = Math.max(localStart + 1, Math.min(slice.length, highlightEnd - start));
|
|
766
997
|
if (!slice.length)
|
package/bin/util.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "fs";
|
|
|
2
2
|
import { BuildOptions, Config, CoverageOptions, CoverageIgnoreOptions, FuzzConfig, ModeConfig, ReporterConfig, RunOptions, Runtime, } from "./types.js";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import { delimiter, dirname, join, resolve } from "path";
|
|
5
|
+
import { basename, delimiter, dirname, join, relative, resolve, sep, } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
const CONFIG_META = new WeakMap();
|
|
8
8
|
export function formatTime(ms) {
|
|
@@ -28,6 +28,19 @@ export function formatTime(ms) {
|
|
|
28
28
|
}
|
|
29
29
|
return `${us}us`;
|
|
30
30
|
}
|
|
31
|
+
export function formatSpecDisplayPath(file) {
|
|
32
|
+
const resolved = resolve(file);
|
|
33
|
+
const parts = resolved.split(/[/\\]+/);
|
|
34
|
+
const testsIndex = parts.lastIndexOf("__tests__");
|
|
35
|
+
if (testsIndex >= 0 && testsIndex < parts.length - 1) {
|
|
36
|
+
return parts.slice(testsIndex + 1).join("/");
|
|
37
|
+
}
|
|
38
|
+
const rel = relative(process.cwd(), resolved).split(sep).join("/");
|
|
39
|
+
if (rel.length && rel != "." && rel != ".." && !rel.startsWith("../")) {
|
|
40
|
+
return rel;
|
|
41
|
+
}
|
|
42
|
+
return basename(resolved);
|
|
43
|
+
}
|
|
31
44
|
export function loadConfig(CONFIG_PATH, warn = false) {
|
|
32
45
|
const resolvedPath = resolve(CONFIG_PATH);
|
|
33
46
|
const raw = readConfigRaw(resolvedPath, warn);
|
|
@@ -102,9 +115,7 @@ function parseConfigRaw(raw, configPath) {
|
|
|
102
115
|
: "";
|
|
103
116
|
const cmd = runtimeRaw && typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length
|
|
104
117
|
? runtimeRaw.cmd
|
|
105
|
-
: runtimeRaw &&
|
|
106
|
-
typeof runtimeRaw.run == "string" &&
|
|
107
|
-
runtimeRaw.run.length
|
|
118
|
+
: runtimeRaw && typeof runtimeRaw.run == "string" && runtimeRaw.run.length
|
|
108
119
|
? runtimeRaw.run
|
|
109
120
|
: legacyRun
|
|
110
121
|
? legacyRun
|
|
@@ -343,7 +354,9 @@ function validateCoverageValue(value, path, issues) {
|
|
|
343
354
|
validateStringArrayField(obj, "include", path, issues);
|
|
344
355
|
validateStringArrayField(obj, "exclude", path, issues);
|
|
345
356
|
if ("ignore" in obj && obj.ignore != undefined) {
|
|
346
|
-
if (!obj.ignore ||
|
|
357
|
+
if (!obj.ignore ||
|
|
358
|
+
typeof obj.ignore != "object" ||
|
|
359
|
+
Array.isArray(obj.ignore)) {
|
|
347
360
|
issues.push({
|
|
348
361
|
path: `${path}.ignore`,
|
|
349
362
|
message: "must be an object",
|
|
@@ -967,14 +980,19 @@ function cloneConfig(config) {
|
|
|
967
980
|
cloned.runOptions = cloneRunOptions(config.runOptions);
|
|
968
981
|
cloned.fuzz = cloneFuzzConfig(config.fuzz);
|
|
969
982
|
cloned.coverage = cloneCoverageOptions(config.coverage);
|
|
970
|
-
cloned.modes = Object.fromEntries(Object.entries(config.modes).map(([name, mode]) => [
|
|
983
|
+
cloned.modes = Object.fromEntries(Object.entries(config.modes).map(([name, mode]) => [
|
|
984
|
+
name,
|
|
985
|
+
cloneModeConfig(mode),
|
|
986
|
+
]));
|
|
971
987
|
CONFIG_META.set(cloned, getConfigMeta(config));
|
|
972
988
|
return cloned;
|
|
973
989
|
}
|
|
974
990
|
function outputOverridesField(raw, field) {
|
|
975
991
|
if (field in raw)
|
|
976
992
|
return true;
|
|
977
|
-
if (!raw.output ||
|
|
993
|
+
if (!raw.output ||
|
|
994
|
+
typeof raw.output != "object" ||
|
|
995
|
+
Array.isArray(raw.output)) {
|
|
978
996
|
return false;
|
|
979
997
|
}
|
|
980
998
|
const output = raw.output;
|
|
@@ -1023,7 +1041,9 @@ function mergeCoverageConfig(base, override, raw) {
|
|
|
1023
1041
|
mergedBase.include = [...overrideOptions.include];
|
|
1024
1042
|
if ("exclude" in rawObject)
|
|
1025
1043
|
mergedBase.exclude = [...overrideOptions.exclude];
|
|
1026
|
-
if (rawObject.ignore &&
|
|
1044
|
+
if (rawObject.ignore &&
|
|
1045
|
+
typeof rawObject.ignore == "object" &&
|
|
1046
|
+
!Array.isArray(rawObject.ignore)) {
|
|
1027
1047
|
mergedBase.ignore = mergeCoverageIgnoreOptions(mergedBase.ignore, overrideOptions.ignore, rawObject.ignore);
|
|
1028
1048
|
}
|
|
1029
1049
|
return mergedBase;
|
|
@@ -1068,7 +1088,8 @@ function mergeRunOptions(base, override, raw) {
|
|
|
1068
1088
|
const merged = cloneRunOptions(base);
|
|
1069
1089
|
if ("runtime" in raw || "run" in raw) {
|
|
1070
1090
|
const runtimeRaw = raw.runtime;
|
|
1071
|
-
if ("run" in raw ||
|
|
1091
|
+
if ("run" in raw ||
|
|
1092
|
+
(runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))) {
|
|
1072
1093
|
merged.runtime.cmd = override.runtime.cmd;
|
|
1073
1094
|
}
|
|
1074
1095
|
if (runtimeRaw && "browser" in runtimeRaw) {
|
|
@@ -1126,10 +1147,14 @@ function mergeRootConfig(base, override) {
|
|
|
1126
1147
|
if ("env" in raw) {
|
|
1127
1148
|
merged.env = { ...override.env };
|
|
1128
1149
|
}
|
|
1129
|
-
if (raw.buildOptions &&
|
|
1150
|
+
if (raw.buildOptions &&
|
|
1151
|
+
typeof raw.buildOptions == "object" &&
|
|
1152
|
+
!Array.isArray(raw.buildOptions)) {
|
|
1130
1153
|
merged.buildOptions = mergeBuildOptions(merged.buildOptions, override.buildOptions, raw.buildOptions);
|
|
1131
1154
|
}
|
|
1132
|
-
if (raw.runOptions &&
|
|
1155
|
+
if (raw.runOptions &&
|
|
1156
|
+
typeof raw.runOptions == "object" &&
|
|
1157
|
+
!Array.isArray(raw.runOptions)) {
|
|
1133
1158
|
merged.runOptions = mergeRunOptions(merged.runOptions, override.runOptions, raw.runOptions);
|
|
1134
1159
|
}
|
|
1135
1160
|
if (raw.fuzz && typeof raw.fuzz == "object" && !Array.isArray(raw.fuzz)) {
|