@justinmiehle/reporter-vitest 0.0.6 → 0.0.7
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/dist/index.d.ts +8 -0
- package/dist/index.js +142 -8
- package/package.json +2 -2
- package/src/index.ts +188 -8
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,13 @@ export default class PanoptesReporter implements Reporter {
|
|
|
16
16
|
onTestCaseResult(test: any): void;
|
|
17
17
|
onTestRunEnd(): Promise<void>;
|
|
18
18
|
private mapStatus;
|
|
19
|
+
private readCodeSnippet;
|
|
20
|
+
private getDiagnostic;
|
|
21
|
+
private getArtifacts;
|
|
22
|
+
/**
|
|
23
|
+
* Read Istanbul-style coverage from coverage/coverage-final.json (or PANOPTES_COVERAGE_PATH).
|
|
24
|
+
* Enable coverage in Vitest (e.g. coverage: { provider: 'v8', reporter: ['json'] }) for LOC to be sent.
|
|
25
|
+
*/
|
|
26
|
+
private readCoverage;
|
|
19
27
|
}
|
|
20
28
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,10 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
var CODE_SNIPPET_CONTEXT_LINES = 10;
|
|
12
|
+
var DEFAULT_COVERAGE_PATH = "coverage/coverage-final.json";
|
|
9
13
|
function getCommitSha() {
|
|
10
14
|
if (process.env.GITHUB_SHA) {
|
|
11
15
|
return process.env.GITHUB_SHA;
|
|
@@ -45,11 +49,22 @@ var PanoptesReporter = class {
|
|
|
45
49
|
}
|
|
46
50
|
// biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface doesn't provide strict types
|
|
47
51
|
onTestCaseResult(test) {
|
|
52
|
+
const filePath = test.file?.name || test.filepath || "unknown";
|
|
53
|
+
const line = test.file?.line ?? test.location?.line;
|
|
54
|
+
const codeSnippet = this.readCodeSnippet(filePath, line);
|
|
55
|
+
const diagnostic = this.getDiagnostic(test);
|
|
56
|
+
const artifactsList = this.getArtifacts(test);
|
|
57
|
+
const metadata = {
|
|
58
|
+
type: test.type,
|
|
59
|
+
mode: test.mode,
|
|
60
|
+
...diagnostic && { diagnostic },
|
|
61
|
+
...artifactsList && artifactsList.length > 0 && { artifacts: artifactsList }
|
|
62
|
+
};
|
|
48
63
|
const testResult = {
|
|
49
64
|
name: test.name || test.title || "Unknown test",
|
|
50
|
-
file:
|
|
51
|
-
line
|
|
52
|
-
column: test.file?.column,
|
|
65
|
+
file: filePath,
|
|
66
|
+
line,
|
|
67
|
+
column: test.file?.column ?? test.location?.column,
|
|
53
68
|
status: this.mapStatus(test.status),
|
|
54
69
|
duration: test.duration || 0,
|
|
55
70
|
error: test.error?.message,
|
|
@@ -57,10 +72,8 @@ var PanoptesReporter = class {
|
|
|
57
72
|
retries: test.retryCount,
|
|
58
73
|
suite: test.suite?.name,
|
|
59
74
|
tags: test.meta?.tags,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
mode: test.mode
|
|
63
|
-
}
|
|
75
|
+
codeSnippet,
|
|
76
|
+
metadata
|
|
64
77
|
};
|
|
65
78
|
this.tests.push(testResult);
|
|
66
79
|
if (test.suite) {
|
|
@@ -98,6 +111,7 @@ var PanoptesReporter = class {
|
|
|
98
111
|
skippedTests: skipped
|
|
99
112
|
};
|
|
100
113
|
});
|
|
114
|
+
const coverage = this.readCoverage();
|
|
101
115
|
const testRun = {
|
|
102
116
|
projectName: this.options.projectName,
|
|
103
117
|
framework: "vitest",
|
|
@@ -114,7 +128,8 @@ var PanoptesReporter = class {
|
|
|
114
128
|
ci: this.options.ci,
|
|
115
129
|
commitSha: getCommitSha(),
|
|
116
130
|
tests: this.tests,
|
|
117
|
-
suites: suiteData
|
|
131
|
+
suites: suiteData,
|
|
132
|
+
...coverage && { coverage }
|
|
118
133
|
};
|
|
119
134
|
if (!this.options.convexUrl) {
|
|
120
135
|
console.warn("[Panoptes] CONVEX_URL not set. Test results will not be sent.");
|
|
@@ -154,6 +169,125 @@ var PanoptesReporter = class {
|
|
|
154
169
|
return "running";
|
|
155
170
|
}
|
|
156
171
|
}
|
|
172
|
+
readCodeSnippet(filePath, line) {
|
|
173
|
+
if (line == null) return void 0;
|
|
174
|
+
let absPath;
|
|
175
|
+
try {
|
|
176
|
+
absPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
177
|
+
if (!fs.existsSync(absPath)) return void 0;
|
|
178
|
+
} catch {
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
183
|
+
const lines = content.split("\n");
|
|
184
|
+
const start = Math.max(0, line - 1 - CODE_SNIPPET_CONTEXT_LINES);
|
|
185
|
+
const end = Math.min(lines.length, line + CODE_SNIPPET_CONTEXT_LINES);
|
|
186
|
+
const slice = lines.slice(start, end).join("\n");
|
|
187
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
188
|
+
const language = ext === ".ts" || ext === ".tsx" ? "typescript" : ext === ".js" || ext === ".jsx" ? "javascript" : void 0;
|
|
189
|
+
return {
|
|
190
|
+
startLine: start + 1,
|
|
191
|
+
endLine: end,
|
|
192
|
+
content: slice,
|
|
193
|
+
language
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface
|
|
200
|
+
getDiagnostic(test) {
|
|
201
|
+
try {
|
|
202
|
+
if (typeof test.diagnostic !== "function") return void 0;
|
|
203
|
+
const d = test.diagnostic();
|
|
204
|
+
if (!d) return void 0;
|
|
205
|
+
return {
|
|
206
|
+
duration: d.duration,
|
|
207
|
+
slow: d.slow,
|
|
208
|
+
flaky: d.flaky,
|
|
209
|
+
retryCount: d.retryCount,
|
|
210
|
+
repeatCount: d.repeatCount,
|
|
211
|
+
heap: d.heap,
|
|
212
|
+
startTime: d.startTime
|
|
213
|
+
};
|
|
214
|
+
} catch {
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface; artifacts() is experimental
|
|
219
|
+
getArtifacts(test) {
|
|
220
|
+
try {
|
|
221
|
+
if (typeof test.artifacts !== "function") return void 0;
|
|
222
|
+
const list = test.artifacts();
|
|
223
|
+
if (!Array.isArray(list) || list.length === 0) return void 0;
|
|
224
|
+
return list.map((a) => ({ type: a?.type ?? "unknown" }));
|
|
225
|
+
} catch {
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Read Istanbul-style coverage from coverage/coverage-final.json (or PANOPTES_COVERAGE_PATH).
|
|
231
|
+
* Enable coverage in Vitest (e.g. coverage: { provider: 'v8', reporter: ['json'] }) for LOC to be sent.
|
|
232
|
+
*/
|
|
233
|
+
readCoverage() {
|
|
234
|
+
const coveragePath = process.env.PANOPTES_COVERAGE_PATH ?? path.resolve(process.cwd(), DEFAULT_COVERAGE_PATH);
|
|
235
|
+
try {
|
|
236
|
+
if (!fs.existsSync(coveragePath)) return void 0;
|
|
237
|
+
const raw = fs.readFileSync(coveragePath, "utf-8");
|
|
238
|
+
const data = JSON.parse(raw);
|
|
239
|
+
const files = {};
|
|
240
|
+
let totalLines = 0;
|
|
241
|
+
let coveredLines = 0;
|
|
242
|
+
for (const [filePath, entry] of Object.entries(data)) {
|
|
243
|
+
if (!entry || typeof entry !== "object") continue;
|
|
244
|
+
const statementCounts = entry.s ?? {};
|
|
245
|
+
const statementMap = entry.statementMap;
|
|
246
|
+
let linesTotal = 0;
|
|
247
|
+
let linesCovered = 0;
|
|
248
|
+
if (statementMap && typeof statementMap === "object") {
|
|
249
|
+
const lineHits = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const [id, loc] of Object.entries(statementMap)) {
|
|
251
|
+
if (!loc?.start?.line) continue;
|
|
252
|
+
const count = statementCounts[id] ?? 0;
|
|
253
|
+
const line = loc.start.line;
|
|
254
|
+
lineHits.set(line, (lineHits.get(line) ?? 0) + count);
|
|
255
|
+
}
|
|
256
|
+
linesTotal = lineHits.size;
|
|
257
|
+
for (const hits of Array.from(lineHits.values())) {
|
|
258
|
+
if (hits > 0) linesCovered += 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const statementsTotal = Object.keys(statementCounts).length;
|
|
262
|
+
const statementsCovered = Object.values(statementCounts).filter(
|
|
263
|
+
(c) => typeof c === "number" && c > 0
|
|
264
|
+
).length;
|
|
265
|
+
if (statementsTotal > 0 && linesTotal === 0) {
|
|
266
|
+
linesTotal = statementsTotal;
|
|
267
|
+
linesCovered = statementsCovered;
|
|
268
|
+
}
|
|
269
|
+
totalLines += linesTotal;
|
|
270
|
+
coveredLines += linesCovered;
|
|
271
|
+
const relPath = path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
|
|
272
|
+
files[relPath] = {
|
|
273
|
+
linesCovered,
|
|
274
|
+
linesTotal: linesTotal || 1,
|
|
275
|
+
statementsCovered: statementsTotal > 0 ? statementsCovered : void 0,
|
|
276
|
+
statementsTotal: statementsTotal > 0 ? statementsTotal : void 0
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (Object.keys(files).length === 0) return void 0;
|
|
280
|
+
return {
|
|
281
|
+
summary: {
|
|
282
|
+
lines: { total: totalLines, covered: coveredLines },
|
|
283
|
+
statements: { total: totalLines, covered: coveredLines }
|
|
284
|
+
},
|
|
285
|
+
files
|
|
286
|
+
};
|
|
287
|
+
} catch {
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
157
291
|
};
|
|
158
292
|
export {
|
|
159
293
|
PanoptesReporter as default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justinmiehle/reporter-vitest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Panoptes reporter for Vitest test results",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@justinmiehle/shared": "
|
|
29
|
+
"@justinmiehle/shared": "workspace:*"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^20.10.0",
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { CoverageIngest, FileCoverage, TestResult, TestRunIngest } from "@justinmiehle/shared";
|
|
2
5
|
import type { Reporter } from "vitest/reporters";
|
|
3
6
|
|
|
7
|
+
const CODE_SNIPPET_CONTEXT_LINES = 10;
|
|
8
|
+
const DEFAULT_COVERAGE_PATH = "coverage/coverage-final.json";
|
|
9
|
+
|
|
4
10
|
interface PanoptesReporterOptions {
|
|
5
11
|
convexUrl?: string;
|
|
6
12
|
projectName?: string;
|
|
@@ -30,6 +36,20 @@ function getCommitSha(): string | undefined {
|
|
|
30
36
|
}
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
function getTriggeredBy(): string | undefined {
|
|
40
|
+
// CI: use provider's actor/username
|
|
41
|
+
if (process.env.GITHUB_ACTOR) return process.env.GITHUB_ACTOR;
|
|
42
|
+
if (process.env.CIRCLE_USERNAME) return process.env.CIRCLE_USERNAME;
|
|
43
|
+
if (process.env.GITLAB_USER_LOGIN) return process.env.GITLAB_USER_LOGIN;
|
|
44
|
+
// Local: machine username
|
|
45
|
+
if (process.env.USER) return process.env.USER;
|
|
46
|
+
try {
|
|
47
|
+
return os.userInfo().username;
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
export default class PanoptesReporter implements Reporter {
|
|
34
54
|
private options: Required<PanoptesReporterOptions>;
|
|
35
55
|
private startTime = 0;
|
|
@@ -56,11 +76,23 @@ export default class PanoptesReporter implements Reporter {
|
|
|
56
76
|
|
|
57
77
|
// biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface doesn't provide strict types
|
|
58
78
|
onTestCaseResult(test: any) {
|
|
79
|
+
const filePath = test.file?.name || test.filepath || "unknown";
|
|
80
|
+
const line = test.file?.line ?? test.location?.line;
|
|
81
|
+
const codeSnippet = this.readCodeSnippet(filePath, line);
|
|
82
|
+
const diagnostic = this.getDiagnostic(test);
|
|
83
|
+
const artifactsList = this.getArtifacts(test);
|
|
84
|
+
const metadata: Record<string, unknown> = {
|
|
85
|
+
type: test.type,
|
|
86
|
+
mode: test.mode,
|
|
87
|
+
...(diagnostic && { diagnostic: diagnostic }),
|
|
88
|
+
...(artifactsList && artifactsList.length > 0 && { artifacts: artifactsList }),
|
|
89
|
+
};
|
|
90
|
+
|
|
59
91
|
const testResult: TestResult = {
|
|
60
92
|
name: test.name || test.title || "Unknown test",
|
|
61
|
-
file:
|
|
62
|
-
line
|
|
63
|
-
column: test.file?.column,
|
|
93
|
+
file: filePath,
|
|
94
|
+
line,
|
|
95
|
+
column: test.file?.column ?? test.location?.column,
|
|
64
96
|
status: this.mapStatus(test.status),
|
|
65
97
|
duration: test.duration || 0,
|
|
66
98
|
error: test.error?.message,
|
|
@@ -68,10 +100,8 @@ export default class PanoptesReporter implements Reporter {
|
|
|
68
100
|
retries: test.retryCount,
|
|
69
101
|
suite: test.suite?.name,
|
|
70
102
|
tags: test.meta?.tags,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
mode: test.mode,
|
|
74
|
-
},
|
|
103
|
+
codeSnippet,
|
|
104
|
+
metadata,
|
|
75
105
|
};
|
|
76
106
|
|
|
77
107
|
this.tests.push(testResult);
|
|
@@ -119,6 +149,8 @@ export default class PanoptesReporter implements Reporter {
|
|
|
119
149
|
};
|
|
120
150
|
});
|
|
121
151
|
|
|
152
|
+
const coverage = this.readCoverage();
|
|
153
|
+
|
|
122
154
|
const testRun: TestRunIngest = {
|
|
123
155
|
projectName: this.options.projectName,
|
|
124
156
|
framework: "vitest",
|
|
@@ -135,6 +167,7 @@ export default class PanoptesReporter implements Reporter {
|
|
|
135
167
|
commitSha: getCommitSha(),
|
|
136
168
|
tests: this.tests,
|
|
137
169
|
suites: suiteData,
|
|
170
|
+
...(coverage && { coverage }),
|
|
138
171
|
};
|
|
139
172
|
|
|
140
173
|
if (!this.options.convexUrl) {
|
|
@@ -178,4 +211,151 @@ export default class PanoptesReporter implements Reporter {
|
|
|
178
211
|
return "running";
|
|
179
212
|
}
|
|
180
213
|
}
|
|
214
|
+
|
|
215
|
+
private readCodeSnippet(filePath: string, line: number | undefined): TestResult["codeSnippet"] {
|
|
216
|
+
if (line == null) return undefined;
|
|
217
|
+
let absPath: string;
|
|
218
|
+
try {
|
|
219
|
+
absPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
220
|
+
if (!fs.existsSync(absPath)) return undefined;
|
|
221
|
+
} catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
226
|
+
const lines = content.split("\n");
|
|
227
|
+
const start = Math.max(0, line - 1 - CODE_SNIPPET_CONTEXT_LINES);
|
|
228
|
+
const end = Math.min(lines.length, line + CODE_SNIPPET_CONTEXT_LINES);
|
|
229
|
+
const slice = lines.slice(start, end).join("\n");
|
|
230
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
231
|
+
const language =
|
|
232
|
+
ext === ".ts" || ext === ".tsx"
|
|
233
|
+
? "typescript"
|
|
234
|
+
: ext === ".js" || ext === ".jsx"
|
|
235
|
+
? "javascript"
|
|
236
|
+
: undefined;
|
|
237
|
+
return {
|
|
238
|
+
startLine: start + 1,
|
|
239
|
+
endLine: end,
|
|
240
|
+
content: slice,
|
|
241
|
+
language,
|
|
242
|
+
};
|
|
243
|
+
} catch {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface
|
|
249
|
+
private getDiagnostic(test: any): Record<string, unknown> | undefined {
|
|
250
|
+
try {
|
|
251
|
+
if (typeof test.diagnostic !== "function") return undefined;
|
|
252
|
+
const d = test.diagnostic();
|
|
253
|
+
if (!d) return undefined;
|
|
254
|
+
return {
|
|
255
|
+
duration: d.duration,
|
|
256
|
+
slow: d.slow,
|
|
257
|
+
flaky: d.flaky,
|
|
258
|
+
retryCount: d.retryCount,
|
|
259
|
+
repeatCount: d.repeatCount,
|
|
260
|
+
heap: d.heap,
|
|
261
|
+
startTime: d.startTime,
|
|
262
|
+
};
|
|
263
|
+
} catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface; artifacts() is experimental
|
|
269
|
+
private getArtifacts(test: any): Array<{ type: string }> | undefined {
|
|
270
|
+
try {
|
|
271
|
+
if (typeof test.artifacts !== "function") return undefined;
|
|
272
|
+
const list = test.artifacts();
|
|
273
|
+
if (!Array.isArray(list) || list.length === 0) return undefined;
|
|
274
|
+
return list.map((a: { type?: string }) => ({ type: a?.type ?? "unknown" }));
|
|
275
|
+
} catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Read Istanbul-style coverage from coverage/coverage-final.json (or PANOPTES_COVERAGE_PATH).
|
|
282
|
+
* Enable coverage in Vitest (e.g. coverage: { provider: 'v8', reporter: ['json'] }) for LOC to be sent.
|
|
283
|
+
*/
|
|
284
|
+
private readCoverage(): CoverageIngest | undefined {
|
|
285
|
+
const coveragePath =
|
|
286
|
+
process.env.PANOPTES_COVERAGE_PATH ?? path.resolve(process.cwd(), DEFAULT_COVERAGE_PATH);
|
|
287
|
+
try {
|
|
288
|
+
if (!fs.existsSync(coveragePath)) return undefined;
|
|
289
|
+
const raw = fs.readFileSync(coveragePath, "utf-8");
|
|
290
|
+
const data = JSON.parse(raw) as Record<
|
|
291
|
+
string,
|
|
292
|
+
{
|
|
293
|
+
s?: Record<string, number>;
|
|
294
|
+
b?: Record<string, number[]>;
|
|
295
|
+
f?: Record<string, number>;
|
|
296
|
+
statementMap?: Record<string, unknown>;
|
|
297
|
+
branchMap?: Record<string, unknown>;
|
|
298
|
+
fnMap?: Record<string, unknown>;
|
|
299
|
+
}
|
|
300
|
+
>;
|
|
301
|
+
const files: Record<string, FileCoverage> = {};
|
|
302
|
+
let totalLines = 0;
|
|
303
|
+
let coveredLines = 0;
|
|
304
|
+
for (const [filePath, entry] of Object.entries(data)) {
|
|
305
|
+
if (!entry || typeof entry !== "object") continue;
|
|
306
|
+
// Istanbul does not always have 'l' (line counts); derive from statementMap + s if needed
|
|
307
|
+
const statementCounts = entry.s ?? {};
|
|
308
|
+
const statementMap = (
|
|
309
|
+
entry as {
|
|
310
|
+
statementMap?: Record<string, { start: { line: number }; end: { line: number } }>;
|
|
311
|
+
}
|
|
312
|
+
).statementMap;
|
|
313
|
+
let linesTotal = 0;
|
|
314
|
+
let linesCovered = 0;
|
|
315
|
+
if (statementMap && typeof statementMap === "object") {
|
|
316
|
+
const lineHits = new Map<number, number>();
|
|
317
|
+
for (const [id, loc] of Object.entries(statementMap)) {
|
|
318
|
+
if (!loc?.start?.line) continue;
|
|
319
|
+
const count = statementCounts[id] ?? 0;
|
|
320
|
+
const line = loc.start.line;
|
|
321
|
+
lineHits.set(line, (lineHits.get(line) ?? 0) + count);
|
|
322
|
+
}
|
|
323
|
+
linesTotal = lineHits.size;
|
|
324
|
+
for (const hits of Array.from(lineHits.values())) {
|
|
325
|
+
if (hits > 0) linesCovered += 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const statementsTotal = Object.keys(statementCounts).length;
|
|
329
|
+
const statementsCovered = Object.values(statementCounts).filter(
|
|
330
|
+
(c) => typeof c === "number" && c > 0
|
|
331
|
+
).length;
|
|
332
|
+
if (statementsTotal > 0 && linesTotal === 0) {
|
|
333
|
+
linesTotal = statementsTotal;
|
|
334
|
+
linesCovered = statementsCovered;
|
|
335
|
+
}
|
|
336
|
+
totalLines += linesTotal;
|
|
337
|
+
coveredLines += linesCovered;
|
|
338
|
+
// Use relative path for consistency
|
|
339
|
+
const relPath = path.isAbsolute(filePath)
|
|
340
|
+
? path.relative(process.cwd(), filePath)
|
|
341
|
+
: filePath;
|
|
342
|
+
files[relPath] = {
|
|
343
|
+
linesCovered,
|
|
344
|
+
linesTotal: linesTotal || 1,
|
|
345
|
+
statementsCovered: statementsTotal > 0 ? statementsCovered : undefined,
|
|
346
|
+
statementsTotal: statementsTotal > 0 ? statementsTotal : undefined,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (Object.keys(files).length === 0) return undefined;
|
|
350
|
+
return {
|
|
351
|
+
summary: {
|
|
352
|
+
lines: { total: totalLines, covered: coveredLines },
|
|
353
|
+
statements: { total: totalLines, covered: coveredLines },
|
|
354
|
+
},
|
|
355
|
+
files,
|
|
356
|
+
};
|
|
357
|
+
} catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
181
361
|
}
|