Skip to content

Commit 481f515

Browse files
authored
Handle multiple coverage files (#483)
* use coverage filename in cache keys * merge duplicate coverage sections * revert node example * simplify test * refactor * cleanup
1 parent be757ea commit 481f515

File tree

2 files changed

+307
-79
lines changed

2 files changed

+307
-79
lines changed

src/files/coverageparser.ts

Lines changed: 204 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import {parseContent as parseContentClover} from "@cvrg-report/clover-json";
2-
import {parseContent as parseContentCobertura} from "cobertura-parse";
3-
import {parseContent as parseContentJacoco} from "@7sean68/jacoco-parse";
4-
import {Section, source} from "lcov-parse";
5-
import {OutputChannel} from "vscode";
1+
import { parseContent as parseContentClover } from "@cvrg-report/clover-json";
2+
import { parseContent as parseContentCobertura } from "cobertura-parse";
3+
import { parseContent as parseContentJacoco } from "@7sean68/jacoco-parse";
4+
import { Section, source } from "lcov-parse";
5+
import { OutputChannel } from "vscode";
66

7-
import {CoverageFile, CoverageType} from "./coveragefile";
7+
import { CoverageFile, CoverageType } from "./coveragefile";
88

99
export class CoverageParser {
1010
private outputChannel: OutputChannel;
@@ -17,147 +17,289 @@ export class CoverageParser {
1717
* Extracts coverage sections of type xml and lcov
1818
* @param files array of coverage files in string format
1919
*/
20-
public async filesToSections(files: Map<string, string>): Promise<Map<string, Section>> {
21-
let coverages = new Map<string, Section>();
22-
23-
for (const file of files) {
24-
const fileName = file[0];
25-
const fileContent = file[1];
26-
27-
// file is an array
28-
let coverage = new Map<string, Section>();
20+
public async filesToSections(
21+
files: Map<string, string>
22+
): Promise<Map<string, Section>> {
23+
const coverages = new Map<string, Section>();
2924

25+
for (const [fileName, fileContent] of files) {
3026
// get coverage file type
3127
const coverageFile = new CoverageFile(fileContent);
3228
switch (coverageFile.type) {
3329
case CoverageType.CLOVER:
34-
coverage = await this.xmlExtractClover(fileName, fileContent);
30+
await this.xmlExtractClover(
31+
coverages,
32+
fileName,
33+
fileContent
34+
);
3535
break;
3636
case CoverageType.JACOCO:
37-
coverage = await this.xmlExtractJacoco(fileName, fileContent);
37+
await this.xmlExtractJacoco(
38+
coverages,
39+
fileName,
40+
fileContent
41+
);
3842
break;
3943
case CoverageType.COBERTURA:
40-
coverage = await this.xmlExtractCobertura(fileName, fileContent);
44+
await this.xmlExtractCobertura(
45+
coverages,
46+
fileName,
47+
fileContent
48+
);
4149
break;
4250
case CoverageType.LCOV:
43-
coverage = await this.lcovExtract(fileName, fileContent);
51+
this.lcovExtract(coverages, fileName, fileContent);
4452
break;
4553
default:
4654
break;
4755
}
48-
49-
// add new coverage map to existing coverages generated so far
50-
coverages = new Map([...coverages, ...coverage]);
5156
}
52-
5357
return coverages;
5458
}
5559

56-
private async convertSectionsToMap(
57-
data: Section[],
58-
): Promise<Map<string, Section>> {
59-
const sections = new Map<string, Section>();
60+
private async addSections(
61+
coverages: Map<string, Section>,
62+
data: Section[]
63+
): Promise<void[]> {
6064
const addToSectionsMap = async (section: Section) => {
61-
sections.set(section.title + "::" + section.file, section);
65+
const key = [section.title, section.file].join("::");
66+
const existingSection = coverages.get(key);
67+
68+
if (!existingSection) {
69+
coverages.set(key, section);
70+
return;
71+
}
72+
73+
const mergedSection = this.mergeSections(existingSection, section);
74+
coverages.set(key, mergedSection);
6275
};
6376

6477
// convert the array of sections into an unique map
6578
const addPromises = data.map(addToSectionsMap);
66-
await Promise.all(addPromises);
67-
return sections;
79+
return await Promise.all(addPromises);
6880
}
6981

70-
private xmlExtractCobertura(filename: string, xmlFile: string) {
71-
return new Promise<Map<string, Section>>((resolve) => {
82+
private xmlExtractCobertura(
83+
coverages: Map<string, Section>,
84+
coverageFilename: string,
85+
xmlFile: string
86+
) {
87+
return new Promise<void>((resolve) => {
7288
const checkError = (err: Error) => {
7389
if (err) {
74-
err.message = `filename: ${filename} ${err.message}`;
90+
err.message = `filename: ${coverageFilename} ${err.message}`;
7591
this.handleError("cobertura-parse", err);
76-
return resolve(new Map<string, Section>());
92+
return resolve();
7793
}
7894
};
7995

8096
try {
81-
parseContentCobertura(xmlFile, async (err, data) => {
82-
checkError(err);
83-
const sections = await this.convertSectionsToMap(data);
84-
return resolve(sections);
85-
}, true);
86-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
97+
parseContentCobertura(
98+
xmlFile,
99+
async (err, data) => {
100+
checkError(err);
101+
await this.addSections(coverages, data);
102+
return resolve();
103+
},
104+
true
105+
);
106+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
87107
} catch (error: any) {
88108
checkError(error);
89109
}
90110
});
91111
}
92112

93-
private xmlExtractJacoco(filename: string, xmlFile: string) {
94-
return new Promise<Map<string, Section>>((resolve) => {
113+
private xmlExtractJacoco(
114+
coverages: Map<string, Section>,
115+
coverageFilename: string,
116+
xmlFile: string
117+
) {
118+
return new Promise<void>((resolve) => {
95119
const checkError = (err: Error) => {
96120
if (err) {
97-
err.message = `filename: ${filename} ${err.message}`;
121+
err.message = `filename: ${coverageFilename} ${err.message}`;
98122
this.handleError("jacoco-parse", err);
99-
return resolve(new Map<string, Section>());
123+
return resolve();
100124
}
101125
};
102126

103127
try {
104128
parseContentJacoco(xmlFile, async (err, data) => {
105129
checkError(err);
106-
const sections = await this.convertSectionsToMap(data);
107-
return resolve(sections);
130+
await this.addSections(coverages, data);
131+
return resolve();
108132
});
109-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
133+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
110134
} catch (error: any) {
111135
checkError(error);
112136
}
113137
});
114138
}
115139

116-
private async xmlExtractClover(filename: string, xmlFile: string) {
140+
private async xmlExtractClover(
141+
coverages: Map<string, Section>,
142+
coverageFilename: string,
143+
xmlFile: string
144+
) {
117145
try {
118146
const data = await parseContentClover(xmlFile);
119-
const sections = await this.convertSectionsToMap(data);
120-
return sections;
121-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
147+
await this.addSections(coverages, data);
148+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
122149
} catch (error: any) {
123-
error.message = `filename: ${filename} ${error.message}`;
150+
error.message = `filename: ${coverageFilename} ${error.message}`;
124151
this.handleError("clover-parse", error);
125-
return new Map<string, Section>();
126152
}
127153
}
128154

129-
private lcovExtract(filename: string, lcovFile: string) {
130-
return new Promise<Map<string, Section>>((resolve) => {
155+
private lcovExtract(
156+
coverages: Map<string, Section>,
157+
coverageFilename: string,
158+
lcovFile: string
159+
) {
160+
return new Promise<void>((resolve) => {
131161
const checkError = (err: Error) => {
132162
if (err) {
133-
err.message = `filename: ${filename} ${err.message}`;
163+
err.message = `filename: ${coverageFilename} ${err.message}`;
134164
this.handleError("lcov-parse", err);
135-
return resolve(new Map<string, Section>());
165+
return resolve();
136166
}
137167
};
138168

139169
try {
140170
source(lcovFile, async (err, data) => {
141171
checkError(err);
142-
const sections = await this.convertSectionsToMap(data);
143-
return resolve(sections);
172+
await this.addSections(coverages, data);
173+
return resolve();
144174
});
145-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
175+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
146176
} catch (error: any) {
147177
checkError(error);
148178
}
149179
});
150180
}
151181

182+
private mergeSections(existingSection: Section, section: Section): Section {
183+
const lines = this.mergeLineCoverage(existingSection, section);
184+
const branches = this.mergeBranchCoverage(existingSection, section);
185+
186+
return {
187+
...existingSection,
188+
lines,
189+
branches,
190+
};
191+
}
192+
193+
private mergeLineCoverage(
194+
existingCoverage: Section,
195+
coverage: Section
196+
): Section["lines"] {
197+
let hit = 0;
198+
let found = 0;
199+
const seen = new Set();
200+
const hits = new Set(
201+
coverage.lines.details
202+
.filter(({ hit }) => hit > 0)
203+
.map(({ line }) => line)
204+
);
205+
206+
const details = existingCoverage.lines.details.map((line) => {
207+
found += 1;
208+
seen.add(line.line);
209+
210+
if (hits.has(line.line)) {
211+
line.hit += 1;
212+
}
213+
214+
if (line.hit > 0) {
215+
hit += 1;
216+
}
217+
218+
return line;
219+
});
220+
221+
coverage.lines.details
222+
.filter(({ line }) => !seen.has(line))
223+
.map((line) => {
224+
found += 1;
225+
226+
if (line.hit > 0) {
227+
hit += 1;
228+
}
229+
230+
details.push(line);
231+
});
232+
233+
return { details, hit, found };
234+
}
235+
236+
private mergeBranchCoverage(
237+
existingCoverage: Section,
238+
coverage: Section
239+
): Section["branches"] {
240+
if (!coverage.branches) {
241+
return existingCoverage.branches;
242+
}
243+
if (!existingCoverage.branches) {
244+
return coverage.branches;
245+
}
246+
247+
let hit = 0;
248+
let found = 0;
249+
const seen = new Set();
250+
251+
const getKey = (branch: {
252+
line: number;
253+
block: number;
254+
branch: number;
255+
}) => [branch.line, branch.block, branch.branch].join(":");
256+
257+
const taken = new Set(
258+
coverage.branches.details
259+
.filter(({ taken }) => taken > 0)
260+
.map(getKey)
261+
);
262+
263+
const details = existingCoverage.branches.details.map((branch) => {
264+
const key = getKey(branch);
265+
found += 1;
266+
seen.add(key);
267+
268+
if (taken.has(key)) {
269+
branch.taken += 1;
270+
}
271+
272+
if (branch.taken > 0) {
273+
hit += 1;
274+
}
275+
276+
return branch;
277+
});
278+
279+
coverage.branches.details
280+
.filter((branch) => !seen.has(getKey(branch)))
281+
.map((branch) => {
282+
found += 1;
283+
284+
if (branch.taken > 0) {
285+
hit += 1;
286+
}
287+
288+
details.push(branch);
289+
});
290+
291+
return { details, hit, found };
292+
}
293+
152294
private handleError(system: string, error: Error) {
153295
const message = error.message ? error.message : error;
154296
const stackTrace = error.stack;
155297
this.outputChannel.appendLine(
156-
`[${Date.now()}][coverageparser][${system}]: Error: ${message}`,
298+
`[${Date.now()}][coverageparser][${system}]: Error: ${message}`
157299
);
158300
if (stackTrace) {
159301
this.outputChannel.appendLine(
160-
`[${Date.now()}][coverageparser][${system}]: Stacktrace: ${stackTrace}`,
302+
`[${Date.now()}][coverageparser][${system}]: Stacktrace: ${stackTrace}`
161303
);
162304
}
163305
}

0 commit comments

Comments
 (0)