@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.
@@ -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
- }
@@ -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
- };
@@ -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
- }