Skip to content

Commit 4332b2a

Browse files
authored
fix(plugin-coverage): merge multiple results for a file (#688)
1 parent 37b3283 commit 4332b2a

File tree

4 files changed

+485
-1
lines changed

4 files changed

+485
-1
lines changed

Diff for: packages/plugin-coverage/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
🧪 **Code PushUp plugin for tracking code coverage.** ☂️
88

99
This plugin allows you to measure and track code coverage on your project.
10+
It accepts the LCOV coverage format and merges coverage results from any test suites provided.
1011

1112
Measured coverage types are mapped to Code PushUp audits in the following way
1213

Diff for: packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { LCOVRecord } from 'parse-lcov';
33
import { AuditOutputs } from '@code-pushup/models';
44
import { exists, readTextFile, toUnixNewlines } from '@code-pushup/utils';
55
import { CoverageResult, CoverageType } from '../../config';
6+
import { mergeLcovResults } from './merge-lcov';
67
import { parseLcov } from './parse-lcov';
78
import {
89
lcovCoverageToAuditOutput,
@@ -26,9 +27,12 @@ export async function lcovResultsToAuditOutputs(
2627
// Parse lcov files
2728
const lcovResults = await parseLcovFiles(results);
2829

30+
// Merge multiple coverage reports for the same file
31+
const mergedResults = mergeLcovResults(lcovResults);
32+
2933
// Calculate code coverage from all coverage results
3034
const totalCoverageStats = getTotalCoverageFromLcovRecords(
31-
lcovResults,
35+
mergedResults,
3236
coverageTypes,
3337
);
3438

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import {
2+
BranchesDetails,
3+
FunctionsDetails,
4+
LCOVRecord,
5+
LinesDetails,
6+
} from 'parse-lcov';
7+
8+
export function mergeLcovResults(records: LCOVRecord[]): LCOVRecord[] {
9+
// Skip if there are no files with multiple records
10+
const allFilenames = records.map(record => record.file);
11+
if (allFilenames.length === new Set(allFilenames).size) {
12+
return records;
13+
}
14+
15+
return records.reduce<LCOVRecord[]>((accMerged, currRecord, currIndex) => {
16+
const filePath = currRecord.file;
17+
const lines = currRecord.lines.found;
18+
19+
const duplicates = records.reduce<[LCOVRecord, number][]>(
20+
(acc, candidateRecord, candidateIndex) => {
21+
if (
22+
candidateRecord.file === filePath &&
23+
candidateRecord.lines.found === lines &&
24+
candidateIndex !== currIndex
25+
) {
26+
return [...acc, [candidateRecord, candidateIndex]];
27+
}
28+
return acc;
29+
},
30+
[],
31+
);
32+
33+
// This is not the first time the record has been identified as a duplicate
34+
if (
35+
duplicates.map(duplicate => duplicate[1]).some(index => index < currIndex)
36+
) {
37+
return accMerged;
38+
}
39+
40+
// Unique record
41+
if (duplicates.length === 0) {
42+
return [...accMerged, currRecord];
43+
}
44+
45+
return [
46+
...accMerged,
47+
mergeDuplicateLcovRecords([
48+
currRecord,
49+
...duplicates.map(duplicate => duplicate[0]),
50+
]),
51+
];
52+
}, []);
53+
}
54+
55+
export function mergeDuplicateLcovRecords(records: LCOVRecord[]): LCOVRecord {
56+
const linesDetails = mergeLcovLineDetails(
57+
records.map(record => record.lines.details),
58+
);
59+
const linesHit = linesDetails.reduce(
60+
(acc, line) => acc + (line.hit > 0 ? 1 : 0),
61+
0,
62+
);
63+
64+
const branchesDetails = mergeLcovBranchesDetails(
65+
records.map(record => record.branches.details),
66+
);
67+
const branchesHit = branchesDetails.reduce(
68+
(acc, branch) => acc + (branch.taken > 0 ? 1 : 0),
69+
0,
70+
);
71+
72+
const functionsDetails = mergeLcovFunctionsDetails(
73+
records.map(record => record.functions.details),
74+
);
75+
76+
const functionsHit = functionsDetails.reduce(
77+
(acc, func) => acc + (func.hit != null && func.hit > 0 ? 1 : 0),
78+
0,
79+
);
80+
81+
const mergedRecord: LCOVRecord = {
82+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
83+
file: records[0]!.file,
84+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
85+
title: records[0]!.title,
86+
lines: {
87+
found: linesDetails.length,
88+
hit: linesHit,
89+
details: linesDetails,
90+
},
91+
branches: {
92+
found: branchesDetails.length,
93+
hit: branchesHit,
94+
details: branchesDetails,
95+
},
96+
functions: {
97+
found: functionsDetails.length,
98+
hit: functionsHit,
99+
details: functionsDetails,
100+
},
101+
};
102+
return mergedRecord;
103+
}
104+
105+
export function mergeLcovLineDetails(
106+
details: LinesDetails[][],
107+
): LinesDetails[] {
108+
const flatDetails = details.flat();
109+
110+
const uniqueLines = [
111+
...new Set(flatDetails.map(flatDetail => flatDetail.line)),
112+
];
113+
114+
return uniqueLines.map(line => {
115+
const hitSum = flatDetails
116+
.filter(lineDetail => lineDetail.line === line)
117+
.reduce((acc, lineDetail) => acc + lineDetail.hit, 0);
118+
119+
return { line, hit: hitSum };
120+
});
121+
}
122+
123+
export function mergeLcovBranchesDetails(
124+
details: BranchesDetails[][],
125+
): BranchesDetails[] {
126+
const flatDetails = details.flat();
127+
128+
const uniqueBranches = [
129+
...new Set(
130+
flatDetails.map(({ line, block, branch }) =>
131+
JSON.stringify({ line, block, branch }),
132+
),
133+
),
134+
].map(
135+
functionJSON =>
136+
JSON.parse(functionJSON) as Pick<
137+
BranchesDetails,
138+
'line' | 'block' | 'branch'
139+
>,
140+
);
141+
142+
return uniqueBranches.map(({ line, block, branch }) => {
143+
const takenSum = flatDetails
144+
.filter(
145+
branchDetail =>
146+
branchDetail.line === line &&
147+
branchDetail.block === block &&
148+
branchDetail.branch === branch,
149+
)
150+
.reduce((acc, branchDetail) => acc + branchDetail.taken, 0);
151+
152+
return { line, block, branch, taken: takenSum };
153+
});
154+
}
155+
156+
export function mergeLcovFunctionsDetails(
157+
details: FunctionsDetails[][],
158+
): FunctionsDetails[] {
159+
const flatDetails = details.flat();
160+
161+
const uniqueFunctions = [
162+
...new Set(
163+
flatDetails.map(({ line, name }) => JSON.stringify({ line, name })),
164+
),
165+
].map(
166+
functionJSON =>
167+
JSON.parse(functionJSON) as Pick<FunctionsDetails, 'line' | 'name'>,
168+
);
169+
170+
return uniqueFunctions.map(({ line, name }) => {
171+
const hitSum = flatDetails
172+
.filter(
173+
functionDetail =>
174+
functionDetail.line === line && functionDetail.name === name,
175+
)
176+
.reduce((acc, functionDetail) => acc + (functionDetail.hit ?? 0), 0);
177+
178+
return { line, name, hit: hitSum };
179+
});
180+
}

0 commit comments

Comments
 (0)