@cldmv/vitest-runner 1.2.0 → 1.4.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/README.md +50 -11
- package/bin/vitest-runner.mjs +110 -83
- package/dist/index.cjs +21 -0
- package/dist/index.mjs +47 -0
- package/package.json +31 -13
- package/types/src/bin/vitest-runner.d.mts +2 -0
- package/types/src/cli/args.d.mts +8 -0
- package/types/src/core/discover.d.mts +23 -0
- package/types/src/core/scratch.d.mts +64 -0
- package/types/src/core/spawn.d.mts +3 -1
- package/types/src/runner.d.mts +15 -0
- package/types/src/utils/ansi.d.mts +12 -0
- package/types/src/utils/duration.d.mts +12 -0
- package/types/src/utils/env.d.mts +12 -0
- package/index.cjs +0 -17
- package/index.mjs +0 -5
- package/src/cli/args.mjs +0 -148
- package/src/cli/help.mjs +0 -86
- package/src/core/discover.mjs +0 -167
- package/src/core/parse.mjs +0 -167
- package/src/core/progress.mjs +0 -164
- package/src/core/report.mjs +0 -255
- package/src/core/spawn.mjs +0 -219
- package/src/runner.mjs +0 -708
- package/src/utils/ansi.mjs +0 -32
- package/src/utils/duration.mjs +0 -25
- package/src/utils/env.mjs +0 -38
- package/src/utils/resolve.mjs +0 -86
package/src/core/parse.mjs
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Vitest output parsing and error deduplication utilities.
|
|
3
|
-
* @module vitest-runner/src/core/parse
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { stripAnsi } from "../utils/ansi.mjs";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {Object} ParsedVitestResult
|
|
10
|
-
* @property {number} testFilesPass - Number of test files that passed.
|
|
11
|
-
* @property {number} testFilesFail - Number of test files that failed.
|
|
12
|
-
* @property {number} testsPass - Number of individual tests that passed.
|
|
13
|
-
* @property {number} testsFail - Number of individual tests that failed.
|
|
14
|
-
* @property {number} testsSkip - Number of individual tests that were skipped.
|
|
15
|
-
* @property {number} duration - Duration in milliseconds (from Vitest output).
|
|
16
|
-
* @property {number|null} heapMb - Peak heap usage in MB, or `null` if not reported.
|
|
17
|
-
* @property {string[]} errors - Array of raw error blocks (with ANSI codes).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Parse raw Vitest stdout/stderr output into structured result data.
|
|
22
|
-
*
|
|
23
|
-
* Extracts test-file counts, individual test counts, duration, heap usage,
|
|
24
|
-
* and error blocks. Counts are parsed from ANSI-stripped output; error blocks
|
|
25
|
-
* are captured from the original coloured output.
|
|
26
|
-
*
|
|
27
|
-
* @param {string} output - Combined raw stdout + stderr from a vitest child process.
|
|
28
|
-
* @returns {ParsedVitestResult}
|
|
29
|
-
* @example
|
|
30
|
-
* const result = parseVitestOutput(rawOutput);
|
|
31
|
-
* console.log(result.testsPass, result.testsFail);
|
|
32
|
-
*/
|
|
33
|
-
export function parseVitestOutput(output) {
|
|
34
|
-
const cleanOutput = stripAnsi(output);
|
|
35
|
-
|
|
36
|
-
const result = {
|
|
37
|
-
testFilesPass: 0,
|
|
38
|
-
testFilesFail: 0,
|
|
39
|
-
testsPass: 0,
|
|
40
|
-
testsFail: 0,
|
|
41
|
-
testsSkip: 0,
|
|
42
|
-
duration: 0,
|
|
43
|
-
heapMb: null,
|
|
44
|
-
errors: []
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// "Test Files 1 passed (1)" / "Test Files 1 failed | 2 passed (3)"
|
|
48
|
-
const testFilesLineMatch = cleanOutput.match(/Test Files\s+(.+)/);
|
|
49
|
-
if (testFilesLineMatch) {
|
|
50
|
-
const line = testFilesLineMatch[1];
|
|
51
|
-
const passMatch = line.match(/(\d+)\s+passed/);
|
|
52
|
-
const failMatch = line.match(/(\d+)\s+failed/);
|
|
53
|
-
if (passMatch) result.testFilesPass = parseInt(passMatch[1], 10);
|
|
54
|
-
if (failMatch) result.testFilesFail = parseInt(failMatch[1], 10);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// "Tests 82 passed (82)" / "Tests 2 failed | 4 passed (6)"
|
|
58
|
-
const testsLineMatch = cleanOutput.match(/^\s*Tests\s+(.+)$/m);
|
|
59
|
-
if (testsLineMatch) {
|
|
60
|
-
const line = testsLineMatch[1];
|
|
61
|
-
const passMatch = line.match(/(\d+)\s+passed/);
|
|
62
|
-
const failMatch = line.match(/(\d+)\s+failed/);
|
|
63
|
-
const skipMatch = line.match(/(\d+)\s+skipped/);
|
|
64
|
-
if (passMatch) result.testsPass = parseInt(passMatch[1], 10);
|
|
65
|
-
if (failMatch) result.testsFail = parseInt(failMatch[1], 10);
|
|
66
|
-
if (skipMatch) result.testsSkip = parseInt(skipMatch[1], 10);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// "Duration Xs"
|
|
70
|
-
const durationMatch = cleanOutput.match(/Duration\s+([\d.]+)s/);
|
|
71
|
-
if (durationMatch) {
|
|
72
|
-
result.duration = parseFloat(durationMatch[1]) * 1000;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// "N MB heap used"
|
|
76
|
-
const heapMatch = cleanOutput.match(/(\d+)\s*MB\s+heap\s+used/i);
|
|
77
|
-
if (heapMatch) {
|
|
78
|
-
result.heapMb = parseInt(heapMatch[1], 10);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Error blocks — captured from the raw (coloured) output
|
|
82
|
-
const failedSectionStart = output.indexOf("Failed Tests");
|
|
83
|
-
if (failedSectionStart !== -1) {
|
|
84
|
-
const failedSectionEnd = output.indexOf("\n Test Files", failedSectionStart);
|
|
85
|
-
const errorSection =
|
|
86
|
-
failedSectionEnd !== -1 ? output.substring(failedSectionStart, failedSectionEnd) : output.substring(failedSectionStart);
|
|
87
|
-
|
|
88
|
-
// eslint-disable-next-line no-control-regex
|
|
89
|
-
const failPattern = /FAIL\s*(?:\x1B\[[0-9;]*[a-zA-Z]|\s)*tests\//g;
|
|
90
|
-
const matches = [...errorSection.matchAll(failPattern)];
|
|
91
|
-
|
|
92
|
-
for (let i = 0; i < matches.length; i++) {
|
|
93
|
-
const matchPos = matches[i].index;
|
|
94
|
-
const lineStart = errorSection.lastIndexOf("\n", matchPos);
|
|
95
|
-
const actualStart = lineStart === -1 ? 0 : lineStart;
|
|
96
|
-
|
|
97
|
-
const matchEnd = i < matches.length - 1 ? matches[i + 1].index : errorSection.length;
|
|
98
|
-
const nextLineStart = errorSection.lastIndexOf("\n", matchEnd);
|
|
99
|
-
const actualEnd = nextLineStart === -1 ? matchEnd : nextLineStart;
|
|
100
|
-
|
|
101
|
-
const errorBlock = errorSection.substring(actualStart, actualEnd).trim();
|
|
102
|
-
if (errorBlock) result.errors.push(errorBlock);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Deduplicate similar FAIL lines that differ only by their `Config:` value.
|
|
111
|
-
*
|
|
112
|
-
* When the same test file fails across multiple matrix configs vitest emits
|
|
113
|
-
* one FAIL line per config. This collapses them into a single line listing
|
|
114
|
-
* all configs as an array, keeping output concise.
|
|
115
|
-
*
|
|
116
|
-
* @param {string[]} errors - Array of raw error blocks (each a complete FAIL section).
|
|
117
|
-
* @returns {string} Deduplicated error text joined as a single string.
|
|
118
|
-
* @example
|
|
119
|
-
* const deduped = deduplicateErrors(result.errors);
|
|
120
|
-
* console.log(deduped);
|
|
121
|
-
*/
|
|
122
|
-
export function deduplicateErrors(errors) {
|
|
123
|
-
const fullText = errors.join("\n");
|
|
124
|
-
const lines = fullText.split("\n");
|
|
125
|
-
|
|
126
|
-
const failLineMap = new Map();
|
|
127
|
-
const lineIndices = new Map();
|
|
128
|
-
|
|
129
|
-
lines.forEach((line, idx) => {
|
|
130
|
-
if (line.includes("FAIL") && line.includes("Config:")) {
|
|
131
|
-
lineIndices.set(line, idx);
|
|
132
|
-
|
|
133
|
-
const cleaned = stripAnsi(line);
|
|
134
|
-
const match = cleaned.match(/^(.+Config:\s+)'*([^'>]+)'*(.+)$/);
|
|
135
|
-
if (match) {
|
|
136
|
-
const [, before, config, after] = match;
|
|
137
|
-
const pattern = `${before.trim()}|||${after.trim()}`;
|
|
138
|
-
|
|
139
|
-
if (!failLineMap.has(pattern)) {
|
|
140
|
-
failLineMap.set(pattern, { lines: [], configs: [] });
|
|
141
|
-
}
|
|
142
|
-
failLineMap.get(pattern).lines.push(line);
|
|
143
|
-
failLineMap.get(pattern).configs.push(config.replace(/'/g, ""));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const skipIndices = new Set();
|
|
149
|
-
|
|
150
|
-
for (const [, data] of failLineMap.entries()) {
|
|
151
|
-
if (data.configs.length > 1) {
|
|
152
|
-
for (let i = 1; i < data.lines.length; i++) {
|
|
153
|
-
const idx = lineIndices.get(data.lines[i]);
|
|
154
|
-
skipIndices.add(idx);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const firstLine = data.lines[0];
|
|
158
|
-
const configArray = `[${data.configs.map((c) => `'${c}'`).join(",")}]`;
|
|
159
|
-
const consolidated = stripAnsi(firstLine).replace(/(Config:\s+)'*[^'>]+'*/, `$1${configArray}`);
|
|
160
|
-
|
|
161
|
-
const firstIdx = lineIndices.get(firstLine);
|
|
162
|
-
lines[firstIdx] = consolidated;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return lines.filter((_, idx) => !skipIndices.has(idx)).join("\n");
|
|
167
|
-
}
|
package/src/core/progress.mjs
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Live progress tracker for coverage runs.
|
|
3
|
-
* @module vitest-runner/src/core/progress
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import chalk from "chalk";
|
|
7
|
-
import { formatDuration } from "../utils/duration.mjs";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Create a live progress tracker that renders to stdout.
|
|
11
|
-
*
|
|
12
|
-
* In TTY mode a spinner + bar overwrites the current line on each update.
|
|
13
|
-
* In non-TTY mode a plain-text line is printed at most once every two seconds,
|
|
14
|
-
* plus once on every `onComplete` call.
|
|
15
|
-
*
|
|
16
|
-
* @param {number} total - Total number of files to process.
|
|
17
|
-
* @returns {{ onStart: () => void, onComplete: (failedRun: boolean) => void, finish: () => void }}
|
|
18
|
-
* @example
|
|
19
|
-
* const progress = createCoverageProgressTracker(120);
|
|
20
|
-
* progress.onStart();
|
|
21
|
-
* progress.onComplete(false);
|
|
22
|
-
* progress.finish();
|
|
23
|
-
*/
|
|
24
|
-
export function createCoverageProgressTracker(total) {
|
|
25
|
-
const isTTY = Boolean(process.stdout.isTTY);
|
|
26
|
-
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
27
|
-
const barWidth = 26;
|
|
28
|
-
const startTime = Date.now();
|
|
29
|
-
let completed = 0;
|
|
30
|
-
let active = 0;
|
|
31
|
-
let failed = 0;
|
|
32
|
-
let frameIndex = 0;
|
|
33
|
-
let maxLineLength = 0;
|
|
34
|
-
let lastPlainLog = 0;
|
|
35
|
-
let spinnerTimer = null;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Build the current progress line text.
|
|
39
|
-
* @returns {string}
|
|
40
|
-
*/
|
|
41
|
-
function buildLine() {
|
|
42
|
-
const percent = total === 0 ? 100 : (completed / total) * 100;
|
|
43
|
-
const elapsedMs = Date.now() - startTime;
|
|
44
|
-
const avgPerFile = completed > 0 ? elapsedMs / completed : 0;
|
|
45
|
-
const etaMs = avgPerFile * Math.max(total - completed, 0);
|
|
46
|
-
const filled = Math.round((percent / 100) * barWidth);
|
|
47
|
-
const spinner = spinnerFrames[frameIndex % spinnerFrames.length];
|
|
48
|
-
const bar = `[${"=".repeat(filled)}${"-".repeat(Math.max(0, barWidth - filled))}]`;
|
|
49
|
-
|
|
50
|
-
let _percent = percent.toFixed(1).padStart(5);
|
|
51
|
-
if (percent < 30) {
|
|
52
|
-
_percent = chalk.red(_percent + "%");
|
|
53
|
-
} else if (percent < 70) {
|
|
54
|
-
_percent = chalk.rgb(255, 136, 0)(_percent + "%");
|
|
55
|
-
} else if (percent > 99) {
|
|
56
|
-
_percent = chalk.green(_percent + "%");
|
|
57
|
-
} else {
|
|
58
|
-
_percent = chalk.yellow(_percent + "%");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (isTTY) {
|
|
62
|
-
return `${chalk.green(spinner)} ${chalk.green(bar)} ${chalk.bold(_percent)} ${completed}/${total} | active ${active} | failed ${failed} | ETA ${formatDuration(etaMs)} | elapsed ${formatDuration(elapsedMs)}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return `progress ${percent.toFixed(1)}% ${completed}/${total} | active ${active} | failed ${failed} | eta ${formatDuration(etaMs)} | elapsed ${formatDuration(elapsedMs)}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Render the progress line to stdout.
|
|
70
|
-
* @param {boolean} [forcePlainLog=false] - Force a plain log line in non-TTY mode.
|
|
71
|
-
* @returns {void}
|
|
72
|
-
*/
|
|
73
|
-
function render(forcePlainLog = false) {
|
|
74
|
-
const line = buildLine();
|
|
75
|
-
frameIndex++;
|
|
76
|
-
|
|
77
|
-
if (isTTY) {
|
|
78
|
-
// eslint-disable-next-line no-control-regex
|
|
79
|
-
const visibleLength = line.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").length;
|
|
80
|
-
maxLineLength = Math.max(maxLineLength, visibleLength);
|
|
81
|
-
process.stdout.write(`\r${line.padEnd(maxLineLength, " ")}`);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const now = Date.now();
|
|
86
|
-
if (forcePlainLog || now - lastPlainLog >= 2000) {
|
|
87
|
-
console.log(line);
|
|
88
|
-
lastPlainLog = now;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Start the periodic spinner redraw for TTY output.
|
|
94
|
-
* @returns {void}
|
|
95
|
-
*/
|
|
96
|
-
function startSpinnerLoop() {
|
|
97
|
-
if (!isTTY || spinnerTimer) return;
|
|
98
|
-
spinnerTimer = setInterval(() => {
|
|
99
|
-
if (completed >= total && active === 0) return;
|
|
100
|
-
render();
|
|
101
|
-
}, 120);
|
|
102
|
-
spinnerTimer.unref?.();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Stop the periodic spinner redraw loop.
|
|
107
|
-
* @returns {void}
|
|
108
|
-
*/
|
|
109
|
-
function stopSpinnerLoop() {
|
|
110
|
-
if (!spinnerTimer) return;
|
|
111
|
-
clearInterval(spinnerTimer);
|
|
112
|
-
spinnerTimer = null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
render(true);
|
|
116
|
-
startSpinnerLoop();
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
/**
|
|
120
|
-
* Call when a file run starts (increments active count).
|
|
121
|
-
* @returns {void}
|
|
122
|
-
*/
|
|
123
|
-
onStart() {
|
|
124
|
-
active++;
|
|
125
|
-
render();
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Call when a file run completes.
|
|
130
|
-
* @param {boolean} failedRun - Whether the run exited with a non-zero code.
|
|
131
|
-
* @returns {void}
|
|
132
|
-
*/
|
|
133
|
-
onComplete(failedRun) {
|
|
134
|
-
active = Math.max(0, active - 1);
|
|
135
|
-
completed++;
|
|
136
|
-
if (failedRun) failed++;
|
|
137
|
-
render(true);
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Stop the spinner and print the final progress line.
|
|
142
|
-
* @returns {void}
|
|
143
|
-
*/
|
|
144
|
-
finish() {
|
|
145
|
-
stopSpinnerLoop();
|
|
146
|
-
render(true);
|
|
147
|
-
if (isTTY) process.stdout.write("\n");
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* A no-op progress tracker used when quiet mode is disabled (progress is
|
|
154
|
-
* handled via `console.log` inline).
|
|
155
|
-
* @type {{ onStart: () => void, onComplete: (failedRun: boolean) => void, finish: () => void }}
|
|
156
|
-
*/
|
|
157
|
-
export const noopProgressTracker = {
|
|
158
|
-
/** @returns {void} */
|
|
159
|
-
onStart() {},
|
|
160
|
-
/** @param {boolean} ___failedRun @returns {void} */
|
|
161
|
-
onComplete(___failedRun) {},
|
|
162
|
-
/** @returns {void} */
|
|
163
|
-
finish() {}
|
|
164
|
-
};
|
package/src/core/report.mjs
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Result reporting and coverage summary utilities.
|
|
3
|
-
* @module vitest-runner/src/core/report
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from "node:fs/promises";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
import { stripAnsi, colourPct } from "../utils/ansi.mjs";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Print verbose output for files that failed during a quiet coverage run.
|
|
13
|
-
*
|
|
14
|
-
* @param {Array<{file: string, code: number, rawOutput: string}>} failedResults
|
|
15
|
-
* @returns {void}
|
|
16
|
-
* @example
|
|
17
|
-
* printQuietCoverageFailureDetails(failedResults);
|
|
18
|
-
*/
|
|
19
|
-
export function printQuietCoverageFailureDetails(failedResults) {
|
|
20
|
-
if (failedResults.length === 0) return;
|
|
21
|
-
|
|
22
|
-
console.log(`\n${"=".repeat(80)}`);
|
|
23
|
-
console.log(chalk.bold.red("✖ FAILED TEST FILES (VERBOSE OUTPUT)"));
|
|
24
|
-
console.log("=".repeat(80));
|
|
25
|
-
|
|
26
|
-
for (const r of failedResults) {
|
|
27
|
-
console.log(`\n${chalk.red("✖")} ${chalk.red(r.file)} ${chalk.dim(`(exit ${r.code})`)}`);
|
|
28
|
-
if (r.rawOutput?.trim()) {
|
|
29
|
-
console.log(r.rawOutput.trimEnd());
|
|
30
|
-
} else {
|
|
31
|
-
console.log(chalk.dim("(no child output captured)"));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
console.log(`\n${"=".repeat(80)}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Print the captured output from a quiet `--mergeReports` step.
|
|
40
|
-
*
|
|
41
|
-
* On success prints only the coverage block (from "% Coverage report from v8").
|
|
42
|
-
* On failure prints the full raw output to stderr.
|
|
43
|
-
*
|
|
44
|
-
* @param {number} exitCode - The merge process exit code.
|
|
45
|
-
* @param {string} output - Raw stdout + stderr from the merge process.
|
|
46
|
-
* @returns {void}
|
|
47
|
-
*/
|
|
48
|
-
export function printMergeOutput(exitCode, output) {
|
|
49
|
-
if (exitCode === 0) {
|
|
50
|
-
const marker = "% Coverage report from v8";
|
|
51
|
-
const rawLines = output.split("\n");
|
|
52
|
-
const markerLineIndex = rawLines.findIndex((line) => stripAnsi(line).includes(marker));
|
|
53
|
-
|
|
54
|
-
if (markerLineIndex >= 0) {
|
|
55
|
-
let endLineIndex = rawLines.length;
|
|
56
|
-
for (let i = markerLineIndex + 1; i < rawLines.length; i++) {
|
|
57
|
-
const line = stripAnsi(rawLines[i]).trimStart();
|
|
58
|
-
if (line.startsWith("stderr |") || line.startsWith("stdout |")) {
|
|
59
|
-
endLineIndex = i;
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const coverageBody = rawLines
|
|
65
|
-
.slice(markerLineIndex + 1, endLineIndex)
|
|
66
|
-
.join("\n")
|
|
67
|
-
.trimEnd();
|
|
68
|
-
if (coverageBody) {
|
|
69
|
-
const fullBlock = rawLines.slice(markerLineIndex, endLineIndex).join("\n").trimEnd();
|
|
70
|
-
console.log(`\n${fullBlock}\n`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
} else {
|
|
74
|
-
const trimmed = output.trimEnd();
|
|
75
|
-
if (trimmed) console.error(trimmed);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Compute a coverage-summary-style object from a raw V8/Istanbul `coverage-final.json`.
|
|
81
|
-
*
|
|
82
|
-
* @param {Record<string, object>} finalData - Parsed `coverage-final.json` contents.
|
|
83
|
-
* @returns {{ total: object, [filePath: string]: object }} Istanbul coverage-summary format.
|
|
84
|
-
*/
|
|
85
|
-
export function computeSummaryFromFinal(finalData) {
|
|
86
|
-
const pct = (covered, total) => (total === 0 ? 100 : parseFloat(((covered / total) * 100).toFixed(2)));
|
|
87
|
-
const summary = {
|
|
88
|
-
total: {
|
|
89
|
-
statements: { total: 0, covered: 0, pct: 0 },
|
|
90
|
-
branches: { total: 0, covered: 0, pct: 0 },
|
|
91
|
-
functions: { total: 0, covered: 0, pct: 0 },
|
|
92
|
-
lines: { total: 0, covered: 0, pct: 0 }
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
for (const [filePath, data] of Object.entries(finalData)) {
|
|
97
|
-
const sKeys = Object.keys(data.s ?? {});
|
|
98
|
-
const stmtTotal = sKeys.length;
|
|
99
|
-
const stmtCovered = sKeys.filter((k) => data.s[k] > 0).length;
|
|
100
|
-
|
|
101
|
-
const fKeys = Object.keys(data.f ?? {});
|
|
102
|
-
const fnTotal = fKeys.length;
|
|
103
|
-
const fnCovered = fKeys.filter((k) => data.f[k] > 0).length;
|
|
104
|
-
|
|
105
|
-
let branchTotal = 0,
|
|
106
|
-
branchCovered = 0;
|
|
107
|
-
for (const counts of Object.values(data.b ?? {})) {
|
|
108
|
-
branchTotal += counts.length;
|
|
109
|
-
branchCovered += counts.filter((c) => c > 0).length;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const coveredLines = new Set();
|
|
113
|
-
const totalLines = new Set();
|
|
114
|
-
for (const [sid, loc] of Object.entries(data.statementMap ?? {})) {
|
|
115
|
-
const line = loc?.start?.line;
|
|
116
|
-
if (line == null) continue;
|
|
117
|
-
totalLines.add(line);
|
|
118
|
-
if ((data.s ?? {})[sid] > 0) coveredLines.add(line);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const fileStats = {
|
|
122
|
-
statements: { total: stmtTotal, covered: stmtCovered, pct: pct(stmtCovered, stmtTotal) },
|
|
123
|
-
branches: { total: branchTotal, covered: branchCovered, pct: pct(branchCovered, branchTotal) },
|
|
124
|
-
functions: { total: fnTotal, covered: fnCovered, pct: pct(fnCovered, fnTotal) },
|
|
125
|
-
lines: { total: totalLines.size, covered: coveredLines.size, pct: pct(coveredLines.size, totalLines.size) }
|
|
126
|
-
};
|
|
127
|
-
summary[filePath] = fileStats;
|
|
128
|
-
|
|
129
|
-
for (const key of ["statements", "branches", "functions", "lines"]) {
|
|
130
|
-
summary.total[key].total += fileStats[key].total;
|
|
131
|
-
summary.total[key].covered += fileStats[key].covered;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
for (const key of ["statements", "branches", "functions", "lines"]) {
|
|
136
|
-
const { total, covered } = summary.total[key];
|
|
137
|
-
summary.total[key].pct = pct(covered, total);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return summary;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Read the coverage JSON produced after a `mergeReports` run and print a
|
|
145
|
-
* worst-offenders table plus overall-coverage totals.
|
|
146
|
-
*
|
|
147
|
-
* Tries `coverage-summary.json` first; falls back to computing from
|
|
148
|
-
* `coverage-final.json` if that is not present.
|
|
149
|
-
*
|
|
150
|
-
* @param {string} cwd - Project root (used to make absolute file paths relative).
|
|
151
|
-
* @param {string[]} extraCoverageArgs - Passthrough `--coverage.*` args (checked for `reportsDirectory`).
|
|
152
|
-
* @param {number} [worstCount=10] - Number of worst-coverage files to show (0 = skip table).
|
|
153
|
-
* @param {{silent?: boolean}} [options] - Output controls.
|
|
154
|
-
* @returns {Promise<{
|
|
155
|
-
* coverageDir: string,
|
|
156
|
-
* total: object,
|
|
157
|
-
* worstFiles: Array<{file: string, lines: number, stmts: number, fns: number, branches: number}>,
|
|
158
|
-
* worstFilesShown: number,
|
|
159
|
-
* worstFilesTotal: number,
|
|
160
|
-
* summary: object
|
|
161
|
-
* }|null>}
|
|
162
|
-
*/
|
|
163
|
-
export async function printCoverageSummary(cwd, extraCoverageArgs, worstCount = 10, options = {}) {
|
|
164
|
-
const { silent = false } = options;
|
|
165
|
-
let coverageDir = path.resolve(cwd, "coverage");
|
|
166
|
-
const repoDirArg = extraCoverageArgs.find((a) => a.startsWith("--coverage.reportsDirectory="));
|
|
167
|
-
if (repoDirArg) {
|
|
168
|
-
const raw = repoDirArg.split("=").slice(1).join("=");
|
|
169
|
-
coverageDir = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let summary;
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const content = await fs.readFile(path.join(coverageDir, "coverage-summary.json"), "utf8");
|
|
176
|
-
summary = JSON.parse(content);
|
|
177
|
-
} catch {
|
|
178
|
-
try {
|
|
179
|
-
const content = await fs.readFile(path.join(coverageDir, "coverage-final.json"), "utf8");
|
|
180
|
-
summary = computeSummaryFromFinal(JSON.parse(content));
|
|
181
|
-
} catch {
|
|
182
|
-
if (!silent) console.log(chalk.dim(" (no coverage JSON found — skipping summary)"));
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const { total, ...fileSummaries } = summary;
|
|
188
|
-
|
|
189
|
-
if (worstCount > 0) {
|
|
190
|
-
const fileRows = Object.entries(fileSummaries)
|
|
191
|
-
.map(([absFile, data]) => ({
|
|
192
|
-
file: path.relative(cwd, absFile),
|
|
193
|
-
lines: data.lines?.pct ?? 0,
|
|
194
|
-
stmts: data.statements?.pct ?? 0,
|
|
195
|
-
fns: data.functions?.pct ?? 0,
|
|
196
|
-
branches: data.branches?.pct ?? 0
|
|
197
|
-
}))
|
|
198
|
-
.filter((r) => Math.min(r.lines, r.stmts, r.fns, r.branches) < 100)
|
|
199
|
-
.sort((a, b) => Math.min(a.lines, a.stmts, a.fns, a.branches) - Math.min(b.lines, b.stmts, b.fns, b.branches));
|
|
200
|
-
const rowsToShow = fileRows.slice(0, worstCount);
|
|
201
|
-
|
|
202
|
-
if (!silent) {
|
|
203
|
-
console.log("\n" + chalk.bold("📉 WORST COVERAGE FILES (lowest metric)"));
|
|
204
|
-
console.log("-".repeat(80));
|
|
205
|
-
|
|
206
|
-
rowsToShow.forEach(({ file, lines, stmts, fns, branches }) => {
|
|
207
|
-
const worst = Math.min(lines, stmts, fns, branches);
|
|
208
|
-
const extras = chalk.dim(
|
|
209
|
-
`lines ${lines.toFixed(0)}% | stmts ${stmts.toFixed(0)}% | fns ${fns.toFixed(0)}% | branches ${branches.toFixed(0)}%`
|
|
210
|
-
);
|
|
211
|
-
console.log(` ${colourPct(chalk, worst)}% ${chalk.dim(file)} ${extras}`);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (fileRows.length > worstCount) {
|
|
215
|
-
console.log(chalk.dim(` ... and ${fileRows.length - worstCount} more files`));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const tl = total.lines?.pct ?? 0;
|
|
220
|
-
const ts = total.statements?.pct ?? 0;
|
|
221
|
-
const tf = total.functions?.pct ?? 0;
|
|
222
|
-
const tb = total.branches?.pct ?? 0;
|
|
223
|
-
if (!silent) {
|
|
224
|
-
console.log(
|
|
225
|
-
`\n ${chalk.bold("Coverage")} ${colourPct(chalk, tl)}% lines ${chalk.dim("|")} ${colourPct(chalk, ts)}% statements ${chalk.dim("|")} ${colourPct(chalk, tf)}% functions ${chalk.dim("|")} ${colourPct(chalk, tb)}% branches`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
229
|
-
coverageDir,
|
|
230
|
-
total,
|
|
231
|
-
worstFiles: rowsToShow,
|
|
232
|
-
worstFilesShown: rowsToShow.length,
|
|
233
|
-
worstFilesTotal: fileRows.length,
|
|
234
|
-
summary
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const tl = total.lines?.pct ?? 0;
|
|
239
|
-
const ts = total.statements?.pct ?? 0;
|
|
240
|
-
const tf = total.functions?.pct ?? 0;
|
|
241
|
-
const tb = total.branches?.pct ?? 0;
|
|
242
|
-
if (!silent) {
|
|
243
|
-
console.log(
|
|
244
|
-
`\n ${chalk.bold("Coverage")} ${colourPct(chalk, tl)}% lines ${chalk.dim("|")} ${colourPct(chalk, ts)}% statements ${chalk.dim("|")} ${colourPct(chalk, tf)}% functions ${chalk.dim("|")} ${colourPct(chalk, tb)}% branches`
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
return {
|
|
248
|
-
coverageDir,
|
|
249
|
-
total,
|
|
250
|
-
worstFiles: [],
|
|
251
|
-
worstFilesShown: 0,
|
|
252
|
-
worstFilesTotal: 0,
|
|
253
|
-
summary
|
|
254
|
-
};
|
|
255
|
-
}
|