qa360 2.2.14 → 2.2.18
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/cli/dist/commands/ai.js +1 -1
- package/cli/dist/commands/ask.js +362 -36
- package/cli/dist/commands/coverage.js +1 -1
- package/cli/dist/commands/crawl.js +1 -1
- package/cli/dist/commands/doctor.js +2 -2
- package/cli/dist/commands/explain.js +2 -2
- package/cli/dist/commands/flakiness.js +1 -1
- package/cli/dist/commands/generate.js +1 -1
- package/cli/dist/commands/history.js +1 -1
- package/cli/dist/commands/monitor.js +3 -3
- package/cli/dist/commands/ollama.js +1 -1
- package/cli/dist/commands/pack.js +2 -2
- package/cli/dist/commands/regression.js +1 -1
- package/cli/dist/commands/repair.js +1 -1
- package/cli/dist/commands/retry.js +1 -1
- package/cli/dist/commands/run.d.ts +1 -1
- package/cli/dist/commands/run.js +2 -1
- package/cli/dist/commands/secrets.js +1 -1
- package/cli/dist/commands/serve.js +1 -1
- package/cli/dist/commands/slo.js +1 -1
- package/cli/dist/commands/verify.js +1 -1
- package/cli/dist/core/ai/ollama-provider.js +3 -15
- package/cli/dist/core/core/coverage/analyzer.d.ts +101 -0
- package/cli/dist/core/core/coverage/analyzer.js +415 -0
- package/cli/dist/core/core/coverage/collector.d.ts +74 -0
- package/cli/dist/core/core/coverage/collector.js +459 -0
- package/cli/dist/core/core/coverage/config.d.ts +37 -0
- package/cli/dist/core/core/coverage/config.js +156 -0
- package/cli/dist/core/core/coverage/index.d.ts +11 -0
- package/cli/dist/core/core/coverage/index.js +15 -0
- package/cli/dist/core/core/coverage/types.d.ts +267 -0
- package/cli/dist/core/core/coverage/types.js +6 -0
- package/cli/dist/core/core/coverage/vault.d.ts +95 -0
- package/cli/dist/core/core/coverage/vault.js +405 -0
- package/cli/dist/core/crawler/selector-generator.js +3 -72
- package/cli/dist/core/generation/crawler-pack-generator.d.ts +1 -1
- package/cli/dist/core/generation/crawler-pack-generator.js +143 -31
- package/cli/dist/core/pack/validator.js +2 -2
- package/cli/dist/core/pack-v2/migrator.d.ts +0 -5
- package/cli/dist/core/pack-v2/migrator.js +6 -74
- package/cli/dist/core/pack-v2/validator.js +3 -4
- package/cli/dist/core/runner/phase3-runner.js +1 -12
- package/cli/dist/utils/config.d.ts +1 -1
- package/cli/dist/utils/config.js +11 -5
- package/package.json +24 -30
- package/cli/CHANGELOG.md +0 -84
- package/cli/LICENSE +0 -24
- package/cli/package.json +0 -76
- package/core/LICENSE +0 -24
- package/core/package.json +0 -81
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage Collector
|
|
3
|
+
*
|
|
4
|
+
* Collects coverage data from various test sources.
|
|
5
|
+
* Supports Jest, Vitest, Cypress, Playwright, and LCOV formats.
|
|
6
|
+
*/
|
|
7
|
+
import type { FileCoverage, CoverageMetrics, CoverageResult, CoverageConfig } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Coverage Collector class
|
|
10
|
+
*/
|
|
11
|
+
export declare class CoverageCollector {
|
|
12
|
+
private config;
|
|
13
|
+
constructor(config?: Partial<CoverageConfig>);
|
|
14
|
+
/**
|
|
15
|
+
* Collect coverage from a file path (auto-detect format)
|
|
16
|
+
*/
|
|
17
|
+
collectFromFile(filePath: string): Promise<CoverageResult | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Collect coverage from LCOV format
|
|
20
|
+
*/
|
|
21
|
+
collectFromLcov(filePath: string): Promise<CoverageResult | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Collect coverage from JSON format (Vitest/Istanbul)
|
|
24
|
+
*/
|
|
25
|
+
collectFromJson(filePath: string): Promise<CoverageResult | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Collect coverage from Istanbul format
|
|
28
|
+
*/
|
|
29
|
+
collectFromIstanbul(filePath: string): Promise<CoverageResult | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Parse LCOV format content
|
|
32
|
+
*/
|
|
33
|
+
private parseLcov;
|
|
34
|
+
/**
|
|
35
|
+
* Convert LCOV record to FileCoverage
|
|
36
|
+
*/
|
|
37
|
+
private convertLcovToFileCoverage;
|
|
38
|
+
/**
|
|
39
|
+
* Convert Vitest coverage data to FileCoverage
|
|
40
|
+
*/
|
|
41
|
+
private convertVitestToFileCoverage;
|
|
42
|
+
/**
|
|
43
|
+
* Convert Istanbul coverage data to FileCoverage
|
|
44
|
+
*/
|
|
45
|
+
private convertIstanbulToFileCoverage;
|
|
46
|
+
/**
|
|
47
|
+
* Calculate aggregate metrics from file coverage data
|
|
48
|
+
*/
|
|
49
|
+
calculateMetrics(files: Record<string, FileCoverage>): CoverageMetrics;
|
|
50
|
+
/**
|
|
51
|
+
* Detect coverage format from file path
|
|
52
|
+
*/
|
|
53
|
+
private detectFormat;
|
|
54
|
+
/**
|
|
55
|
+
* Filter files based on include/exclude patterns
|
|
56
|
+
*/
|
|
57
|
+
filterFiles(files: Record<string, FileCoverage>): Record<string, FileCoverage>;
|
|
58
|
+
/**
|
|
59
|
+
* Match path against glob pattern
|
|
60
|
+
*/
|
|
61
|
+
private matchPattern;
|
|
62
|
+
/**
|
|
63
|
+
* Merge multiple coverage results
|
|
64
|
+
*/
|
|
65
|
+
mergeCoverageResults(results: CoverageResult[]): CoverageResult;
|
|
66
|
+
/**
|
|
67
|
+
* Update configuration
|
|
68
|
+
*/
|
|
69
|
+
updateConfig(updates: Partial<CoverageConfig>): void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a coverage collector with default config
|
|
73
|
+
*/
|
|
74
|
+
export declare function createCoverageCollector(config?: Partial<CoverageConfig>): CoverageCollector;
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage Collector
|
|
3
|
+
*
|
|
4
|
+
* Collects coverage data from various test sources.
|
|
5
|
+
* Supports Jest, Vitest, Cypress, Playwright, and LCOV formats.
|
|
6
|
+
*/
|
|
7
|
+
import { createDefaultCoverageConfig } from './config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Coverage Collector class
|
|
10
|
+
*/
|
|
11
|
+
export class CoverageCollector {
|
|
12
|
+
config;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = { ...createDefaultCoverageConfig(), ...config };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Collect coverage from a file path (auto-detect format)
|
|
18
|
+
*/
|
|
19
|
+
async collectFromFile(filePath) {
|
|
20
|
+
const format = this.detectFormat(filePath);
|
|
21
|
+
switch (format) {
|
|
22
|
+
case 'lcov':
|
|
23
|
+
return this.collectFromLcov(filePath);
|
|
24
|
+
case 'json':
|
|
25
|
+
return this.collectFromJson(filePath);
|
|
26
|
+
case 'istanbul':
|
|
27
|
+
return this.collectFromIstanbul(filePath);
|
|
28
|
+
default:
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Collect coverage from LCOV format
|
|
34
|
+
*/
|
|
35
|
+
async collectFromLcov(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
const fs = await import('node:fs/promises');
|
|
38
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
39
|
+
const records = this.parseLcov(content);
|
|
40
|
+
if (records.length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const files = {};
|
|
44
|
+
for (const record of records) {
|
|
45
|
+
files[record.file] = this.convertLcovToFileCoverage(record);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
source: 'lcov',
|
|
49
|
+
gate: 'unknown',
|
|
50
|
+
files,
|
|
51
|
+
metrics: this.calculateMetrics(files),
|
|
52
|
+
format: 'lcov',
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Collect coverage from JSON format (Vitest/Istanbul)
|
|
62
|
+
*/
|
|
63
|
+
async collectFromJson(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const fs = await import('node:fs/promises');
|
|
66
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
67
|
+
const data = JSON.parse(content);
|
|
68
|
+
const files = {};
|
|
69
|
+
// Handle Vitest format
|
|
70
|
+
if (data.result && typeof data.result === 'object') {
|
|
71
|
+
for (const [filePath, coverageData] of Object.entries(data.result)) {
|
|
72
|
+
if (typeof coverageData === 'object' && coverageData !== null) {
|
|
73
|
+
files[filePath] = this.convertVitestToFileCoverage(filePath, coverageData);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
source: 'vitest',
|
|
78
|
+
gate: 'unknown',
|
|
79
|
+
files,
|
|
80
|
+
metrics: this.calculateMetrics(files),
|
|
81
|
+
format: 'json',
|
|
82
|
+
timestamp: Date.now()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Handle Istanbul format
|
|
86
|
+
if (typeof data === 'object' && !data.result) {
|
|
87
|
+
for (const [filePath, coverageData] of Object.entries(data)) {
|
|
88
|
+
if (typeof coverageData === 'object' && coverageData !== null) {
|
|
89
|
+
files[filePath] = this.convertIstanbulToFileCoverage(filePath, coverageData);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
source: 'istanbul',
|
|
94
|
+
gate: 'unknown',
|
|
95
|
+
files,
|
|
96
|
+
metrics: this.calculateMetrics(files),
|
|
97
|
+
format: 'istanbul',
|
|
98
|
+
timestamp: Date.now()
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Collect coverage from Istanbul format
|
|
109
|
+
*/
|
|
110
|
+
async collectFromIstanbul(filePath) {
|
|
111
|
+
return this.collectFromJson(filePath);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse LCOV format content
|
|
115
|
+
*/
|
|
116
|
+
parseLcov(content) {
|
|
117
|
+
const records = [];
|
|
118
|
+
const lines = content.split('\n');
|
|
119
|
+
let currentRecord = null;
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
if (line.startsWith('SF:')) {
|
|
122
|
+
// Start of new file record
|
|
123
|
+
currentRecord = {
|
|
124
|
+
file: line.substring(3),
|
|
125
|
+
lines: {},
|
|
126
|
+
branches: {},
|
|
127
|
+
functions: {}
|
|
128
|
+
};
|
|
129
|
+
records.push(currentRecord);
|
|
130
|
+
}
|
|
131
|
+
else if (currentRecord) {
|
|
132
|
+
if (line.startsWith('DA:')) {
|
|
133
|
+
// Line data: DA:<line>,<count>
|
|
134
|
+
const [, lineNum, count] = line.match(/^DA:(\d+),(\d+)$/) || [];
|
|
135
|
+
if (lineNum) {
|
|
136
|
+
currentRecord.lines[parseInt(lineNum, 10)] = parseInt(count, 10);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (line.startsWith('BRDA:')) {
|
|
140
|
+
// Branch data: BRDA:<line>,<block>,<branch>,<count>
|
|
141
|
+
const parts = line.substring(5).split(',');
|
|
142
|
+
if (parts.length >= 4) {
|
|
143
|
+
const lineNum = parts[0];
|
|
144
|
+
const branchId = `${lineNum}-${parts[1]}-${parts[2]}`;
|
|
145
|
+
const count = parts[3] === '-' ? 0 : parseInt(parts[3], 10);
|
|
146
|
+
if (!currentRecord.branches[lineNum]) {
|
|
147
|
+
currentRecord.branches[lineNum] = [];
|
|
148
|
+
}
|
|
149
|
+
currentRecord.branches[lineNum].push(count);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (line.startsWith('FN:')) {
|
|
153
|
+
// Function data: FN:<line>,<function name>
|
|
154
|
+
const parts = line.substring(3).split(',');
|
|
155
|
+
if (parts.length >= 2) {
|
|
156
|
+
currentRecord.functions[parts[1]] = 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (line.startsWith('FNDA:')) {
|
|
160
|
+
// Function execution data: FNDA:<count>,<function name>
|
|
161
|
+
const parts = line.substring(5).split(',');
|
|
162
|
+
if (parts.length >= 2) {
|
|
163
|
+
currentRecord.functions[parts[1]] = parseInt(parts[0], 10);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return records;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Convert LCOV record to FileCoverage
|
|
172
|
+
*/
|
|
173
|
+
convertLcovToFileCoverage(record) {
|
|
174
|
+
const lineNumbers = Object.keys(record.lines).map(Number);
|
|
175
|
+
const totalLines = lineNumbers.length > 0 ? Math.max(...lineNumbers) : 0;
|
|
176
|
+
const coveredLines = Object.values(record.lines).filter(c => c > 0).length;
|
|
177
|
+
// Count branches
|
|
178
|
+
let totalBranches = 0;
|
|
179
|
+
let coveredBranches = 0;
|
|
180
|
+
const branchesByLine = {};
|
|
181
|
+
for (const [lineNum, branchCounts] of Object.entries(record.branches)) {
|
|
182
|
+
const line = parseInt(lineNum, 10);
|
|
183
|
+
const branches = branchCounts;
|
|
184
|
+
totalBranches += branches.length;
|
|
185
|
+
coveredBranches += branches.filter(c => c > 0).length;
|
|
186
|
+
branchesByLine[line] = branches.map((count, idx) => ({
|
|
187
|
+
index: idx,
|
|
188
|
+
count,
|
|
189
|
+
location: { start: { line, column: 0 }, end: { line, column: 0 } }
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
// Count functions
|
|
193
|
+
const totalFunctions = Object.keys(record.functions).length;
|
|
194
|
+
const coveredFunctions = Object.values(record.functions).filter(f => f > 0).length;
|
|
195
|
+
// Uncovered lines
|
|
196
|
+
const uncoveredLines = Object.entries(record.lines)
|
|
197
|
+
.filter(([, count]) => count === 0)
|
|
198
|
+
.map(([line]) => parseInt(line, 10));
|
|
199
|
+
const partiallyCoveredLines = Object.entries(record.lines)
|
|
200
|
+
.filter(([, count]) => count > 0 && count < 10) // Low execution count
|
|
201
|
+
.map(([line]) => parseInt(line, 10));
|
|
202
|
+
return {
|
|
203
|
+
path: record.file,
|
|
204
|
+
totalLines,
|
|
205
|
+
coveredLines,
|
|
206
|
+
lineCoverage: totalLines > 0 ? (coveredLines / totalLines) * 100 : 0,
|
|
207
|
+
totalBranches,
|
|
208
|
+
coveredBranches,
|
|
209
|
+
branchCoverage: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0,
|
|
210
|
+
totalFunctions,
|
|
211
|
+
coveredFunctions,
|
|
212
|
+
functionCoverage: totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0,
|
|
213
|
+
totalStatements: totalLines, // Approximation
|
|
214
|
+
coveredStatements: coveredLines,
|
|
215
|
+
statementCoverage: totalLines > 0 ? (coveredLines / totalLines) * 100 : 0,
|
|
216
|
+
uncoveredLines,
|
|
217
|
+
partiallyCoveredLines,
|
|
218
|
+
branchesByLine
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Convert Vitest coverage data to FileCoverage
|
|
223
|
+
*/
|
|
224
|
+
convertVitestToFileCoverage(filePath, data) {
|
|
225
|
+
const s = data.s || { total: 0, covered: 0, pct: 0 };
|
|
226
|
+
const b = data.b || {};
|
|
227
|
+
const f = data.f || {};
|
|
228
|
+
const stmt = data.statementMap || {};
|
|
229
|
+
const branchMap = data.branchMap || {};
|
|
230
|
+
// Calculate line coverage from statement map
|
|
231
|
+
const lines = new Set();
|
|
232
|
+
const coveredLines = new Set();
|
|
233
|
+
for (const [idx, stmtInfo] of Object.entries(stmt)) {
|
|
234
|
+
if (stmtInfo && typeof stmtInfo === 'object') {
|
|
235
|
+
const start = stmtInfo.start;
|
|
236
|
+
const end = stmtInfo.end;
|
|
237
|
+
if (start && end) {
|
|
238
|
+
for (let i = start.line; i <= end.line; i++) {
|
|
239
|
+
lines.add(i);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const count = s[parseInt(idx)];
|
|
244
|
+
if (count > 0 && stmtInfo) {
|
|
245
|
+
const start = stmtInfo.start;
|
|
246
|
+
const end = stmtInfo.end;
|
|
247
|
+
if (start && end) {
|
|
248
|
+
for (let i = start.line; i <= end.line; i++) {
|
|
249
|
+
coveredLines.add(i);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Calculate branch coverage
|
|
255
|
+
let totalBranches = 0;
|
|
256
|
+
let coveredBranches = 0;
|
|
257
|
+
const branchesByLine = {};
|
|
258
|
+
for (const [idx, branches] of Object.entries(b)) {
|
|
259
|
+
if (Array.isArray(branches)) {
|
|
260
|
+
const branchInfo = branchMap[parseInt(idx)];
|
|
261
|
+
const line = branchInfo?.line || 0;
|
|
262
|
+
totalBranches += branches.length;
|
|
263
|
+
coveredBranches += branches.filter((c) => c > 0).length;
|
|
264
|
+
branchesByLine[line] = branches.map((count, idx) => ({
|
|
265
|
+
index: idx,
|
|
266
|
+
count,
|
|
267
|
+
location: { start: { line, column: 0 }, end: { line, column: 0 } }
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Calculate function coverage
|
|
272
|
+
const totalFunctions = Object.keys(f).length;
|
|
273
|
+
const coveredFunctions = Object.values(f).filter((c) => typeof c === 'number' && c > 0).length;
|
|
274
|
+
const totalLines = lines.size;
|
|
275
|
+
const coveredLinesCount = coveredLines.size;
|
|
276
|
+
const uncoveredLines = Array.from(lines).filter(l => !coveredLines.has(l));
|
|
277
|
+
return {
|
|
278
|
+
path: filePath,
|
|
279
|
+
totalLines,
|
|
280
|
+
coveredLines: coveredLinesCount,
|
|
281
|
+
lineCoverage: s.pct || 0,
|
|
282
|
+
totalBranches,
|
|
283
|
+
coveredBranches,
|
|
284
|
+
branchCoverage: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0,
|
|
285
|
+
totalFunctions,
|
|
286
|
+
coveredFunctions,
|
|
287
|
+
functionCoverage: totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0,
|
|
288
|
+
totalStatements: s.total || 0,
|
|
289
|
+
coveredStatements: s.covered || 0,
|
|
290
|
+
statementCoverage: s.pct || 0,
|
|
291
|
+
uncoveredLines,
|
|
292
|
+
partiallyCoveredLines: [],
|
|
293
|
+
branchesByLine
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Convert Istanbul coverage data to FileCoverage
|
|
298
|
+
*/
|
|
299
|
+
convertIstanbulToFileCoverage(filePath, data) {
|
|
300
|
+
return this.convertVitestToFileCoverage(filePath, data);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Calculate aggregate metrics from file coverage data
|
|
304
|
+
*/
|
|
305
|
+
calculateMetrics(files) {
|
|
306
|
+
const fileArray = Object.values(files);
|
|
307
|
+
const threshold = this.config.thresholds.line || 80;
|
|
308
|
+
if (fileArray.length === 0) {
|
|
309
|
+
return {
|
|
310
|
+
lineCoverage: 0,
|
|
311
|
+
branchCoverage: 0,
|
|
312
|
+
functionCoverage: 0,
|
|
313
|
+
statementCoverage: 0,
|
|
314
|
+
totalFiles: 0,
|
|
315
|
+
filesWithCoverage: 0,
|
|
316
|
+
filesMeetingThreshold: 0,
|
|
317
|
+
totalLines: 0,
|
|
318
|
+
coveredLines: 0,
|
|
319
|
+
timestamp: Date.now()
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
let totalLines = 0;
|
|
323
|
+
let coveredLines = 0;
|
|
324
|
+
let totalBranches = 0;
|
|
325
|
+
let coveredBranches = 0;
|
|
326
|
+
let totalFunctions = 0;
|
|
327
|
+
let coveredFunctions = 0;
|
|
328
|
+
let totalStatements = 0;
|
|
329
|
+
let coveredStatements = 0;
|
|
330
|
+
let filesMeetingThreshold = 0;
|
|
331
|
+
for (const file of fileArray) {
|
|
332
|
+
totalLines += file.totalLines;
|
|
333
|
+
coveredLines += file.coveredLines;
|
|
334
|
+
totalBranches += file.totalBranches;
|
|
335
|
+
coveredBranches += file.coveredBranches;
|
|
336
|
+
totalFunctions += file.totalFunctions;
|
|
337
|
+
coveredFunctions += file.coveredFunctions;
|
|
338
|
+
totalStatements += file.totalStatements;
|
|
339
|
+
coveredStatements += file.coveredStatements;
|
|
340
|
+
if (file.lineCoverage >= threshold) {
|
|
341
|
+
filesMeetingThreshold++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
lineCoverage: totalLines > 0 ? (coveredLines / totalLines) * 100 : 0,
|
|
346
|
+
branchCoverage: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0,
|
|
347
|
+
functionCoverage: totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0,
|
|
348
|
+
statementCoverage: totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0,
|
|
349
|
+
totalFiles: fileArray.length,
|
|
350
|
+
filesWithCoverage: fileArray.filter(f => f.totalLines > 0).length,
|
|
351
|
+
filesMeetingThreshold,
|
|
352
|
+
totalLines,
|
|
353
|
+
coveredLines,
|
|
354
|
+
timestamp: Date.now()
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Detect coverage format from file path
|
|
359
|
+
*/
|
|
360
|
+
detectFormat(filePath) {
|
|
361
|
+
if (filePath.endsWith('.json')) {
|
|
362
|
+
return 'json';
|
|
363
|
+
}
|
|
364
|
+
if (filePath.endsWith('.lcov') || filePath.endsWith('.info')) {
|
|
365
|
+
return 'lcov';
|
|
366
|
+
}
|
|
367
|
+
if (filePath.includes('coverage') && filePath.endsWith('.json')) {
|
|
368
|
+
return 'istanbul';
|
|
369
|
+
}
|
|
370
|
+
return 'unknown';
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Filter files based on include/exclude patterns
|
|
374
|
+
*/
|
|
375
|
+
filterFiles(files) {
|
|
376
|
+
const filtered = {};
|
|
377
|
+
const isIncluded = (path) => {
|
|
378
|
+
// Check exclude patterns first
|
|
379
|
+
for (const pattern of this.config.exclude) {
|
|
380
|
+
if (this.matchPattern(path, pattern)) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Check include patterns
|
|
385
|
+
if (this.config.include.length === 0) {
|
|
386
|
+
return true; // No include filters means all
|
|
387
|
+
}
|
|
388
|
+
for (const pattern of this.config.include) {
|
|
389
|
+
if (this.matchPattern(path, pattern)) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
};
|
|
395
|
+
for (const [path, coverage] of Object.entries(files)) {
|
|
396
|
+
if (isIncluded(path)) {
|
|
397
|
+
filtered[path] = coverage;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return filtered;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Match path against glob pattern
|
|
404
|
+
*/
|
|
405
|
+
matchPattern(path, pattern) {
|
|
406
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.').replace(/\//g, '\\/') + '$');
|
|
407
|
+
return regex.test(path);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Merge multiple coverage results
|
|
411
|
+
*/
|
|
412
|
+
mergeCoverageResults(results) {
|
|
413
|
+
const mergedFiles = {};
|
|
414
|
+
for (const result of results) {
|
|
415
|
+
for (const [path, file] of Object.entries(result.files)) {
|
|
416
|
+
if (!mergedFiles[path]) {
|
|
417
|
+
mergedFiles[path] = { ...file };
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
// Merge coverage data
|
|
421
|
+
const existing = mergedFiles[path];
|
|
422
|
+
// For lines, take the max execution count
|
|
423
|
+
const uncoveredLines = new Set([
|
|
424
|
+
...existing.uncoveredLines,
|
|
425
|
+
...file.uncoveredLines
|
|
426
|
+
]);
|
|
427
|
+
// Update totals (approximate merging)
|
|
428
|
+
mergedFiles[path] = {
|
|
429
|
+
...existing,
|
|
430
|
+
uncoveredLines: Array.from(uncoveredLines),
|
|
431
|
+
lineCoverage: Math.max(existing.lineCoverage, file.lineCoverage),
|
|
432
|
+
branchCoverage: Math.max(existing.branchCoverage, file.branchCoverage),
|
|
433
|
+
functionCoverage: Math.max(existing.functionCoverage, file.functionCoverage)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
source: 'custom',
|
|
440
|
+
gate: 'merged',
|
|
441
|
+
files: mergedFiles,
|
|
442
|
+
metrics: this.calculateMetrics(mergedFiles),
|
|
443
|
+
format: 'json',
|
|
444
|
+
timestamp: Date.now()
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Update configuration
|
|
449
|
+
*/
|
|
450
|
+
updateConfig(updates) {
|
|
451
|
+
this.config = { ...this.config, ...updates };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Create a coverage collector with default config
|
|
456
|
+
*/
|
|
457
|
+
export function createCoverageCollector(config) {
|
|
458
|
+
return new CoverageCollector(config);
|
|
459
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage Configuration
|
|
3
|
+
*
|
|
4
|
+
* Default configuration and factory functions for coverage analytics.
|
|
5
|
+
*/
|
|
6
|
+
import type { CoverageConfig, CoverageThreshold } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create default coverage configuration
|
|
9
|
+
*/
|
|
10
|
+
export declare function createDefaultCoverageConfig(): CoverageConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Create strict coverage configuration (enterprise/production)
|
|
13
|
+
*/
|
|
14
|
+
export declare function createStrictCoverageConfig(): CoverageConfig;
|
|
15
|
+
/**
|
|
16
|
+
* Create relaxed coverage configuration (development/prototyping)
|
|
17
|
+
*/
|
|
18
|
+
export declare function createRelaxedCoverageConfig(): CoverageConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Parse coverage threshold from string format
|
|
21
|
+
* Supported formats:
|
|
22
|
+
* - "80" (line coverage only)
|
|
23
|
+
* - "line:80,branch:70"
|
|
24
|
+
* - "{line:80,branch:70,function:75}"
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseCoverageThreshold(input: string): CoverageThreshold;
|
|
27
|
+
/**
|
|
28
|
+
* Format coverage threshold as string
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatCoverageThreshold(threshold: CoverageThreshold): string;
|
|
31
|
+
/**
|
|
32
|
+
* Validate coverage configuration
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateCoverageConfig(config: CoverageConfig): {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
errors: string[];
|
|
37
|
+
};
|