11'use strict';
22const {
33 ArrayPrototypeFlatMap,
4+ ArrayPrototypeForEach,
45 ArrayPrototypeJoin,
56 ArrayPrototypeMap,
7+ ArrayPrototypePop,
68 ArrayPrototypePush,
79 ArrayPrototypeReduce,
810 ArrayPrototypeSome,
@@ -24,7 +26,7 @@ const {
2426} = primordials;
2527
2628const { AsyncResource } = require('async_hooks');
27- const { relative } = require('path');
29+ const { relative, sep } = require('path');
2830const { createWriteStream } = require('fs');
2931const { pathToFileURL } = require('internal/url');
3032const { createDeferredPromise } = require('internal/util');
@@ -409,6 +411,36 @@ const kColumns = ['line %', 'branch %', 'funcs %'];
409411const kColumnsKeys = ['coveredLinePercent', 'coveredBranchPercent', 'coveredFunctionPercent'];
410412const kSeparator = ' | ';
411413
414+ function buildFileTree(summary) {
415+ const tree = { __proto__: null };
416+ let treeDepth = 1;
417+ let longestFile = 0;
418+
419+ ArrayPrototypeForEach(summary.files, (file) => {
420+ let longestPart = 0;
421+ const parts = StringPrototypeSplit(relative(summary.workingDirectory, file.path), sep);
422+ let current = tree;
423+
424+ ArrayPrototypeForEach(parts, (part, index) => {
425+ if (!current[part]) {
426+ current[part] = { __proto__: null };
427+ }
428+ current = current[part];
429+ // If this is the last part, add the file to the tree
430+ if (index === parts.length - 1) {
431+ current.file = file;
432+ }
433+ // Keep track of the longest part for padding
434+ longestPart = MathMax(longestPart, part.length);
435+ });
436+
437+ treeDepth = MathMax(treeDepth, parts.length);
438+ longestFile = MathMax(longestPart, longestFile);
439+ });
440+
441+ return { __proto__: null, tree, treeDepth, longestFile };
442+ }
443+
412444function getCoverageReport(pad, summary, symbol, color, table) {
413445 const prefix = `${pad}${symbol}`;
414446 let report = `${color}${prefix}start of coverage report\n`;
@@ -418,11 +450,19 @@ function getCoverageReport(pad, summary, symbol, color, table) {
418450 let uncoveredLinesPadLength;
419451 let tableWidth;
420452
453+ // Create a tree of file paths
454+ const { tree, treeDepth, longestFile } = buildFileTree(summary);
421455 if (table) {
422- // Get expected column sizes
423- filePadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) =>
424- MathMax(acc, relative(summary.workingDirectory, file.path).length), 0);
456+ // Calculate expected column sizes based on the tree
457+ filePadLength = table && longestFile;
458+ filePadLength += (treeDepth - 1);
459+ if (color) {
460+ filePadLength += 2;
461+ }
425462 filePadLength = MathMax(filePadLength, 'file'.length);
463+ if (filePadLength > (process.stdout.columns / 2)) {
464+ filePadLength = MathFloor(process.stdout.columns / 2);
465+ }
426466 const fileWidth = filePadLength + 2;
427467
428468 columnPadLengths = ArrayPrototypeMap(kColumns, (column) => (table ? MathMax(column.length, 6) : 0));
@@ -435,26 +475,17 @@ function getCoverageReport(pad, summary, symbol, color, table) {
435475
436476 tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth;
437477
438- // Fit with sensible defaults
439478 const availableWidth = (process.stdout.columns || Infinity) - prefix.length;
440479 const columnsExtras = tableWidth - availableWidth;
441480 if (table && columnsExtras > 0) {
442- // Ensure file name is sufficiently visible
443- const minFilePad = MathMin(8, filePadLength);
444- filePadLength -= MathFloor(columnsExtras * 0.2);
445- filePadLength = MathMax(filePadLength, minFilePad);
446-
447- // Get rest of available space, subtracting margins
481+ filePadLength = MathMin(availableWidth * 0.5, filePadLength);
448482 uncoveredLinesPadLength = MathMax(availableWidth - columnsWidth - (filePadLength + 2) - 2, 1);
449-
450- // Update table width
451483 tableWidth = availableWidth;
452484 } else {
453485 uncoveredLinesPadLength = Infinity;
454486 }
455487 }
456488
457-
458489 function getCell(string, width, pad, truncate, coverage) {
459490 if (!table) return string;
460491
@@ -469,35 +500,85 @@ function getCoverageReport(pad, summary, symbol, color, table) {
469500 return result;
470501 }
471502
472- // Head
473- if (table) report += addTableLine( prefix, tableWidth) ;
474- report += `${prefix}${getCell('file', filePadLength, StringPrototypePadEnd, truncateEnd)}${kSeparator}` +
475- `${ArrayPrototypeJoin(ArrayPrototypeMap(kColumns, (column, i) => getCell(column, columnPadLengths[i], StringPrototypePadStart)), kSeparator)}${kSeparator}` +
476- `${ getCell('uncovered lines', uncoveredLinesPadLength , false, truncateEnd)}\n` ;
477- if (table) report += addTableLine(prefix, tableWidth );
503+ function writeReportLine({ file, depth = 0, coveragesColumns, fileCoverage, uncoveredLines }) {
504+ const fileColumn = `${ prefix}${StringPrototypeRepeat(' ', depth)}${getCell(file, filePadLength - depth, StringPrototypePadEnd, truncateStart, fileCoverage)}` ;
505+ const coverageColumns = ArrayPrototypeJoin(ArrayPrototypeMap(coveragesColumns, (coverage, j) => {
506+ const coverageText = typeof coverage === 'number' ? NumberPrototypeToFixed(coverage, 2) : coverage;
507+ return getCell(coverageText, columnPadLengths[j], StringPrototypePadStart , false, coverage) ;
508+ }), kSeparator );
478509
479- // Body
480- for (let i = 0; i < summary.files.length; ++i) {
481- const file = summary.files[i];
482- const relativePath = relative(summary.workingDirectory, file.path);
510+ const uncoveredLinesColumn = getCell(uncoveredLines, uncoveredLinesPadLength, false, truncateEnd);
483511
484- let fileCoverage = 0;
485- const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
486- const percent = file[columnKey];
487- fileCoverage += percent;
488- return percent;
489- });
490- fileCoverage /= kColumnsKeys.length;
512+ return `${fileColumn}${kSeparator}${coverageColumns}${kSeparator}${uncoveredLinesColumn}\n`;
513+ }
491514
492- report += `${prefix}${getCell(relativePath, filePadLength, StringPrototypePadEnd, truncateStart, fileCoverage)}${kSeparator}` +
493- `${ArrayPrototypeJoin(ArrayPrototypeMap(coverages, (coverage, j) => getCell(NumberPrototypeToFixed(coverage, 2), columnPadLengths[j], StringPrototypePadStart, false, coverage)), kSeparator)}${kSeparator}` +
494- `${getCell(formatUncoveredLines(getUncoveredLines(file.lines), table), uncoveredLinesPadLength, false, truncateEnd)}\n`;
515+ function printCoverageBodyTree(tree, depth = 0) {
516+ for (const key in tree) {
517+ if (tree[key].file) {
518+ const file = tree[key].file;
519+ const fileName = ArrayPrototypePop(StringPrototypeSplit(file.path, sep));
520+
521+ let fileCoverage = 0;
522+ const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
523+ const percent = file[columnKey];
524+ fileCoverage += percent;
525+ return percent;
526+ });
527+ fileCoverage /= kColumnsKeys.length;
528+
529+ const uncoveredLines = formatUncoveredLines(getUncoveredLines(file.lines), table);
530+
531+ report += writeReportLine({
532+ __proto__: null,
533+ file: fileName,
534+ depth: depth,
535+ coveragesColumns: coverages,
536+ fileCoverage: fileCoverage,
537+ uncoveredLines: uncoveredLines,
538+ });
539+ } else {
540+ report += writeReportLine({
541+ __proto__: null,
542+ file: key,
543+ depth: depth,
544+ coveragesColumns: ArrayPrototypeMap(columnPadLengths, () => ''),
545+ fileCoverage: undefined,
546+ uncoveredLines: '',
547+ });
548+ printCoverageBodyTree(tree[key], depth + 1);
549+ }
550+ }
495551 }
496552
497- // Foot
553+ // -------------------------- Coverage Report --------------------------
554+ if (table) report += addTableLine(prefix, tableWidth);
555+
556+ // Print the header
557+ report += writeReportLine({
558+ __proto__: null,
559+ file: 'file',
560+ coveragesColumns: kColumns,
561+ fileCoverage: undefined,
562+ uncoveredLines: 'uncovered lines',
563+ });
564+
565+ if (table) report += addTableLine(prefix, tableWidth);
566+
567+ // Print the body
568+ printCoverageBodyTree(tree);
569+
498570 if (table) report += addTableLine(prefix, tableWidth);
499- report += `${prefix}${getCell('all files', filePadLength, StringPrototypePadEnd, truncateEnd)}${kSeparator}` +
500- `${ArrayPrototypeJoin(ArrayPrototypeMap(kColumnsKeys, (columnKey, j) => getCell(NumberPrototypeToFixed(summary.totals[columnKey], 2), columnPadLengths[j], StringPrototypePadStart, false, summary.totals[columnKey])), kSeparator)} |\n`;
571+
572+ // Print the footer
573+ const allFilesCoverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => summary.totals[columnKey]);
574+ report += writeReportLine({
575+ __proto__: null,
576+ file: 'all files',
577+ coveragesColumns: allFilesCoverages,
578+ fileCoverage: undefined,
579+ uncoveredLines: '',
580+ });
581+
501582 if (table) report += addTableLine(prefix, tableWidth);
502583
503584 report += `${prefix}end of coverage report\n`;
0 commit comments