@cldmv/vitest-runner 1.0.3 → 1.1.0
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 +26 -4
- package/bin/vitest-runner.mjs +29 -20
- package/package.json +1 -1
- package/src/cli/args.mjs +24 -0
- package/src/cli/help.mjs +5 -1
- package/src/core/report.mjs +58 -19
- package/src/runner.mjs +244 -51
- package/types/src/cli/args.d.mts +16 -0
- package/types/src/core/report.d.mts +25 -2
- package/types/src/runner.d.mts +18 -2
package/README.md
CHANGED
|
@@ -53,7 +53,11 @@ vitest-runner [OPTIONS] [PATTERNS...]
|
|
|
53
53
|
| `--solo-pattern <pat>` | Run files matching this path substring solo (one at a time) before the worker pool; repeatable |
|
|
54
54
|
| `--no-error-details` | Hide inline error blocks — show only counts in the summary |
|
|
55
55
|
| `--coverage-quiet` | Implies `--coverage`; suppress per-file output and show only a live progress bar and final summaries |
|
|
56
|
-
| `--log-file <path>` | Write a clean (ANSI-stripped) copy of all output to this file
|
|
56
|
+
| `--log-file <path>` | Write a clean (ANSI-stripped) copy of all output to this file. Defaults to `coverage/coverage-run.log` when `--coverage-quiet` is active |
|
|
57
|
+
| `--suppress-file-output` | Suppress per-file runner output blocks in any mode |
|
|
58
|
+
| `--suppress-passing-files` | Hide the `PASSED TEST FILES` section in the final summary |
|
|
59
|
+
| `--no-top-summary` | Hide `TOP MEMORY USERS` and `TOP DURATION` summary sections |
|
|
60
|
+
| `--json` | Print a JSON run report (no runner text output) |
|
|
57
61
|
| `--help`, `-h` | Print this help and exit |
|
|
58
62
|
|
|
59
63
|
### Test patterns
|
|
@@ -128,6 +132,12 @@ VITEST_HEAP_MB=8192 vitest-runner --workers 2 src/tests/heavy
|
|
|
128
132
|
|
|
129
133
|
# Suppress error details in the summary
|
|
130
134
|
vitest-runner --no-error-details
|
|
135
|
+
|
|
136
|
+
# JSON output for automation
|
|
137
|
+
vitest-runner --json
|
|
138
|
+
|
|
139
|
+
# JSON output without top summary arrays
|
|
140
|
+
vitest-runner --json --no-top-summary
|
|
131
141
|
```
|
|
132
142
|
|
|
133
143
|
---
|
|
@@ -141,9 +151,9 @@ import { run } from 'vitest-runner';
|
|
|
141
151
|
const { run } = await require('vitest-runner');
|
|
142
152
|
```
|
|
143
153
|
|
|
144
|
-
### `run(options)` → `Promise<number>`
|
|
154
|
+
### `run(options)` → `Promise<number | object>`
|
|
145
155
|
|
|
146
|
-
Runs the test suite and resolves with an exit code (`0` = all passed, `1` = any failure).
|
|
156
|
+
Runs the test suite and resolves with an exit code (`0` = all passed, `1` = any failure) by default. When `json: true` is passed, it returns a structured JSON report object (including `exitCode`) instead of printing runner text output.
|
|
147
157
|
|
|
148
158
|
```js
|
|
149
159
|
import { run } from 'vitest-runner';
|
|
@@ -168,6 +178,10 @@ process.exit(code);
|
|
|
168
178
|
| `vitestArgs` | `string[]` | `[]` | Extra CLI args forwarded verbatim to every vitest invocation |
|
|
169
179
|
| `showErrorDetails` | `boolean` | `true` | Print inline error blocks under each failed file in the summary |
|
|
170
180
|
| `coverageQuiet` | `boolean` | `false` | Suppress per-file output; show only the progress bar and final summaries |
|
|
181
|
+
| `suppressFileOutput` | `boolean` | `false` | Suppress per-file runner output blocks in all modes |
|
|
182
|
+
| `suppressPassingFiles` | `boolean` | `false` | Hide passed-file rows in the final summary |
|
|
183
|
+
| `topSummary` | `boolean` | `true` | Show or hide top memory/duration summary sections (and JSON arrays) |
|
|
184
|
+
| `json` | `boolean` | `false` | Return a JSON report object instead of printing text output |
|
|
171
185
|
| `workers` | `number` | `4` | Maximum parallel worker slots (overrides `VITEST_WORKERS`) |
|
|
172
186
|
| `worstCoverageCount` | `number` | `10` | Rows in the worst-coverage table after a coverage run (`0` disables it) |
|
|
173
187
|
| `maxOldSpaceMb` | `number` | `undefined` | Global `--max-old-space-size` ceiling in MB (overrides `VITEST_HEAP_MB`) |
|
|
@@ -208,6 +222,14 @@ await run({
|
|
|
208
222
|
coverageQuiet: true,
|
|
209
223
|
});
|
|
210
224
|
|
|
225
|
+
// Machine-readable output (no text logs)
|
|
226
|
+
const report = await run({
|
|
227
|
+
testDir: 'src/tests',
|
|
228
|
+
json: true,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
console.log(report.exitCode);
|
|
232
|
+
|
|
211
233
|
// Give heap-heavy files a larger ceiling while keeping the global limit lower
|
|
212
234
|
await run({
|
|
213
235
|
testDir: 'src/tests',
|
|
@@ -235,7 +257,7 @@ This avoids the OOM crash that occurs when a single vitest process holds coverag
|
|
|
235
257
|
|
|
236
258
|
`--coverage-quiet` / `coverageQuiet: true` suppresses all per-file output and renders a live progress bar instead. On completion it prints the coverage table and any failures verbosely. When running in this mode, output is also mirrored to `coverage/coverage-run.log` (CLI only) with ANSI colour codes stripped so the file is human-readable in any editor.
|
|
237
259
|
|
|
238
|
-
The log file path can be overridden with `--log-file <path>`. Passing `--log-file`
|
|
260
|
+
The log file path can be overridden with `--log-file <path>`. Passing `--log-file` by itself only enables log mirroring (it does not enable coverage mode).
|
|
239
261
|
|
|
240
262
|
---
|
|
241
263
|
|
package/bin/vitest-runner.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @fileoverview CLI entry point for the vitest-runner binary.
|
|
4
4
|
* @module vitest-runner/bin/vitest-runner
|
|
5
5
|
*
|
|
6
|
-
* Mirrors its own output to a log file when --coverage-quiet is set, then
|
|
6
|
+
* Mirrors its own output to a log file when --coverage-quiet or --log-file is set, then
|
|
7
7
|
* delegates all logic to `src/runner.mjs` via the programmatic `run()` API.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -23,7 +23,7 @@ if (args.help) {
|
|
|
23
23
|
|
|
24
24
|
// Mirror all output (excluding progress bar lines) to a log file
|
|
25
25
|
// when running in --coverage-quiet mode (or when --log-file is set).
|
|
26
|
-
if (args.coverageQuiet || args.logFile) {
|
|
26
|
+
if ((args.coverageQuiet || args.logFile) && !args.json) {
|
|
27
27
|
const cwd = process.cwd();
|
|
28
28
|
const resolvedLogFile = args.logFile
|
|
29
29
|
? path.isAbsolute(args.logFile)
|
|
@@ -62,25 +62,34 @@ if (args.coverageQuiet || args.logFile) {
|
|
|
62
62
|
const cwd = process.cwd();
|
|
63
63
|
|
|
64
64
|
const vitestArgs = [...args.vitestPassthroughArgs];
|
|
65
|
-
if (
|
|
65
|
+
if (args.coverageQuiet && !vitestArgs.some((a) => a === "--coverage" || a.startsWith("--coverage."))) {
|
|
66
66
|
vitestArgs.unshift("--coverage");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.error("Fatal error:", err);
|
|
85
|
-
process.exit(1);
|
|
69
|
+
try {
|
|
70
|
+
const runResult = await run({
|
|
71
|
+
cwd,
|
|
72
|
+
testPatterns: args.testPatterns,
|
|
73
|
+
testListFile: args.testListFile,
|
|
74
|
+
testFilePattern: args.testFilePattern,
|
|
75
|
+
vitestArgs,
|
|
76
|
+
showErrorDetails: args.showErrorDetails,
|
|
77
|
+
coverageQuiet: args.coverageQuiet,
|
|
78
|
+
suppressFileOutput: args.suppressFileOutput,
|
|
79
|
+
suppressPassingFiles: args.suppressPassingFiles,
|
|
80
|
+
topSummary: args.topSummary,
|
|
81
|
+
json: args.json,
|
|
82
|
+
...(args.workers !== undefined && { workers: args.workers }),
|
|
83
|
+
...(args.soloPatterns.length > 0 && { earlyRunPatterns: args.soloPatterns })
|
|
86
84
|
});
|
|
85
|
+
|
|
86
|
+
if (args.json) {
|
|
87
|
+
process.stdout.write(`${JSON.stringify(runResult, null, 2)}\n`);
|
|
88
|
+
process.exit(typeof runResult === "object" && runResult !== null && "exitCode" in runResult ? runResult.exitCode : 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
process.exit(typeof runResult === "number" ? runResult : 1);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error("Fatal error:", err);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
package/package.json
CHANGED
package/src/cli/args.mjs
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* @property {boolean} showErrorDetails - `false` when `--no-error-details` was passed.
|
|
10
10
|
* @property {boolean} coverageQuiet - Whether `--coverage-quiet` was passed.
|
|
11
11
|
* @property {string|undefined} logFile - Path for the coverage run log (`--log-file`); defaults to `coverage/coverage-run.log`.
|
|
12
|
+
* @property {boolean} suppressFileOutput - Suppress per-file runner output blocks (`--suppress-file-output`).
|
|
13
|
+
* @property {boolean} suppressPassingFiles - Suppress the passed-files section in the final summary (`--suppress-passing-files`).
|
|
14
|
+
* @property {boolean} topSummary - Show top-summary sections for memory and duration (`true` by default, disabled by `--no-top-summary`).
|
|
15
|
+
* @property {boolean} json - Emit a JSON run report instead of text output (`--json`).
|
|
12
16
|
* @property {boolean} help - Whether `--help` / `-h` was passed.
|
|
13
17
|
* @property {number|undefined} workers - Worker count from `--workers <n>`, or undefined.
|
|
14
18
|
* @property {string[]} soloPatterns - Path substrings from `--solo-pattern <pattern>` (repeatable).
|
|
@@ -23,6 +27,10 @@ const RUNNER_FLAGS = new Set([
|
|
|
23
27
|
"--no-error-details",
|
|
24
28
|
"--coverage-quiet",
|
|
25
29
|
"--log-file",
|
|
30
|
+
"--suppress-file-output",
|
|
31
|
+
"--suppress-passing-files",
|
|
32
|
+
"--no-top-summary",
|
|
33
|
+
"--json",
|
|
26
34
|
"--workers",
|
|
27
35
|
"--solo-pattern",
|
|
28
36
|
"--file-pattern",
|
|
@@ -51,6 +59,10 @@ export function parseArguments(args) {
|
|
|
51
59
|
let showErrorDetails = true;
|
|
52
60
|
let coverageQuiet = false;
|
|
53
61
|
let logFile;
|
|
62
|
+
let suppressFileOutput = false;
|
|
63
|
+
let suppressPassingFiles = false;
|
|
64
|
+
let topSummary = true;
|
|
65
|
+
let json = false;
|
|
54
66
|
let workers;
|
|
55
67
|
let help = false;
|
|
56
68
|
let testFilePattern;
|
|
@@ -69,6 +81,14 @@ export function parseArguments(args) {
|
|
|
69
81
|
logFile = args[++i];
|
|
70
82
|
} else if (arg.startsWith("--log-file=")) {
|
|
71
83
|
logFile = arg.slice("--log-file=".length);
|
|
84
|
+
} else if (arg === "--suppress-file-output") {
|
|
85
|
+
suppressFileOutput = true;
|
|
86
|
+
} else if (arg === "--suppress-passing-files") {
|
|
87
|
+
suppressPassingFiles = true;
|
|
88
|
+
} else if (arg === "--no-top-summary") {
|
|
89
|
+
topSummary = false;
|
|
90
|
+
} else if (arg === "--json") {
|
|
91
|
+
json = true;
|
|
72
92
|
} else if (arg === "--workers") {
|
|
73
93
|
workers = parseInt(args[++i], 10);
|
|
74
94
|
} else if (arg.startsWith("--workers=")) {
|
|
@@ -100,6 +120,10 @@ export function parseArguments(args) {
|
|
|
100
120
|
showErrorDetails,
|
|
101
121
|
coverageQuiet,
|
|
102
122
|
logFile,
|
|
123
|
+
suppressFileOutput,
|
|
124
|
+
suppressPassingFiles,
|
|
125
|
+
topSummary,
|
|
126
|
+
json,
|
|
103
127
|
help,
|
|
104
128
|
workers,
|
|
105
129
|
soloPatterns,
|
package/src/cli/help.mjs
CHANGED
|
@@ -26,7 +26,11 @@ ${chalk.bold("SPECIAL FLAGS:")}
|
|
|
26
26
|
--solo-pattern <pat> Run files matching this path substring solo first (repeatable)
|
|
27
27
|
--no-error-details Hide detailed error output (show only counts)
|
|
28
28
|
--coverage-quiet Implies --coverage; show progress bar + final summaries only
|
|
29
|
-
--log-file <path> Path for
|
|
29
|
+
--log-file <path> Path for mirrored runner output (default: coverage/coverage-run.log with --coverage-quiet)
|
|
30
|
+
--suppress-file-output Suppress per-file runner output blocks
|
|
31
|
+
--suppress-passing-files Hide the PASSED TEST FILES section in the final summary
|
|
32
|
+
--no-top-summary Hide TOP MEMORY USERS and TOP DURATION sections
|
|
33
|
+
--json Emit JSON summary output instead of text logs
|
|
30
34
|
--help, -h Show this help message
|
|
31
35
|
|
|
32
36
|
${chalk.bold("TEST PATTERNS:")}
|
package/src/core/report.mjs
CHANGED
|
@@ -150,9 +150,18 @@ export function computeSummaryFromFinal(finalData) {
|
|
|
150
150
|
* @param {string} cwd - Project root (used to make absolute file paths relative).
|
|
151
151
|
* @param {string[]} extraCoverageArgs - Passthrough `--coverage.*` args (checked for `reportsDirectory`).
|
|
152
152
|
* @param {number} [worstCount=10] - Number of worst-coverage files to show (0 = skip table).
|
|
153
|
-
* @
|
|
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>}
|
|
154
162
|
*/
|
|
155
|
-
export async function printCoverageSummary(cwd, extraCoverageArgs, worstCount = 10) {
|
|
163
|
+
export async function printCoverageSummary(cwd, extraCoverageArgs, worstCount = 10, options = {}) {
|
|
164
|
+
const { silent = false } = options;
|
|
156
165
|
let coverageDir = path.resolve(cwd, "coverage");
|
|
157
166
|
const repoDirArg = extraCoverageArgs.find((a) => a.startsWith("--coverage.reportsDirectory="));
|
|
158
167
|
if (repoDirArg) {
|
|
@@ -170,8 +179,8 @@ export async function printCoverageSummary(cwd, extraCoverageArgs, worstCount =
|
|
|
170
179
|
const content = await fs.readFile(path.join(coverageDir, "coverage-final.json"), "utf8");
|
|
171
180
|
summary = computeSummaryFromFinal(JSON.parse(content));
|
|
172
181
|
} catch {
|
|
173
|
-
console.log(chalk.dim(" (no coverage JSON found — skipping summary)"));
|
|
174
|
-
return;
|
|
182
|
+
if (!silent) console.log(chalk.dim(" (no coverage JSON found — skipping summary)"));
|
|
183
|
+
return null;
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
|
|
@@ -188,29 +197,59 @@ export async function printCoverageSummary(cwd, extraCoverageArgs, worstCount =
|
|
|
188
197
|
}))
|
|
189
198
|
.filter((r) => Math.min(r.lines, r.stmts, r.fns, r.branches) < 100)
|
|
190
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);
|
|
191
201
|
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
if (!silent) {
|
|
203
|
+
console.log("\n" + chalk.bold("📉 WORST COVERAGE FILES (lowest metric)"));
|
|
204
|
+
console.log("-".repeat(80));
|
|
194
205
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
203
218
|
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
);
|
|
206
227
|
}
|
|
228
|
+
return {
|
|
229
|
+
coverageDir,
|
|
230
|
+
total,
|
|
231
|
+
worstFiles: rowsToShow,
|
|
232
|
+
worstFilesShown: rowsToShow.length,
|
|
233
|
+
worstFilesTotal: fileRows.length,
|
|
234
|
+
summary
|
|
235
|
+
};
|
|
207
236
|
}
|
|
208
237
|
|
|
209
238
|
const tl = total.lines?.pct ?? 0;
|
|
210
239
|
const ts = total.statements?.pct ?? 0;
|
|
211
240
|
const tf = total.functions?.pct ?? 0;
|
|
212
241
|
const tb = total.branches?.pct ?? 0;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
};
|
|
216
255
|
}
|
package/src/runner.mjs
CHANGED
|
@@ -55,6 +55,10 @@ import { colourPct } from "./utils/ansi.mjs";
|
|
|
55
55
|
* @property {string[]} [vitestArgs=[]] - Extra CLI args forwarded verbatim to every vitest invocation.
|
|
56
56
|
* @property {boolean} [showErrorDetails=true] - Print inline error blocks under each failed file.
|
|
57
57
|
* @property {boolean} [coverageQuiet=false] - Suppress per-file output; show only progress bar + summaries.
|
|
58
|
+
* @property {boolean} [suppressFileOutput=false] - Suppress per-file runner output blocks in all modes.
|
|
59
|
+
* @property {boolean} [suppressPassingFiles=false] - Hide passed-file rows in the final summary.
|
|
60
|
+
* @property {boolean} [topSummary=true] - Show top memory and duration summary sections.
|
|
61
|
+
* @property {boolean} [json=false] - Return a JSON run report instead of printing text output.
|
|
58
62
|
* @property {number} [workers=4] - Maximum number of parallel worker slots.
|
|
59
63
|
* @property {number} [worstCoverageCount=10] - Rows in the worst-coverage table (0 = disable).
|
|
60
64
|
* @property {number} [maxOldSpaceMb] - Global `--max-old-space-size` ceiling; per-file overrides may raise it.
|
|
@@ -96,7 +100,7 @@ function getHeapForFile(filePath, globalMaxMb, overrides) {
|
|
|
96
100
|
* pool for the non-solo phase) and return an exit code.
|
|
97
101
|
*
|
|
98
102
|
* @param {RunOptions} opts
|
|
99
|
-
* @returns {Promise<number>} `0`
|
|
103
|
+
* @returns {Promise<number|object>} `0`/`1` by default; JSON report object when `opts.json` is true.
|
|
100
104
|
*/
|
|
101
105
|
export async function run(opts) {
|
|
102
106
|
const {
|
|
@@ -108,6 +112,10 @@ export async function run(opts) {
|
|
|
108
112
|
vitestArgs: rawVitestArgs = [],
|
|
109
113
|
showErrorDetails = true,
|
|
110
114
|
coverageQuiet = false,
|
|
115
|
+
suppressFileOutput = false,
|
|
116
|
+
suppressPassingFiles = false,
|
|
117
|
+
topSummary = true,
|
|
118
|
+
json = false,
|
|
111
119
|
workers = parseInt(process.env.VITEST_WORKERS ?? "4", 10),
|
|
112
120
|
worstCoverageCount = 10,
|
|
113
121
|
earlyRunPatterns = [],
|
|
@@ -119,6 +127,7 @@ export async function run(opts) {
|
|
|
119
127
|
} = opts;
|
|
120
128
|
|
|
121
129
|
const maxOldSpaceMb = opts.maxOldSpaceMb ?? (process.env.VITEST_HEAP_MB ? parseInt(process.env.VITEST_HEAP_MB, 10) : undefined);
|
|
130
|
+
const emitTextOutput = !json;
|
|
122
131
|
|
|
123
132
|
// Resolve vitest binary and config
|
|
124
133
|
const vitestBin = resolveBin(cwd, "vitest", "vitest");
|
|
@@ -149,9 +158,28 @@ export async function run(opts) {
|
|
|
149
158
|
const allTestFiles = await discoverVitestFiles({ cwd, testDir, testPatterns, testListFile, testFilePattern, earlyRunPatterns });
|
|
150
159
|
|
|
151
160
|
if (allTestFiles.length === 0) {
|
|
152
|
-
|
|
153
|
-
testPatterns.length > 0 ? `❌ No Vitest test files found matching: ${testPatterns.join(", ")}` : "❌ No Vitest test files found"
|
|
154
|
-
);
|
|
161
|
+
const noTestsMessage =
|
|
162
|
+
testPatterns.length > 0 ? `❌ No Vitest test files found matching: ${testPatterns.join(", ")}` : "❌ No Vitest test files found";
|
|
163
|
+
if (emitTextOutput) console.log(noTestsMessage);
|
|
164
|
+
if (json) {
|
|
165
|
+
return {
|
|
166
|
+
exitCode: 1,
|
|
167
|
+
mode: "coverage",
|
|
168
|
+
message: noTestsMessage,
|
|
169
|
+
options: {
|
|
170
|
+
cwd,
|
|
171
|
+
testDir,
|
|
172
|
+
testPatterns,
|
|
173
|
+
testListFile,
|
|
174
|
+
workers,
|
|
175
|
+
coverageQuiet,
|
|
176
|
+
suppressFileOutput,
|
|
177
|
+
suppressPassingFiles,
|
|
178
|
+
topSummary,
|
|
179
|
+
vitestArgs
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
155
183
|
return 1;
|
|
156
184
|
}
|
|
157
185
|
|
|
@@ -161,15 +189,17 @@ export async function run(opts) {
|
|
|
161
189
|
|
|
162
190
|
const soloFiles = allTestFiles.filter((f) => earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
|
|
163
191
|
const parallelFiles = allTestFiles.filter((f) => !earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
|
|
192
|
+
const suppressPerFileCoverageOutput = coverageQuiet || suppressFileOutput || json;
|
|
193
|
+
const showCoverageProgress = coverageQuiet && !json;
|
|
164
194
|
|
|
165
|
-
if (!coverageQuiet) {
|
|
195
|
+
if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
|
|
166
196
|
console.log(`\n🧪 Running ${allTestFiles.length} test files for coverage (blob + merge mode)`);
|
|
167
197
|
console.log(`⚙️ Workers: ${workers} (${soloFiles.length} solo first, then parallel)`);
|
|
168
198
|
if (maxOldSpaceMb) console.log(`🧠 Heap limit: ${maxOldSpaceMb} MB`);
|
|
169
199
|
console.log("");
|
|
170
200
|
}
|
|
171
201
|
|
|
172
|
-
const progress =
|
|
202
|
+
const progress = showCoverageProgress ? createCoverageProgressTracker(allTestFiles.length) : noopProgressTracker;
|
|
173
203
|
const coverageResults = [];
|
|
174
204
|
let blobIndex = 0;
|
|
175
205
|
|
|
@@ -182,7 +212,7 @@ export async function run(opts) {
|
|
|
182
212
|
const blobPath = path.join(blobsDir, `run-${blobIndex}.blob`);
|
|
183
213
|
blobIndex++;
|
|
184
214
|
|
|
185
|
-
if (!
|
|
215
|
+
if (!suppressPerFileCoverageOutput && emitTextOutput) {
|
|
186
216
|
console.log(`\n${"=".repeat(80)}`);
|
|
187
217
|
console.log(`▶️ ${filePath}`);
|
|
188
218
|
console.log("=".repeat(80));
|
|
@@ -204,13 +234,13 @@ export async function run(opts) {
|
|
|
204
234
|
...spawnBase,
|
|
205
235
|
maxOldSpaceMb: getHeapForFile(filePath, maxOldSpaceMb, perFileHeapOverrides),
|
|
206
236
|
vitestArgs: blobArgs,
|
|
207
|
-
streamOutput: !
|
|
237
|
+
streamOutput: !suppressPerFileCoverageOutput
|
|
208
238
|
});
|
|
209
239
|
|
|
210
240
|
coverageResults.push(result);
|
|
211
241
|
progress.onComplete(result.code !== 0);
|
|
212
242
|
|
|
213
|
-
if (!
|
|
243
|
+
if (!suppressPerFileCoverageOutput && emitTextOutput) {
|
|
214
244
|
const durationSec = (result.duration / 1000).toFixed(2);
|
|
215
245
|
if (result.code === 0) {
|
|
216
246
|
const heapInfo = result.heapMb ? ` | ${result.heapMb} MB heap` : "";
|
|
@@ -223,7 +253,9 @@ export async function run(opts) {
|
|
|
223
253
|
|
|
224
254
|
// Phase 1: solo files — one at a time
|
|
225
255
|
for (const filePath of soloFiles) {
|
|
226
|
-
await runCoverageFile(filePath).catch((err) =>
|
|
256
|
+
await runCoverageFile(filePath).catch((err) => {
|
|
257
|
+
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
258
|
+
});
|
|
227
259
|
}
|
|
228
260
|
|
|
229
261
|
// Phase 2: parallel files with worker pool
|
|
@@ -234,7 +266,9 @@ export async function run(opts) {
|
|
|
234
266
|
while (coverageFileIndex < parallelFiles.length && coverageActivePromises.size < workers) {
|
|
235
267
|
const filePath = parallelFiles[coverageFileIndex++];
|
|
236
268
|
const promise = runCoverageFile(filePath)
|
|
237
|
-
.catch((err) =>
|
|
269
|
+
.catch((err) => {
|
|
270
|
+
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
271
|
+
})
|
|
238
272
|
.finally(() => coverageActivePromises.delete(promise));
|
|
239
273
|
coverageActivePromises.add(promise);
|
|
240
274
|
}
|
|
@@ -246,11 +280,48 @@ export async function run(opts) {
|
|
|
246
280
|
|
|
247
281
|
const blobFiles = (await fs.readdir(blobsDir).catch(() => [])).filter((f) => f.endsWith(".blob"));
|
|
248
282
|
if (blobFiles.length === 0) {
|
|
249
|
-
|
|
283
|
+
const message = "❌ No coverage blobs were generated — coverage report cannot be produced";
|
|
284
|
+
if (emitTextOutput) console.error(message);
|
|
285
|
+
if (json) {
|
|
286
|
+
return {
|
|
287
|
+
exitCode: 1,
|
|
288
|
+
mode: "coverage",
|
|
289
|
+
message,
|
|
290
|
+
options: {
|
|
291
|
+
cwd,
|
|
292
|
+
testDir,
|
|
293
|
+
testPatterns,
|
|
294
|
+
testListFile,
|
|
295
|
+
workers,
|
|
296
|
+
coverageQuiet,
|
|
297
|
+
suppressFileOutput,
|
|
298
|
+
suppressPassingFiles,
|
|
299
|
+
topSummary,
|
|
300
|
+
showErrorDetails,
|
|
301
|
+
worstCoverageCount,
|
|
302
|
+
vitestArgs
|
|
303
|
+
},
|
|
304
|
+
totals: {
|
|
305
|
+
testFiles: allTestFiles.length,
|
|
306
|
+
failedFiles: coverageResults.filter((r) => r.code !== 0).length,
|
|
307
|
+
passedFiles: coverageResults.filter((r) => r.code === 0).length
|
|
308
|
+
},
|
|
309
|
+
results: {
|
|
310
|
+
all: coverageResults,
|
|
311
|
+
failed: coverageResults.filter((r) => r.code !== 0)
|
|
312
|
+
},
|
|
313
|
+
merge: {
|
|
314
|
+
blobFiles: 0,
|
|
315
|
+
exitCode: 1,
|
|
316
|
+
output: ""
|
|
317
|
+
},
|
|
318
|
+
coverageSummary: null
|
|
319
|
+
};
|
|
320
|
+
}
|
|
250
321
|
return 1;
|
|
251
322
|
}
|
|
252
323
|
|
|
253
|
-
if (!coverageQuiet) {
|
|
324
|
+
if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
|
|
254
325
|
console.log(`\n${"=".repeat(80)}`);
|
|
255
326
|
console.log(`📊 Merging ${blobFiles.length} coverage blobs into final report...`);
|
|
256
327
|
console.log("=".repeat(80));
|
|
@@ -260,14 +331,14 @@ export async function run(opts) {
|
|
|
260
331
|
...spawnBase,
|
|
261
332
|
maxOldSpaceMb,
|
|
262
333
|
extraCoverageArgs,
|
|
263
|
-
quietOutput: coverageQuiet
|
|
334
|
+
quietOutput: coverageQuiet || json
|
|
264
335
|
});
|
|
265
336
|
|
|
266
|
-
if (coverageQuiet) {
|
|
337
|
+
if (coverageQuiet && emitTextOutput) {
|
|
267
338
|
printMergeOutput(mergeExitCode, mergeOutput);
|
|
268
339
|
}
|
|
269
340
|
|
|
270
|
-
await printCoverageSummary(cwd, extraCoverageArgs, worstCoverageCount);
|
|
341
|
+
const coverageSummary = await printCoverageSummary(cwd, extraCoverageArgs, worstCoverageCount, { silent: json });
|
|
271
342
|
|
|
272
343
|
// Clean up blobs and temp dirs
|
|
273
344
|
await Promise.all([
|
|
@@ -276,18 +347,81 @@ export async function run(opts) {
|
|
|
276
347
|
]);
|
|
277
348
|
|
|
278
349
|
const coverageFailed = coverageResults.filter((r) => r.code !== 0);
|
|
279
|
-
if (coverageQuiet) printQuietCoverageFailureDetails(coverageFailed);
|
|
350
|
+
if (coverageQuiet && emitTextOutput) printQuietCoverageFailureDetails(coverageFailed);
|
|
351
|
+
|
|
352
|
+
const exitCode = coverageFailed.length > 0 ? 1 : mergeExitCode;
|
|
353
|
+
if (json) {
|
|
354
|
+
const coverageWithHeap = coverageResults.filter((r) => r.heapMb !== null);
|
|
355
|
+
return {
|
|
356
|
+
exitCode,
|
|
357
|
+
mode: "coverage",
|
|
358
|
+
options: {
|
|
359
|
+
cwd,
|
|
360
|
+
testDir,
|
|
361
|
+
testPatterns,
|
|
362
|
+
testListFile,
|
|
363
|
+
workers,
|
|
364
|
+
coverageQuiet,
|
|
365
|
+
suppressFileOutput,
|
|
366
|
+
suppressPassingFiles,
|
|
367
|
+
topSummary,
|
|
368
|
+
showErrorDetails,
|
|
369
|
+
worstCoverageCount,
|
|
370
|
+
vitestArgs
|
|
371
|
+
},
|
|
372
|
+
totals: {
|
|
373
|
+
testFiles: allTestFiles.length,
|
|
374
|
+
failedFiles: coverageFailed.length,
|
|
375
|
+
passedFiles: coverageResults.length - coverageFailed.length
|
|
376
|
+
},
|
|
377
|
+
results: {
|
|
378
|
+
all: coverageResults,
|
|
379
|
+
failed: coverageFailed
|
|
380
|
+
},
|
|
381
|
+
merge: {
|
|
382
|
+
blobFiles: blobFiles.length,
|
|
383
|
+
exitCode: mergeExitCode,
|
|
384
|
+
output: mergeOutput
|
|
385
|
+
},
|
|
386
|
+
coverageSummary,
|
|
387
|
+
...(topSummary
|
|
388
|
+
? {
|
|
389
|
+
topMemoryUsers: [...coverageWithHeap].sort((a, b) => b.heapMb - a.heapMb).slice(0, 10),
|
|
390
|
+
topDuration: [...coverageResults].sort((a, b) => b.duration - a.duration).slice(0, 10)
|
|
391
|
+
}
|
|
392
|
+
: {})
|
|
393
|
+
};
|
|
394
|
+
}
|
|
280
395
|
|
|
281
|
-
return
|
|
396
|
+
return exitCode;
|
|
282
397
|
}
|
|
283
398
|
|
|
284
399
|
// ─── STANDARD (NON-COVERAGE) MODE ────────────────────────────────────────────
|
|
285
400
|
const testFiles = await discoverVitestFiles({ cwd, testDir, testPatterns, testListFile, testFilePattern, earlyRunPatterns });
|
|
286
401
|
|
|
287
402
|
if (testFiles.length === 0) {
|
|
288
|
-
|
|
289
|
-
testPatterns.length > 0 ? `❌ No Vitest test files found matching: ${testPatterns.join(", ")}` : "❌ No Vitest test files found"
|
|
290
|
-
);
|
|
403
|
+
const noTestsMessage =
|
|
404
|
+
testPatterns.length > 0 ? `❌ No Vitest test files found matching: ${testPatterns.join(", ")}` : "❌ No Vitest test files found";
|
|
405
|
+
if (emitTextOutput) console.log(noTestsMessage);
|
|
406
|
+
if (json) {
|
|
407
|
+
return {
|
|
408
|
+
exitCode: 1,
|
|
409
|
+
mode: "standard",
|
|
410
|
+
message: noTestsMessage,
|
|
411
|
+
options: {
|
|
412
|
+
cwd,
|
|
413
|
+
testDir,
|
|
414
|
+
testPatterns,
|
|
415
|
+
testListFile,
|
|
416
|
+
workers,
|
|
417
|
+
coverageQuiet,
|
|
418
|
+
suppressFileOutput,
|
|
419
|
+
suppressPassingFiles,
|
|
420
|
+
topSummary,
|
|
421
|
+
vitestArgs
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
291
425
|
return 1;
|
|
292
426
|
}
|
|
293
427
|
|
|
@@ -297,15 +431,17 @@ export async function run(opts) {
|
|
|
297
431
|
const scriptStartTime = Date.now();
|
|
298
432
|
const scriptStartTimeFormatted = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
299
433
|
|
|
300
|
-
if (testPatterns.length > 0) {
|
|
434
|
+
if (emitTextOutput && !suppressFileOutput && testPatterns.length > 0) {
|
|
301
435
|
console.log(`\n🧪 Running ${testFiles.length} test files matching: ${testPatterns.join(", ")}`);
|
|
302
|
-
} else {
|
|
436
|
+
} else if (emitTextOutput && !suppressFileOutput) {
|
|
303
437
|
console.log(`\n🧪 Running ${testFiles.length} test files (${soloFiles.length} solo first, then parallel)`);
|
|
304
438
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
439
|
+
if (emitTextOutput && !suppressFileOutput) {
|
|
440
|
+
console.log(`⚙️ Workers: ${workers}`);
|
|
441
|
+
if (maxOldSpaceMb) console.log(`🧠 Heap limit: ${maxOldSpaceMb} MB`);
|
|
442
|
+
if (vitestArgs.length > 0) console.log(`🔧 Vitest args: ${vitestArgs.join(" ")}`);
|
|
443
|
+
console.log("");
|
|
444
|
+
}
|
|
309
445
|
|
|
310
446
|
const results = [...(_testResultsOverride ?? [])];
|
|
311
447
|
|
|
@@ -315,22 +451,27 @@ export async function run(opts) {
|
|
|
315
451
|
* @returns {Promise<import('./core/spawn.mjs').SingleFileResult>}
|
|
316
452
|
*/
|
|
317
453
|
const runTestFile = async (filePath) => {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
454
|
+
if (!suppressFileOutput && emitTextOutput) {
|
|
455
|
+
console.log(`\n${"=".repeat(80)}`);
|
|
456
|
+
console.log(`▶️ ${filePath}`);
|
|
457
|
+
console.log("=".repeat(80));
|
|
458
|
+
}
|
|
321
459
|
|
|
322
460
|
const result = await runSingleFile(filePath, {
|
|
323
461
|
...spawnBase,
|
|
324
462
|
maxOldSpaceMb: getHeapForFile(filePath, maxOldSpaceMb, perFileHeapOverrides),
|
|
325
|
-
vitestArgs
|
|
463
|
+
vitestArgs,
|
|
464
|
+
streamOutput: !suppressFileOutput && !json
|
|
326
465
|
});
|
|
327
466
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
467
|
+
if (!suppressFileOutput && emitTextOutput) {
|
|
468
|
+
const durationSec = (result.duration / 1000).toFixed(2);
|
|
469
|
+
if (result.code === 0) {
|
|
470
|
+
const heapInfo = result.heapMb ? ` | ${result.heapMb} MB heap` : "";
|
|
471
|
+
console.log(`\n✅ PASSED (${durationSec}s${heapInfo})\n`);
|
|
472
|
+
} else {
|
|
473
|
+
console.log(`\n❌ FAILED (exit code ${result.code}, ${durationSec}s)\n`);
|
|
474
|
+
}
|
|
334
475
|
}
|
|
335
476
|
|
|
336
477
|
return result;
|
|
@@ -340,7 +481,7 @@ export async function run(opts) {
|
|
|
340
481
|
// Phase 1: solo files
|
|
341
482
|
for (const filePath of soloFiles) {
|
|
342
483
|
const result = await runTestFile(filePath).catch((err) => {
|
|
343
|
-
console.error(`Error running ${filePath}:`, err);
|
|
484
|
+
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
344
485
|
return null;
|
|
345
486
|
});
|
|
346
487
|
if (result) results.push(result);
|
|
@@ -355,7 +496,9 @@ export async function run(opts) {
|
|
|
355
496
|
const filePath = parallelFiles[index++];
|
|
356
497
|
const promise = runTestFile(filePath)
|
|
357
498
|
.then((result) => results.push(result))
|
|
358
|
-
.catch((err) =>
|
|
499
|
+
.catch((err) => {
|
|
500
|
+
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
501
|
+
})
|
|
359
502
|
.finally(() => activePromises.delete(promise));
|
|
360
503
|
activePromises.add(promise);
|
|
361
504
|
}
|
|
@@ -373,12 +516,69 @@ export async function run(opts) {
|
|
|
373
516
|
const totalDuration = results.reduce((s, r) => s + r.duration, 0);
|
|
374
517
|
const failedFiles = results.filter((r) => r.code !== 0);
|
|
375
518
|
const passedFiles = results.filter((r) => r.code === 0);
|
|
519
|
+
const exitCode = failedFiles.length > 0 ? 1 : 0;
|
|
520
|
+
const scriptMemory = process.memoryUsage();
|
|
521
|
+
const scriptHeapMb = Math.round(scriptMemory.heapUsed / 1024 / 1024);
|
|
522
|
+
const scriptRssMb = Math.round(scriptMemory.rss / 1024 / 1024);
|
|
523
|
+
const withHeap = results.filter((r) => r.heapMb !== null);
|
|
524
|
+
const actualDurationSec = ((Date.now() - scriptStartTime) / 1000).toFixed(2);
|
|
525
|
+
const testsDurationSec = (totalDuration / 1000).toFixed(2);
|
|
526
|
+
|
|
527
|
+
if (json) {
|
|
528
|
+
return {
|
|
529
|
+
exitCode,
|
|
530
|
+
mode: "standard",
|
|
531
|
+
options: {
|
|
532
|
+
cwd,
|
|
533
|
+
testDir,
|
|
534
|
+
testPatterns,
|
|
535
|
+
testListFile,
|
|
536
|
+
workers,
|
|
537
|
+
coverageQuiet,
|
|
538
|
+
suppressFileOutput,
|
|
539
|
+
suppressPassingFiles,
|
|
540
|
+
topSummary,
|
|
541
|
+
showErrorDetails,
|
|
542
|
+
worstCoverageCount,
|
|
543
|
+
vitestArgs
|
|
544
|
+
},
|
|
545
|
+
totals: {
|
|
546
|
+
testFilesPass: totalTestFilesPass,
|
|
547
|
+
testFilesFail: totalTestFilesFail,
|
|
548
|
+
testsPass: totalTestsPass,
|
|
549
|
+
testsFail: totalTestsFail,
|
|
550
|
+
testsSkip: totalTestsSkip,
|
|
551
|
+
totalTests: totalTestsPass + totalTestsFail + totalTestsSkip
|
|
552
|
+
},
|
|
553
|
+
timing: {
|
|
554
|
+
startAt: scriptStartTimeFormatted,
|
|
555
|
+
wallClockSeconds: Number(actualDurationSec),
|
|
556
|
+
testsSeconds: Number(testsDurationSec)
|
|
557
|
+
},
|
|
558
|
+
heap: {
|
|
559
|
+
scriptHeapMb,
|
|
560
|
+
scriptRssMb,
|
|
561
|
+
maxHeapMb: withHeap.length > 0 ? Math.max(...withHeap.map((r) => r.heapMb)) : null,
|
|
562
|
+
avgHeapMb: withHeap.length > 0 ? Number((withHeap.reduce((sum, r) => sum + r.heapMb, 0) / withHeap.length).toFixed(0)) : null
|
|
563
|
+
},
|
|
564
|
+
results: {
|
|
565
|
+
all: results,
|
|
566
|
+
passed: passedFiles,
|
|
567
|
+
failed: failedFiles
|
|
568
|
+
},
|
|
569
|
+
...(topSummary
|
|
570
|
+
? {
|
|
571
|
+
topMemoryUsers: [...withHeap].sort((a, b) => b.heapMb - a.heapMb).slice(0, 10),
|
|
572
|
+
topDuration: [...results].sort((a, b) => b.duration - a.duration).slice(0, 10)
|
|
573
|
+
}
|
|
574
|
+
: {})
|
|
575
|
+
};
|
|
576
|
+
}
|
|
376
577
|
|
|
377
578
|
console.log("\n" + "=".repeat(80));
|
|
378
579
|
|
|
379
580
|
// Top memory users
|
|
380
|
-
|
|
381
|
-
if (withHeap.length > 0) {
|
|
581
|
+
if (topSummary && withHeap.length > 0) {
|
|
382
582
|
console.log("\n" + chalk.bold("🧠 TOP MEMORY USERS"));
|
|
383
583
|
console.log("-".repeat(80));
|
|
384
584
|
[...withHeap]
|
|
@@ -390,7 +590,7 @@ export async function run(opts) {
|
|
|
390
590
|
}
|
|
391
591
|
|
|
392
592
|
// Top duration
|
|
393
|
-
if (results.length > 0) {
|
|
593
|
+
if (topSummary && results.length > 0) {
|
|
394
594
|
console.log("\n" + chalk.bold("⏱️ TOP DURATION"));
|
|
395
595
|
console.log("-".repeat(80));
|
|
396
596
|
[...results]
|
|
@@ -403,7 +603,7 @@ export async function run(opts) {
|
|
|
403
603
|
}
|
|
404
604
|
|
|
405
605
|
// Passed files
|
|
406
|
-
if (passedFiles.length > 0) {
|
|
606
|
+
if (passedFiles.length > 0 && !suppressPassingFiles) {
|
|
407
607
|
console.log("\n" + "=".repeat(80));
|
|
408
608
|
console.log(chalk.bold.green("✓ PASSED TEST FILES"));
|
|
409
609
|
console.log("=".repeat(80));
|
|
@@ -468,13 +668,8 @@ export async function run(opts) {
|
|
|
468
668
|
console.log(` ${chalk.bold("Tests")} ${testsParts.join(` ${chalk.dim("|")} `)} ${chalk.dim(`(${totalTests})`)}`);
|
|
469
669
|
console.log(` ${chalk.bold("Start at")} ${scriptStartTimeFormatted}`);
|
|
470
670
|
|
|
471
|
-
const actualDurationSec = ((Date.now() - scriptStartTime) / 1000).toFixed(2);
|
|
472
|
-
const testsDurationSec = (totalDuration / 1000).toFixed(2);
|
|
473
671
|
console.log(` ${chalk.bold("Duration")} ${actualDurationSec}s ${chalk.dim(`(tests ${testsDurationSec}s)`)}`);
|
|
474
672
|
|
|
475
|
-
const scriptMemory = process.memoryUsage();
|
|
476
|
-
const scriptHeapMb = Math.round(scriptMemory.heapUsed / 1024 / 1024);
|
|
477
|
-
const scriptRssMb = Math.round(scriptMemory.rss / 1024 / 1024);
|
|
478
673
|
if (withHeap.length > 0) {
|
|
479
674
|
const maxHeap = Math.max(...withHeap.map((r) => r.heapMb));
|
|
480
675
|
const avgHeap = (withHeap.reduce((s, r) => s + r.heapMb, 0) / withHeap.length).toFixed(0);
|
|
@@ -483,9 +678,7 @@ export async function run(opts) {
|
|
|
483
678
|
console.log(` ${chalk.bold("Heap")} script ${scriptHeapMb} MB (RSS ${scriptRssMb} MB)`);
|
|
484
679
|
}
|
|
485
680
|
|
|
486
|
-
if (failedFiles.length > 0)
|
|
487
|
-
return 1;
|
|
488
|
-
}
|
|
681
|
+
if (failedFiles.length > 0) return 1;
|
|
489
682
|
|
|
490
683
|
console.log(`\n✅ All ${passedFiles.length} test files passed\n`);
|
|
491
684
|
return 0;
|
package/types/src/cli/args.d.mts
CHANGED
|
@@ -29,6 +29,22 @@ export type ParsedArgs = {
|
|
|
29
29
|
* - Path for the coverage run log (`--log-file`); defaults to `coverage/coverage-run.log`.
|
|
30
30
|
*/
|
|
31
31
|
logFile: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* - Suppress per-file runner output blocks (`--suppress-file-output`).
|
|
34
|
+
*/
|
|
35
|
+
suppressFileOutput: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* - Suppress the passed-files section in the final summary (`--suppress-passing-files`).
|
|
38
|
+
*/
|
|
39
|
+
suppressPassingFiles: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* - Show top-summary sections for memory and duration (`true` by default, disabled by `--no-top-summary`).
|
|
42
|
+
*/
|
|
43
|
+
topSummary: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* - Emit a JSON run report instead of text output (`--json`).
|
|
46
|
+
*/
|
|
47
|
+
json: boolean;
|
|
32
48
|
/**
|
|
33
49
|
* - Whether `--help` / `-h` was passed.
|
|
34
50
|
*/
|
|
@@ -42,6 +42,29 @@ export function computeSummaryFromFinal(finalData: Record<string, object>): {
|
|
|
42
42
|
* @param {string} cwd - Project root (used to make absolute file paths relative).
|
|
43
43
|
* @param {string[]} extraCoverageArgs - Passthrough `--coverage.*` args (checked for `reportsDirectory`).
|
|
44
44
|
* @param {number} [worstCount=10] - Number of worst-coverage files to show (0 = skip table).
|
|
45
|
-
* @
|
|
45
|
+
* @param {{silent?: boolean}} [options] - Output controls.
|
|
46
|
+
* @returns {Promise<{
|
|
47
|
+
* coverageDir: string,
|
|
48
|
+
* total: object,
|
|
49
|
+
* worstFiles: Array<{file: string, lines: number, stmts: number, fns: number, branches: number}>,
|
|
50
|
+
* worstFilesShown: number,
|
|
51
|
+
* worstFilesTotal: number,
|
|
52
|
+
* summary: object
|
|
53
|
+
* }|null>}
|
|
46
54
|
*/
|
|
47
|
-
export function printCoverageSummary(cwd: string, extraCoverageArgs: string[], worstCount?: number
|
|
55
|
+
export function printCoverageSummary(cwd: string, extraCoverageArgs: string[], worstCount?: number, options?: {
|
|
56
|
+
silent?: boolean;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
coverageDir: string;
|
|
59
|
+
total: object;
|
|
60
|
+
worstFiles: Array<{
|
|
61
|
+
file: string;
|
|
62
|
+
lines: number;
|
|
63
|
+
stmts: number;
|
|
64
|
+
fns: number;
|
|
65
|
+
branches: number;
|
|
66
|
+
}>;
|
|
67
|
+
worstFilesShown: number;
|
|
68
|
+
worstFilesTotal: number;
|
|
69
|
+
summary: object;
|
|
70
|
+
} | null>;
|
package/types/src/runner.d.mts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* pool for the non-solo phase) and return an exit code.
|
|
4
4
|
*
|
|
5
5
|
* @param {RunOptions} opts
|
|
6
|
-
* @returns {Promise<number>} `0`
|
|
6
|
+
* @returns {Promise<number|object>} `0`/`1` by default; JSON report object when `opts.json` is true.
|
|
7
7
|
*/
|
|
8
|
-
export function run(opts: RunOptions): Promise<number>;
|
|
8
|
+
export function run(opts: RunOptions): Promise<number | object>;
|
|
9
9
|
export { formatDuration } from "./utils/duration.mjs";
|
|
10
10
|
export { buildNodeOptions } from "./utils/env.mjs";
|
|
11
11
|
export type PerFileHeapOverride = {
|
|
@@ -55,6 +55,22 @@ export type RunOptions = {
|
|
|
55
55
|
* - Suppress per-file output; show only progress bar + summaries.
|
|
56
56
|
*/
|
|
57
57
|
coverageQuiet?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* - Suppress per-file runner output blocks in all modes.
|
|
60
|
+
*/
|
|
61
|
+
suppressFileOutput?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* - Hide passed-file rows in the final summary.
|
|
64
|
+
*/
|
|
65
|
+
suppressPassingFiles?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* - Show top memory and duration summary sections.
|
|
68
|
+
*/
|
|
69
|
+
topSummary?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* - Return a JSON run report instead of printing text output.
|
|
72
|
+
*/
|
|
73
|
+
json?: boolean;
|
|
58
74
|
/**
|
|
59
75
|
* - Maximum number of parallel worker slots.
|
|
60
76
|
*/
|