@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/runner.mjs
DELETED
|
@@ -1,708 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Main sequential Vitest runner orchestration.
|
|
3
|
-
* @module vitest-runner/src/runner
|
|
4
|
-
*
|
|
5
|
-
* @description
|
|
6
|
-
* Runs each Vitest test file in its own child process to avoid OOM issues in
|
|
7
|
-
* large test suites, collects results, and produces a Vitest-style summary.
|
|
8
|
-
*
|
|
9
|
-
* Supports coverage mode (blob-per-file + mergeReports) and a quiet progress-bar
|
|
10
|
-
* variant so CI pipelines can keep output concise. All standard Vitest CLI flags
|
|
11
|
-
* are forwarded to child processes unchanged.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* // Run every test file found under src/tests/ (cwd defaults to process.cwd())
|
|
15
|
-
* import { run } from 'vitest-runner';
|
|
16
|
-
* const code = await run({ testDir: 'src/tests' });
|
|
17
|
-
* process.exit(code);
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // Run a subset with custom heap limit
|
|
21
|
-
* const code = await run({
|
|
22
|
-
* testDir: 'src/tests',
|
|
23
|
-
* testPatterns: ['src/tests/config'],
|
|
24
|
-
* maxOldSpaceMb: 4096,
|
|
25
|
-
* });
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import fs from "node:fs/promises";
|
|
29
|
-
import path from "node:path";
|
|
30
|
-
import chalk from "chalk";
|
|
31
|
-
|
|
32
|
-
import { resolveBin, resolveVitestConfig } from "./utils/resolve.mjs";
|
|
33
|
-
import { discoverVitestFiles } from "./core/discover.mjs";
|
|
34
|
-
import { runSingleFile, runMergeReports } from "./core/spawn.mjs";
|
|
35
|
-
import { deduplicateErrors } from "./core/parse.mjs";
|
|
36
|
-
import { createCoverageProgressTracker, noopProgressTracker } from "./core/progress.mjs";
|
|
37
|
-
import { printQuietCoverageFailureDetails, printMergeOutput, printCoverageSummary } from "./core/report.mjs";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {Object} PerFileHeapOverride
|
|
41
|
-
* @property {string} pattern - Substring matched against the normalised file path.
|
|
42
|
-
* @property {number} heapMb - Minimum heap ceiling in MB for matching files.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @typedef {Object} RunOptions
|
|
47
|
-
* @property {string} [cwd=process.cwd()] - Absolute project root directory (defaults to `process.cwd()`).
|
|
48
|
-
* @property {string} [testDir] - Directory to scan for test files (relative or absolute; defaults to `cwd`).
|
|
49
|
-
* @property {string} [vitestConfig] - Explicit vitest config path; auto-detected from `cwd` when omitted.
|
|
50
|
-
* @property {string[]} [testPatterns=[]] - File / folder patterns to filter (empty = all files in `testDir`).
|
|
51
|
-
* @property {string} [testListFile] - Path to a JSON array of test file paths; when set, scanning is skipped.
|
|
52
|
-
* @property {RegExp} [testFilePattern] - Regex matched against file names when scanning (default: `*.test.vitest.{js,mjs,cjs}`).
|
|
53
|
-
* @property {string[]} [vitestArgs=[]] - Extra CLI args forwarded verbatim to every vitest invocation.
|
|
54
|
-
* @property {boolean} [showErrorDetails=true] - Print inline error blocks under each failed file.
|
|
55
|
-
* @property {boolean} [coverageQuiet=false] - Suppress per-file output; show only progress bar + summaries.
|
|
56
|
-
* @property {boolean} [suppressFileOutput=false] - Suppress per-file runner output blocks in all modes.
|
|
57
|
-
* @property {boolean} [suppressPassingFiles=false] - Hide passed-file rows in the final summary.
|
|
58
|
-
* @property {boolean} [topSummary=true] - Show top memory and duration summary sections.
|
|
59
|
-
* @property {boolean} [json=false] - Return a JSON run report instead of printing text output.
|
|
60
|
-
* @property {number} [workers=4] - Maximum number of parallel worker slots.
|
|
61
|
-
* @property {number} [worstCoverageCount=10] - Rows in the worst-coverage table (0 = disable).
|
|
62
|
-
* @property {string} [blobsDir] - Directory for per-file coverage blobs (default `<cwd>/.vitest-coverage-blobs`). Relative paths resolve against `cwd`. Always cleared at the start of a coverage run.
|
|
63
|
-
* @property {boolean} [mergeReports=true] - When `true`, blobs are merged via `vitest --mergeReports`, the coverage summary is printed, and `blobsDir` is deleted at the end. When `false`, the run stops after producing blobs: no merge, no summary, and `blobsDir` is left populated for an external merge step.
|
|
64
|
-
* @property {number} [maxOldSpaceMb] - Global `--max-old-space-size` ceiling; per-file overrides may raise it.
|
|
65
|
-
* @property {string[]} [earlyRunPatterns=[]] - Path substrings ā matching files run solo before the worker pool.
|
|
66
|
-
* @property {PerFileHeapOverride[]} [perFileHeapOverrides=[]] - Per-file minimum heap overrides.
|
|
67
|
-
* @property {string[]} [conditions=[]] - Additional `--conditions` Node flags forwarded to children.
|
|
68
|
-
* @property {string} [nodeEnv='development'] - Value for `NODE_ENV` in child processes.
|
|
69
|
-
* @property {object[] | null} [_testResultsOverride=null] - @internal Inject pre-built results to bypass discovery and spawn (for testing final-report render paths).
|
|
70
|
-
*/
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Return the effective heap ceiling (MB) for a given test file.
|
|
74
|
-
*
|
|
75
|
-
* Takes the maximum of the global limit and the first matching per-file override.
|
|
76
|
-
* Returns `undefined` when neither source provides a value.
|
|
77
|
-
*
|
|
78
|
-
* @param {string} filePath - Test file path.
|
|
79
|
-
* @param {number|undefined} globalMaxMb - Global ceiling from options.
|
|
80
|
-
* @param {PerFileHeapOverride[]} overrides - Per-file override table.
|
|
81
|
-
* @returns {number|undefined}
|
|
82
|
-
*/
|
|
83
|
-
function getHeapForFile(filePath, globalMaxMb, overrides) {
|
|
84
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
85
|
-
let perFileMb;
|
|
86
|
-
for (const { pattern, heapMb } of overrides) {
|
|
87
|
-
if (normalized.includes(pattern)) {
|
|
88
|
-
perFileMb = heapMb;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (perFileMb === undefined && globalMaxMb === undefined) return undefined;
|
|
93
|
-
if (perFileMb === undefined) return globalMaxMb;
|
|
94
|
-
if (globalMaxMb === undefined) return perFileMb;
|
|
95
|
-
return Math.max(perFileMb, globalMaxMb);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Run all discovered Vitest test files sequentially (with a configurable worker
|
|
100
|
-
* pool for the non-solo phase) and return an exit code.
|
|
101
|
-
*
|
|
102
|
-
* @param {RunOptions} opts
|
|
103
|
-
* @returns {Promise<number|object>} `0`/`1` by default; JSON report object when `opts.json` is true.
|
|
104
|
-
*/
|
|
105
|
-
export async function run(opts) {
|
|
106
|
-
const {
|
|
107
|
-
cwd = process.cwd(),
|
|
108
|
-
testDir,
|
|
109
|
-
testPatterns = [],
|
|
110
|
-
testListFile,
|
|
111
|
-
testFilePattern,
|
|
112
|
-
vitestArgs: rawVitestArgs = [],
|
|
113
|
-
showErrorDetails = true,
|
|
114
|
-
coverageQuiet = false,
|
|
115
|
-
suppressFileOutput = false,
|
|
116
|
-
suppressPassingFiles = false,
|
|
117
|
-
topSummary = true,
|
|
118
|
-
json = false,
|
|
119
|
-
workers = parseInt(process.env.VITEST_WORKERS ?? "4", 10),
|
|
120
|
-
worstCoverageCount = 10,
|
|
121
|
-
blobsDir: blobsDirOpt,
|
|
122
|
-
mergeReports = true,
|
|
123
|
-
earlyRunPatterns = [],
|
|
124
|
-
perFileHeapOverrides = [],
|
|
125
|
-
conditions = [],
|
|
126
|
-
nodeEnv = "development",
|
|
127
|
-
/** @internal Inject pre-built results to bypass discovery and spawn (for testing final-report render paths). */
|
|
128
|
-
_testResultsOverride = null
|
|
129
|
-
} = opts;
|
|
130
|
-
|
|
131
|
-
const maxOldSpaceMb = opts.maxOldSpaceMb ?? (process.env.VITEST_HEAP_MB ? parseInt(process.env.VITEST_HEAP_MB, 10) : undefined);
|
|
132
|
-
const emitTextOutput = !json;
|
|
133
|
-
|
|
134
|
-
// Resolve vitest binary and config
|
|
135
|
-
const vitestBin = resolveBin(cwd, "vitest", "vitest");
|
|
136
|
-
const vitestConfig = await resolveVitestConfig(cwd, opts.vitestConfig);
|
|
137
|
-
|
|
138
|
-
/** Common spawn options shared across all child invocations. */
|
|
139
|
-
const spawnBase = { cwd, vitestBin, vitestConfig, conditions, nodeEnv };
|
|
140
|
-
|
|
141
|
-
const vitestArgs = [...rawVitestArgs];
|
|
142
|
-
|
|
143
|
-
// --coverage-quiet implies --coverage
|
|
144
|
-
if (coverageQuiet && !vitestArgs.some((a) => a === "--coverage" || a.startsWith("--coverage."))) {
|
|
145
|
-
vitestArgs.unshift("--coverage");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const hasCoverage = vitestArgs.some((a) => a === "--coverage" || a.startsWith("--coverage."));
|
|
149
|
-
|
|
150
|
-
// āāā COVERAGE MODE āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
151
|
-
if (hasCoverage) {
|
|
152
|
-
const blobsDir = path.resolve(cwd, blobsDirOpt ?? ".vitest-coverage-blobs");
|
|
153
|
-
// Temp coverage dirs live OUTSIDE blobsDir ā vitest --mergeReports errors on
|
|
154
|
-
// any non-blob entry found inside the blobs directory.
|
|
155
|
-
const coverageTmpBase = path.resolve(cwd, ".vitest-coverage-tmp");
|
|
156
|
-
|
|
157
|
-
await Promise.all([fs.rm(blobsDir, { recursive: true, force: true }), fs.rm(coverageTmpBase, { recursive: true, force: true })]);
|
|
158
|
-
await Promise.all([fs.mkdir(blobsDir, { recursive: true }), fs.mkdir(coverageTmpBase, { recursive: true })]);
|
|
159
|
-
|
|
160
|
-
const allTestFiles = await discoverVitestFiles({ cwd, testDir, testPatterns, testListFile, testFilePattern, earlyRunPatterns });
|
|
161
|
-
|
|
162
|
-
if (allTestFiles.length === 0) {
|
|
163
|
-
const noTestsMessage =
|
|
164
|
-
testPatterns.length > 0 ? `ā No Vitest test files found matching: ${testPatterns.join(", ")}` : "ā No Vitest test files found";
|
|
165
|
-
if (emitTextOutput) console.log(noTestsMessage);
|
|
166
|
-
if (json) {
|
|
167
|
-
return {
|
|
168
|
-
exitCode: 1,
|
|
169
|
-
mode: "coverage",
|
|
170
|
-
message: noTestsMessage,
|
|
171
|
-
options: {
|
|
172
|
-
cwd,
|
|
173
|
-
testDir,
|
|
174
|
-
testPatterns,
|
|
175
|
-
testListFile,
|
|
176
|
-
workers,
|
|
177
|
-
coverageQuiet,
|
|
178
|
-
suppressFileOutput,
|
|
179
|
-
suppressPassingFiles,
|
|
180
|
-
topSummary,
|
|
181
|
-
vitestArgs
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
return 1;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Separate --coverage / --coverage.* args (merge step only) from other passthroughs
|
|
189
|
-
const extraCoverageArgs = vitestArgs.filter((a) => a !== "--coverage" && a.startsWith("--coverage."));
|
|
190
|
-
const nonCoveragePassthrough = vitestArgs.filter((a) => a !== "--coverage" && !a.startsWith("--coverage."));
|
|
191
|
-
|
|
192
|
-
const soloFiles = allTestFiles.filter((f) => earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
|
|
193
|
-
const parallelFiles = allTestFiles.filter((f) => !earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
|
|
194
|
-
const suppressPerFileCoverageOutput = coverageQuiet || suppressFileOutput || json;
|
|
195
|
-
const showCoverageProgress = coverageQuiet && !json;
|
|
196
|
-
|
|
197
|
-
if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
|
|
198
|
-
console.log(`\nš§Ŗ Running ${allTestFiles.length} test files for coverage (blob + merge mode)`);
|
|
199
|
-
console.log(`āļø Workers: ${workers} (${soloFiles.length} solo first, then parallel)`);
|
|
200
|
-
if (maxOldSpaceMb) console.log(`š§ Heap limit: ${maxOldSpaceMb} MB`);
|
|
201
|
-
console.log("");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const progress = showCoverageProgress ? createCoverageProgressTracker(allTestFiles.length) : noopProgressTracker;
|
|
205
|
-
const coverageResults = [];
|
|
206
|
-
let blobIndex = 0;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Run one file with the blob reporter and push its result.
|
|
210
|
-
* @param {string} filePath
|
|
211
|
-
* @returns {Promise<void>}
|
|
212
|
-
*/
|
|
213
|
-
const runCoverageFile = async (filePath) => {
|
|
214
|
-
const blobPath = path.join(blobsDir, `run-${blobIndex}.blob`);
|
|
215
|
-
blobIndex++;
|
|
216
|
-
|
|
217
|
-
if (!suppressPerFileCoverageOutput && emitTextOutput) {
|
|
218
|
-
console.log(`\n${"=".repeat(80)}`);
|
|
219
|
-
console.log(`ā¶ļø ${filePath}`);
|
|
220
|
-
console.log("=".repeat(80));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
progress.onStart();
|
|
224
|
-
|
|
225
|
-
const tmpCoverageDir = path.join(coverageTmpBase, `run-${blobIndex}`);
|
|
226
|
-
const blobArgs = [
|
|
227
|
-
...nonCoveragePassthrough,
|
|
228
|
-
"--coverage",
|
|
229
|
-
`--coverage.reportsDirectory=${tmpCoverageDir}`,
|
|
230
|
-
"--reporter=default",
|
|
231
|
-
"--reporter=blob",
|
|
232
|
-
`--outputFile=${blobPath}`
|
|
233
|
-
];
|
|
234
|
-
|
|
235
|
-
const result = await runSingleFile(filePath, {
|
|
236
|
-
...spawnBase,
|
|
237
|
-
maxOldSpaceMb: getHeapForFile(filePath, maxOldSpaceMb, perFileHeapOverrides),
|
|
238
|
-
vitestArgs: blobArgs,
|
|
239
|
-
streamOutput: !suppressPerFileCoverageOutput
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
coverageResults.push(result);
|
|
243
|
-
progress.onComplete(result.code !== 0);
|
|
244
|
-
|
|
245
|
-
if (!suppressPerFileCoverageOutput && emitTextOutput) {
|
|
246
|
-
const durationSec = (result.duration / 1000).toFixed(2);
|
|
247
|
-
if (result.code === 0) {
|
|
248
|
-
const heapInfo = result.heapMb ? ` | ${result.heapMb} MB heap` : "";
|
|
249
|
-
console.log(`\nā
PASSED (${durationSec}s${heapInfo})\n`);
|
|
250
|
-
} else {
|
|
251
|
-
console.log(`\nā FAILED (exit code ${result.code}, ${durationSec}s)\n`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Phase 1: solo files ā one at a time
|
|
257
|
-
for (const filePath of soloFiles) {
|
|
258
|
-
await runCoverageFile(filePath).catch((err) => {
|
|
259
|
-
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Phase 2: parallel files with worker pool
|
|
264
|
-
let coverageFileIndex = 0;
|
|
265
|
-
const coverageActivePromises = new Set();
|
|
266
|
-
|
|
267
|
-
while (coverageFileIndex < parallelFiles.length || coverageActivePromises.size > 0) {
|
|
268
|
-
while (coverageFileIndex < parallelFiles.length && coverageActivePromises.size < workers) {
|
|
269
|
-
const filePath = parallelFiles[coverageFileIndex++];
|
|
270
|
-
const promise = runCoverageFile(filePath)
|
|
271
|
-
.catch((err) => {
|
|
272
|
-
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
273
|
-
})
|
|
274
|
-
.finally(() => coverageActivePromises.delete(promise));
|
|
275
|
-
coverageActivePromises.add(promise);
|
|
276
|
-
}
|
|
277
|
-
// c8 ignore next -- size is always >0 when the outer while body is reached
|
|
278
|
-
if (coverageActivePromises.size > 0) await Promise.race(coverageActivePromises);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
progress.finish();
|
|
282
|
-
|
|
283
|
-
const blobFiles = (await fs.readdir(blobsDir).catch(() => [])).filter((f) => f.endsWith(".blob"));
|
|
284
|
-
if (blobFiles.length === 0) {
|
|
285
|
-
const message = "ā No coverage blobs were generated ā coverage report cannot be produced";
|
|
286
|
-
if (emitTextOutput) console.error(message);
|
|
287
|
-
if (json) {
|
|
288
|
-
return {
|
|
289
|
-
exitCode: 1,
|
|
290
|
-
mode: "coverage",
|
|
291
|
-
message,
|
|
292
|
-
options: {
|
|
293
|
-
cwd,
|
|
294
|
-
testDir,
|
|
295
|
-
testPatterns,
|
|
296
|
-
testListFile,
|
|
297
|
-
workers,
|
|
298
|
-
coverageQuiet,
|
|
299
|
-
suppressFileOutput,
|
|
300
|
-
suppressPassingFiles,
|
|
301
|
-
topSummary,
|
|
302
|
-
showErrorDetails,
|
|
303
|
-
worstCoverageCount,
|
|
304
|
-
vitestArgs
|
|
305
|
-
},
|
|
306
|
-
totals: {
|
|
307
|
-
testFiles: allTestFiles.length,
|
|
308
|
-
failedFiles: coverageResults.filter((r) => r.code !== 0).length,
|
|
309
|
-
passedFiles: coverageResults.filter((r) => r.code === 0).length
|
|
310
|
-
},
|
|
311
|
-
results: {
|
|
312
|
-
all: coverageResults,
|
|
313
|
-
failed: coverageResults.filter((r) => r.code !== 0)
|
|
314
|
-
},
|
|
315
|
-
merge: {
|
|
316
|
-
blobFiles: 0,
|
|
317
|
-
exitCode: 1,
|
|
318
|
-
output: ""
|
|
319
|
-
},
|
|
320
|
-
coverageSummary: null
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
return 1;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// When mergeReports is false, stop after producing the blobs: skip the merge
|
|
327
|
-
// step and the coverage summary, and leave blobsDir intact so an external step
|
|
328
|
-
// can merge these blobs together with a separately-produced blob set.
|
|
329
|
-
let mergeExitCode = 0;
|
|
330
|
-
let mergeOutput = "";
|
|
331
|
-
let coverageSummary = null;
|
|
332
|
-
|
|
333
|
-
if (mergeReports) {
|
|
334
|
-
if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
|
|
335
|
-
console.log(`\n${"=".repeat(80)}`);
|
|
336
|
-
console.log(`š Merging ${blobFiles.length} coverage blobs into final report...`);
|
|
337
|
-
console.log("=".repeat(80));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
({ exitCode: mergeExitCode, output: mergeOutput } = await runMergeReports(blobsDir, {
|
|
341
|
-
...spawnBase,
|
|
342
|
-
maxOldSpaceMb,
|
|
343
|
-
extraCoverageArgs,
|
|
344
|
-
quietOutput: coverageQuiet || json
|
|
345
|
-
}));
|
|
346
|
-
|
|
347
|
-
if (coverageQuiet && emitTextOutput) {
|
|
348
|
-
printMergeOutput(mergeExitCode, mergeOutput);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
coverageSummary = await printCoverageSummary(cwd, extraCoverageArgs, worstCoverageCount, { silent: json });
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Clean up temp dirs always; only delete blobsDir when we consumed it via merge.
|
|
355
|
-
// With mergeReports:false the blobs must survive for the caller's external merge.
|
|
356
|
-
await Promise.all([
|
|
357
|
-
...(mergeReports ? [fs.rm(blobsDir, { recursive: true, force: true }).catch(() => {})] : []),
|
|
358
|
-
fs.rm(coverageTmpBase, { recursive: true, force: true }).catch(() => {})
|
|
359
|
-
]);
|
|
360
|
-
|
|
361
|
-
const coverageFailed = coverageResults.filter((r) => r.code !== 0);
|
|
362
|
-
if (coverageQuiet && emitTextOutput) printQuietCoverageFailureDetails(coverageFailed);
|
|
363
|
-
|
|
364
|
-
const exitCode = coverageFailed.length > 0 ? 1 : mergeExitCode;
|
|
365
|
-
if (json) {
|
|
366
|
-
const coverageWithHeap = coverageResults.filter((r) => r.heapMb !== null);
|
|
367
|
-
return {
|
|
368
|
-
exitCode,
|
|
369
|
-
mode: "coverage",
|
|
370
|
-
options: {
|
|
371
|
-
cwd,
|
|
372
|
-
testDir,
|
|
373
|
-
testPatterns,
|
|
374
|
-
testListFile,
|
|
375
|
-
workers,
|
|
376
|
-
coverageQuiet,
|
|
377
|
-
suppressFileOutput,
|
|
378
|
-
suppressPassingFiles,
|
|
379
|
-
topSummary,
|
|
380
|
-
showErrorDetails,
|
|
381
|
-
worstCoverageCount,
|
|
382
|
-
vitestArgs
|
|
383
|
-
},
|
|
384
|
-
totals: {
|
|
385
|
-
testFiles: allTestFiles.length,
|
|
386
|
-
failedFiles: coverageFailed.length,
|
|
387
|
-
passedFiles: coverageResults.length - coverageFailed.length
|
|
388
|
-
},
|
|
389
|
-
results: {
|
|
390
|
-
all: coverageResults,
|
|
391
|
-
failed: coverageFailed
|
|
392
|
-
},
|
|
393
|
-
merge: {
|
|
394
|
-
blobFiles: blobFiles.length,
|
|
395
|
-
exitCode: mergeExitCode,
|
|
396
|
-
output: mergeOutput
|
|
397
|
-
},
|
|
398
|
-
coverageSummary,
|
|
399
|
-
...(topSummary
|
|
400
|
-
? {
|
|
401
|
-
topMemoryUsers: [...coverageWithHeap].sort((a, b) => b.heapMb - a.heapMb).slice(0, 10),
|
|
402
|
-
topDuration: [...coverageResults].sort((a, b) => b.duration - a.duration).slice(0, 10)
|
|
403
|
-
}
|
|
404
|
-
: {})
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return exitCode;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// āāā STANDARD (NON-COVERAGE) MODE āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
412
|
-
const testFiles = await discoverVitestFiles({ cwd, testDir, testPatterns, testListFile, testFilePattern, earlyRunPatterns });
|
|
413
|
-
|
|
414
|
-
if (testFiles.length === 0) {
|
|
415
|
-
const noTestsMessage =
|
|
416
|
-
testPatterns.length > 0 ? `ā No Vitest test files found matching: ${testPatterns.join(", ")}` : "ā No Vitest test files found";
|
|
417
|
-
if (emitTextOutput) console.log(noTestsMessage);
|
|
418
|
-
if (json) {
|
|
419
|
-
return {
|
|
420
|
-
exitCode: 1,
|
|
421
|
-
mode: "standard",
|
|
422
|
-
message: noTestsMessage,
|
|
423
|
-
options: {
|
|
424
|
-
cwd,
|
|
425
|
-
testDir,
|
|
426
|
-
testPatterns,
|
|
427
|
-
testListFile,
|
|
428
|
-
workers,
|
|
429
|
-
coverageQuiet,
|
|
430
|
-
suppressFileOutput,
|
|
431
|
-
suppressPassingFiles,
|
|
432
|
-
topSummary,
|
|
433
|
-
vitestArgs
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
return 1;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const soloFiles = testFiles.filter((f) => earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
|
|
441
|
-
const parallelFiles = testFiles.filter((f) => !earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
|
|
442
|
-
|
|
443
|
-
const scriptStartTime = Date.now();
|
|
444
|
-
const scriptStartTimeFormatted = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
445
|
-
|
|
446
|
-
if (emitTextOutput && !suppressFileOutput && testPatterns.length > 0) {
|
|
447
|
-
console.log(`\nš§Ŗ Running ${testFiles.length} test files matching: ${testPatterns.join(", ")}`);
|
|
448
|
-
} else if (emitTextOutput && !suppressFileOutput) {
|
|
449
|
-
console.log(`\nš§Ŗ Running ${testFiles.length} test files (${soloFiles.length} solo first, then parallel)`);
|
|
450
|
-
}
|
|
451
|
-
if (emitTextOutput && !suppressFileOutput) {
|
|
452
|
-
console.log(`āļø Workers: ${workers}`);
|
|
453
|
-
if (maxOldSpaceMb) console.log(`š§ Heap limit: ${maxOldSpaceMb} MB`);
|
|
454
|
-
if (vitestArgs.length > 0) console.log(`š§ Vitest args: ${vitestArgs.join(" ")}`);
|
|
455
|
-
console.log("");
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const results = [...(_testResultsOverride ?? [])];
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Run one test file, log progress, and return the result.
|
|
462
|
-
* @param {string} filePath
|
|
463
|
-
* @returns {Promise<import('./core/spawn.mjs').SingleFileResult>}
|
|
464
|
-
*/
|
|
465
|
-
const runTestFile = async (filePath) => {
|
|
466
|
-
if (!suppressFileOutput && emitTextOutput) {
|
|
467
|
-
console.log(`\n${"=".repeat(80)}`);
|
|
468
|
-
console.log(`ā¶ļø ${filePath}`);
|
|
469
|
-
console.log("=".repeat(80));
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const result = await runSingleFile(filePath, {
|
|
473
|
-
...spawnBase,
|
|
474
|
-
maxOldSpaceMb: getHeapForFile(filePath, maxOldSpaceMb, perFileHeapOverrides),
|
|
475
|
-
vitestArgs,
|
|
476
|
-
streamOutput: !suppressFileOutput && !json
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
if (!suppressFileOutput && emitTextOutput) {
|
|
480
|
-
const durationSec = (result.duration / 1000).toFixed(2);
|
|
481
|
-
if (result.code === 0) {
|
|
482
|
-
const heapInfo = result.heapMb ? ` | ${result.heapMb} MB heap` : "";
|
|
483
|
-
console.log(`\nā
PASSED (${durationSec}s${heapInfo})\n`);
|
|
484
|
-
} else {
|
|
485
|
-
console.log(`\nā FAILED (exit code ${result.code}, ${durationSec}s)\n`);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return result;
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
if (!_testResultsOverride) {
|
|
493
|
-
// Phase 1: solo files
|
|
494
|
-
for (const filePath of soloFiles) {
|
|
495
|
-
const result = await runTestFile(filePath).catch((err) => {
|
|
496
|
-
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
497
|
-
return null;
|
|
498
|
-
});
|
|
499
|
-
if (result) results.push(result);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Phase 2: parallel files with worker pool
|
|
503
|
-
let index = 0;
|
|
504
|
-
const activePromises = new Set();
|
|
505
|
-
|
|
506
|
-
while (index < parallelFiles.length || activePromises.size > 0) {
|
|
507
|
-
while (index < parallelFiles.length && activePromises.size < workers) {
|
|
508
|
-
const filePath = parallelFiles[index++];
|
|
509
|
-
const promise = runTestFile(filePath)
|
|
510
|
-
.then((result) => results.push(result))
|
|
511
|
-
.catch((err) => {
|
|
512
|
-
if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
|
|
513
|
-
})
|
|
514
|
-
.finally(() => activePromises.delete(promise));
|
|
515
|
-
activePromises.add(promise);
|
|
516
|
-
}
|
|
517
|
-
// c8 ignore next -- size is always >0 when the outer while body is reached
|
|
518
|
-
if (activePromises.size > 0) await Promise.race(activePromises);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// āāā FINAL REPORT āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
523
|
-
const totalTestFilesPass = results.reduce((s, r) => s + r.testFilesPass, 0);
|
|
524
|
-
const totalTestFilesFail = results.reduce((s, r) => s + r.testFilesFail, 0);
|
|
525
|
-
const totalTestsPass = results.reduce((s, r) => s + r.testsPass, 0);
|
|
526
|
-
const totalTestsFail = results.reduce((s, r) => s + r.testsFail, 0);
|
|
527
|
-
const totalTestsSkip = results.reduce((s, r) => s + (r.testsSkip || 0), 0);
|
|
528
|
-
const totalDuration = results.reduce((s, r) => s + r.duration, 0);
|
|
529
|
-
const failedFiles = results.filter((r) => r.code !== 0);
|
|
530
|
-
const passedFiles = results.filter((r) => r.code === 0);
|
|
531
|
-
const exitCode = failedFiles.length > 0 ? 1 : 0;
|
|
532
|
-
const scriptMemory = process.memoryUsage();
|
|
533
|
-
const scriptHeapMb = Math.round(scriptMemory.heapUsed / 1024 / 1024);
|
|
534
|
-
const scriptRssMb = Math.round(scriptMemory.rss / 1024 / 1024);
|
|
535
|
-
const withHeap = results.filter((r) => r.heapMb !== null);
|
|
536
|
-
const actualDurationSec = ((Date.now() - scriptStartTime) / 1000).toFixed(2);
|
|
537
|
-
const testsDurationSec = (totalDuration / 1000).toFixed(2);
|
|
538
|
-
|
|
539
|
-
if (json) {
|
|
540
|
-
return {
|
|
541
|
-
exitCode,
|
|
542
|
-
mode: "standard",
|
|
543
|
-
options: {
|
|
544
|
-
cwd,
|
|
545
|
-
testDir,
|
|
546
|
-
testPatterns,
|
|
547
|
-
testListFile,
|
|
548
|
-
workers,
|
|
549
|
-
coverageQuiet,
|
|
550
|
-
suppressFileOutput,
|
|
551
|
-
suppressPassingFiles,
|
|
552
|
-
topSummary,
|
|
553
|
-
showErrorDetails,
|
|
554
|
-
worstCoverageCount,
|
|
555
|
-
vitestArgs
|
|
556
|
-
},
|
|
557
|
-
totals: {
|
|
558
|
-
testFilesPass: totalTestFilesPass,
|
|
559
|
-
testFilesFail: totalTestFilesFail,
|
|
560
|
-
testsPass: totalTestsPass,
|
|
561
|
-
testsFail: totalTestsFail,
|
|
562
|
-
testsSkip: totalTestsSkip,
|
|
563
|
-
totalTests: totalTestsPass + totalTestsFail + totalTestsSkip
|
|
564
|
-
},
|
|
565
|
-
timing: {
|
|
566
|
-
startAt: scriptStartTimeFormatted,
|
|
567
|
-
wallClockSeconds: Number(actualDurationSec),
|
|
568
|
-
testsSeconds: Number(testsDurationSec)
|
|
569
|
-
},
|
|
570
|
-
heap: {
|
|
571
|
-
scriptHeapMb,
|
|
572
|
-
scriptRssMb,
|
|
573
|
-
maxHeapMb: withHeap.length > 0 ? Math.max(...withHeap.map((r) => r.heapMb)) : null,
|
|
574
|
-
avgHeapMb: withHeap.length > 0 ? Number((withHeap.reduce((sum, r) => sum + r.heapMb, 0) / withHeap.length).toFixed(0)) : null
|
|
575
|
-
},
|
|
576
|
-
results: {
|
|
577
|
-
all: results,
|
|
578
|
-
passed: passedFiles,
|
|
579
|
-
failed: failedFiles
|
|
580
|
-
},
|
|
581
|
-
...(topSummary
|
|
582
|
-
? {
|
|
583
|
-
topMemoryUsers: [...withHeap].sort((a, b) => b.heapMb - a.heapMb).slice(0, 10),
|
|
584
|
-
topDuration: [...results].sort((a, b) => b.duration - a.duration).slice(0, 10)
|
|
585
|
-
}
|
|
586
|
-
: {})
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
console.log("\n" + "=".repeat(80));
|
|
591
|
-
|
|
592
|
-
// Top memory users
|
|
593
|
-
if (topSummary && withHeap.length > 0) {
|
|
594
|
-
console.log("\n" + chalk.bold("š§ TOP MEMORY USERS"));
|
|
595
|
-
console.log("-".repeat(80));
|
|
596
|
-
[...withHeap]
|
|
597
|
-
.sort((a, b) => b.heapMb - a.heapMb)
|
|
598
|
-
.slice(0, 10)
|
|
599
|
-
.forEach((r) => {
|
|
600
|
-
console.log(` ${String(r.heapMb).padStart(4)} MB ${chalk.dim(r.file)}`);
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Top duration
|
|
605
|
-
if (topSummary && results.length > 0) {
|
|
606
|
-
console.log("\n" + chalk.bold("ā±ļø TOP DURATION"));
|
|
607
|
-
console.log("-".repeat(80));
|
|
608
|
-
[...results]
|
|
609
|
-
.sort((a, b) => b.duration - a.duration)
|
|
610
|
-
.slice(0, 10)
|
|
611
|
-
.forEach((r) => {
|
|
612
|
-
const sec = (r.duration / 1000).toFixed(2);
|
|
613
|
-
console.log(` ${(sec + "s").padStart(8)} ${chalk.dim(r.file)}`);
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Passed files
|
|
618
|
-
if (passedFiles.length > 0 && !suppressPassingFiles) {
|
|
619
|
-
console.log("\n" + "=".repeat(80));
|
|
620
|
-
console.log(chalk.bold.green("ā PASSED TEST FILES"));
|
|
621
|
-
console.log("=".repeat(80));
|
|
622
|
-
passedFiles.forEach((r) => {
|
|
623
|
-
const durationSec = (r.duration / 1000).toFixed(2);
|
|
624
|
-
const statsInfo = [...(r.heapMb ? [`${r.heapMb} MB`] : []), `${durationSec}s`];
|
|
625
|
-
const testInfo = r.testsPass > 0 ? ` - ${r.testsPass} tests` : "";
|
|
626
|
-
console.log(chalk.green(`ā ${r.file}${testInfo}`) + chalk.dim(` (${statsInfo.join(", ")})`));
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Summary banner
|
|
631
|
-
console.log("\n" + chalk.bold("=".repeat(80)));
|
|
632
|
-
|
|
633
|
-
if (failedFiles.length > 0) {
|
|
634
|
-
console.log(`\nā ${failedFiles.length} test file(s) failed`);
|
|
635
|
-
console.log(chalk.bold.red("\nFailed Test Files:"));
|
|
636
|
-
|
|
637
|
-
failedFiles.forEach((r) => {
|
|
638
|
-
const durationSec = (r.duration / 1000).toFixed(2);
|
|
639
|
-
const testCounts = [
|
|
640
|
-
...(r.testsFail > 0 ? [chalk.red(`${r.testsFail} failed`)] : []),
|
|
641
|
-
...(r.testsPass > 0 ? [chalk.green(`${r.testsPass} passed`)] : []),
|
|
642
|
-
...(r.testsSkip > 0 ? [chalk.yellow(`${r.testsSkip} skipped`)] : [])
|
|
643
|
-
];
|
|
644
|
-
const statsInfo = [...(r.heapMb ? [`${r.heapMb} MB`] : []), `${durationSec}s`];
|
|
645
|
-
const countStr = testCounts.length > 0 ? ` (${testCounts.join(", ")})` : "";
|
|
646
|
-
console.log(` ${chalk.red("ā")} ${r.file}${countStr}` + chalk.dim(` [${statsInfo.join(", ")}]`));
|
|
647
|
-
|
|
648
|
-
if (showErrorDetails && r.errors.length > 0) {
|
|
649
|
-
const deduped = deduplicateErrors(r.errors)
|
|
650
|
-
.split("\n")
|
|
651
|
-
.map((line) => (line.trim() ? ` ${line}` : ""))
|
|
652
|
-
.join("\n");
|
|
653
|
-
console.log(deduped);
|
|
654
|
-
console.log("");
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
console.log("");
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
console.log(chalk.bold("=".repeat(80)));
|
|
662
|
-
|
|
663
|
-
// Vitest-style summary lines
|
|
664
|
-
if (totalTestFilesFail > 0 && totalTestFilesPass > 0) {
|
|
665
|
-
console.log(
|
|
666
|
-
` ${chalk.bold("Test Files")} ${chalk.red(`${totalTestFilesFail} failed`)} ${chalk.dim("|")} ${chalk.green(`${totalTestFilesPass} passed`)} ${chalk.dim(`(${totalTestFilesPass + totalTestFilesFail})`)}`
|
|
667
|
-
);
|
|
668
|
-
} else if (totalTestFilesFail > 0) {
|
|
669
|
-
console.log(` ${chalk.bold("Test Files")} ${chalk.red(`${totalTestFilesFail} failed`)} ${chalk.dim(`(${totalTestFilesFail})`)}`);
|
|
670
|
-
} else {
|
|
671
|
-
console.log(` ${chalk.bold("Test Files")} ${chalk.green(`${totalTestFilesPass} passed`)} ${chalk.dim(`(${totalTestFilesPass})`)}`);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const totalTests = totalTestsPass + totalTestsFail + totalTestsSkip;
|
|
675
|
-
const testsParts = [
|
|
676
|
-
...(totalTestsFail > 0 ? [chalk.red(`${totalTestsFail} failed`)] : []),
|
|
677
|
-
...(totalTestsPass > 0 ? [chalk.green(`${totalTestsPass} passed`)] : []),
|
|
678
|
-
...(totalTestsSkip > 0 ? [chalk.yellow(`${totalTestsSkip} skipped`)] : [])
|
|
679
|
-
];
|
|
680
|
-
console.log(` ${chalk.bold("Tests")} ${testsParts.join(` ${chalk.dim("|")} `)} ${chalk.dim(`(${totalTests})`)}`);
|
|
681
|
-
console.log(` ${chalk.bold("Start at")} ${scriptStartTimeFormatted}`);
|
|
682
|
-
|
|
683
|
-
console.log(` ${chalk.bold("Duration")} ${actualDurationSec}s ${chalk.dim(`(tests ${testsDurationSec}s)`)}`);
|
|
684
|
-
|
|
685
|
-
if (withHeap.length > 0) {
|
|
686
|
-
const maxHeap = Math.max(...withHeap.map((r) => r.heapMb));
|
|
687
|
-
const avgHeap = (withHeap.reduce((s, r) => s + r.heapMb, 0) / withHeap.length).toFixed(0);
|
|
688
|
-
console.log(` ${chalk.bold("Heap")} max ${maxHeap} MB | avg ${avgHeap} MB | script ${scriptHeapMb} MB (RSS ${scriptRssMb} MB)`);
|
|
689
|
-
} else {
|
|
690
|
-
console.log(` ${chalk.bold("Heap")} script ${scriptHeapMb} MB (RSS ${scriptRssMb} MB)`);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (failedFiles.length > 0) return 1;
|
|
694
|
-
|
|
695
|
-
console.log(`\nā
All ${passedFiles.length} test files passed\n`);
|
|
696
|
-
return 0;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// Re-export sub-module utilities so callers can use them without deep imports
|
|
700
|
-
export { resolveBin, resolveVitestConfig } from "./utils/resolve.mjs";
|
|
701
|
-
export { discoverVitestFiles, sortWithPriority, discoverFilesInDir } from "./core/discover.mjs";
|
|
702
|
-
export { parseVitestOutput, deduplicateErrors } from "./core/parse.mjs";
|
|
703
|
-
export { runSingleFile, runVitestDirect, runMergeReports } from "./core/spawn.mjs";
|
|
704
|
-
export { createCoverageProgressTracker, noopProgressTracker } from "./core/progress.mjs";
|
|
705
|
-
export { printCoverageSummary, printMergeOutput, printQuietCoverageFailureDetails } from "./core/report.mjs";
|
|
706
|
-
export { formatDuration } from "./utils/duration.mjs";
|
|
707
|
-
export { stripAnsi, colourPct } from "./utils/ansi.mjs";
|
|
708
|
-
export { buildNodeOptions } from "./utils/env.mjs";
|