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.
@@ -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, readCoverageSourceLine, resolveCoverageHighlightSpan, } from "../coverage-points.js";
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 = [`${this.badgeRunning()} ${this.currentFile}`];
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 ")} ${event.file}${time}`;
119
+ return `${chalk.bgRed.white(" FAIL ")} ${file}${time}`;
117
120
  if (this.fileHasWarning)
118
- return `${chalk.bgYellow.black(" WARN ")} ${event.file}${time}`;
121
+ return `${chalk.bgYellow.black(" WARN ")} ${file}${time}`;
119
122
  if (verdict == "ok")
120
- return `${chalk.bgGreenBright.black(" PASS ")} ${event.file}${time}`;
121
- return `${chalk.bgBlackBright.white(" SKIP ")} ${event.file}${time}`;
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} ${path.basename(file)} ${chalk.dim(detail)}${crashSuffix}\n`);
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(path.basename(modeResult.file))} ${chalk.dim("(crash)")}`);
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 ?? []).map((failure) => String(failure.seed)).join(", ");
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
- ? ` (${path.basename(file)}:${location})`
426
- : ` (${path.basename(file)})`;
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 printed = new Set();
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, [], printed);
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, printed, inheritedModeName = "") {
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 dedupeKey = `${file}::${modeName}::${title}::${String(test.left)}::${String(test.right)}::${message}`;
490
- if (printed.has(dedupeKey))
491
- continue;
492
- printed.add(dedupeKey);
493
- console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(title)} ${chalk.dim("(" + where + ")")}`);
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
- console.log(chalk.dim(`Mode: ${modeName}`));
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
- if (suitePath.length) {
498
- console.log(chalk.dim(`Repro: ${buildSuiteReproCommand(toRelativeResultPath(file), suitePath, modeName)}`));
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, printed, modeName);
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 ? "1 point missing" : `${summary.uncovered} points missing`;
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((a, b) => {
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
- if (point.executed)
718
- continue;
719
- const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
720
- const snippet = formatCoverageSnippet(point.file, point.line, point.column);
721
- const typeLabel = describeCoveragePoint(point.file, point.line, point.column, point.type).displayType.padEnd(layout.typeWidth + 4);
722
- const locationLabel = location.padEnd(layout.locationWidth + 4);
723
- console.log(` ${chalk.red("x")} ${chalk.dim(typeLabel)}${chalk.dim(locationLabel)}${snippet}`);
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}`.length), 1),
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 sourceLine = readCoverageSourceLine(file, line);
740
- if (!sourceLine)
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, Math.max(0, column - 1 - firstNonWhitespace)));
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 || typeof obj.ignore != "object" || Array.isArray(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]) => [name, cloneModeConfig(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 || typeof raw.output != "object" || Array.isArray(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 && typeof rawObject.ignore == "object" && !Array.isArray(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 || (runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))) {
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 && typeof raw.buildOptions == "object" && !Array.isArray(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 && typeof raw.runOptions == "object" && !Array.isArray(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)) {