Skip to content

Commit 645be98

Browse files
committed
Support branch coverage
Branch coverage parsing is only implemented for the xml coverage format.
1 parent 0e5d939 commit 645be98

File tree

2 files changed

+89
-28
lines changed

2 files changed

+89
-28
lines changed

src/parse.js

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,8 @@ const toTable = (data, options, dataFromXml = null) => {
217217
return null;
218218
}
219219
const totalLine = dataFromXml ? dataFromXml.total : getTotal(data);
220-
options.hasMissing = coverage.some((c) => c.missing);
220+
options.hasMissing = totalLine.miss > 0;
221+
options.hasBranches = totalLine.branches > 0;
221222

222223
core.info(`Generating coverage report`);
223224
const headTr = toHeadRow(options);
@@ -264,28 +265,38 @@ const toTable = (data, options, dataFromXml = null) => {
264265

265266
// make html head row - th
266267
const toHeadRow = (options) => {
268+
const branchTd = options.hasBranches ? '<th>Branch</th><th>BrPart</th>' : '';
267269
const lastTd = options.hasMissing ? '<th>Missing</th>' : '';
268270

269-
return `<tr><th>File</th><th>Stmts</th><th>Miss</th><th>Cover</th>${lastTd}</tr>`;
271+
return `<tr><th>File</th><th>Stmts</th><th>Miss</th>${branchTd}<th>Cover</th>${lastTd}</tr>`;
270272
};
271273

272274
// make html row - tr
273275
const toRow = (item, indent = false, options) => {
274-
const { stmts, miss, cover } = item;
276+
const { stmts, miss, branches, partBranches, cover } = item;
275277

276278
const name = toFileNameTd(item, indent, options);
277279
const missing = toMissingTd(item, options);
280+
const branchTd = options.hasBranches
281+
? `<td>${branches}</td><td>${partBranches}</td>`
282+
: '';
278283
const lastTd = options.hasMissing ? `<td>${missing}</td>` : '';
279284

280-
return `<tr><td>${name}</td><td>${stmts}</td><td>${miss}</td><td>${cover}</td>${lastTd}</tr>`;
285+
return `<tr><td>${name}</td><td>${stmts}</td><td>${miss}</td>${branchTd}<td>${cover}</td>${lastTd}</tr>`;
281286
};
282287

283288
// make summary row - tr
284289
const toTotalRow = (item, options) => {
285-
const { name, stmts, miss, cover } = item;
290+
const { name, stmts, miss, branches, partBranches, cover } = item;
291+
const branchTd = options.hasBranches
292+
? `<td><b>${branches}</b></td><td><b>${partBranches}</b></td>`
293+
: '';
286294
const emptyCell = options.hasMissing ? '<td>&nbsp;</td>' : '';
287295

288-
return `<tr><td><b>${name}</b></td><td><b>${stmts}</b></td><td><b>${miss}</b></td><td><b>${cover}</b></td>${emptyCell}</tr>`;
296+
return `
297+
<tr><td><b>${name}</b></td><td><b>${stmts}</b></td><td><b>${miss}</b></td>
298+
${branchTd}<td><b>${cover}</b></td>${emptyCell}</tr>
299+
`;
289300
};
290301

291302
// make fileName cell - td
@@ -305,7 +316,8 @@ const toFolderTd = (path, options) => {
305316
return '';
306317
}
307318

308-
const colspan = options.hasMissing ? 5 : 4;
319+
const colspan =
320+
4 + (options.hasBranches ? 2 : 0) + (options.hasMissing ? 1 : 0);
309321
return `<tr><td colspan="${colspan}"><b>${path}</b></td></tr>`;
310322
};
311323

@@ -317,11 +329,13 @@ const toMissingTd = (item, options) => {
317329

318330
return item.missing
319331
.map((range) => {
320-
const [start, end = start] = range.split('-');
321-
const fragment = start === end ? `L${start}` : `L${start}-L${end}`;
332+
const isBranch = range.includes('->');
333+
const [start, end] = isBranch ? range.split('->') : range.split('-');
334+
const fragment = end === undefined ? `L${start}` : `L${start}-L${end}`;
322335
const relative = item.name;
323336
const href = `${options.repoUrl}/blob/${options.commit}/${options.pathPrefix}${relative}#${fragment}`;
324-
const text = start === end ? start : `${start}&ndash;${end}`;
337+
const sign = isBranch ? '&rarrc;' : '&ndash;';
338+
const text = end === undefined ? start : `${start}${sign}${end}`;
325339

326340
return `<a href="${href}">${text}</a>`;
327341
})

src/parseXml.js

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,37 @@ const getParsedXml = (options) => {
1414
return '';
1515
};
1616

17+
const coveragePct = (num, denom) => {
18+
// follow the same logic as coverage.py: only return 0 when there are no hits,
19+
// only return 100% when all lines are covered, otherwise round.
20+
// https://github.com/nedbat/coveragepy/blob/0957c07064f8cd89a2a81a0ff1e51ca4bab77c69/coverage/results.py#L276
21+
if (num === denom) return '100%';
22+
if (num === 0) return '0%';
23+
const cover = Math.max(1, Math.min(99, Math.round((100 * num) / denom)));
24+
return `${cover}%`;
25+
};
26+
1727
const getTotalCoverage = (parsedXml) => {
1828
if (!parsedXml) {
1929
return null;
2030
}
2131

2232
const coverage = parsedXml['$'];
23-
const cover = parseInt(parseFloat(coverage['line-rate']) * 100);
2433
const linesValid = parseInt(coverage['lines-valid']);
2534
const linesCovered = parseInt(coverage['lines-covered']);
35+
const branchesValid = parseInt(coverage['branches-valid']);
36+
const branchesCovered = parseInt(coverage['branches-covered']);
2637

2738
return {
2839
name: 'TOTAL',
2940
stmts: linesValid,
3041
miss: linesValid - linesCovered,
31-
cover: cover !== '0' ? `${cover}%` : '0',
42+
branches: branchesValid,
43+
partBranches: branchesValid - branchesCovered,
44+
cover: coveragePct(
45+
linesCovered + branchesCovered,
46+
linesValid + branchesValid,
47+
),
3248
};
3349
};
3450

@@ -51,8 +67,6 @@ const getCoverageXmlReport = (options) => {
5167
try {
5268
const parsedXml = getParsedXml(options);
5369

54-
const coverage = getTotalCoverage(parsedXml);
55-
// const coverage = getCoverageReportXml(getContent(options.covXmlFile));
5670
const isValid = isValidCoverageContent(parsedXml);
5771

5872
if (parsedXml && !isValid) {
@@ -62,6 +76,7 @@ const getCoverageXmlReport = (options) => {
6276

6377
if (parsedXml && isValid) {
6478
const coverageObj = coverageXmlToFiles(parsedXml);
79+
const coverage = getTotalCoverage(parsedXml);
6580
const dataFromXml = { coverage: coverageObj, total: coverage };
6681
const html = toHtml(null, options, dataFromXml);
6782
const color = getCoverageColor(coverage ? coverage.cover : '0');
@@ -126,32 +141,61 @@ const parseClass = (classObj) => {
126141
return null;
127142
}
128143

129-
const { stmts, missing, totalMissing: miss } = parseLines(classObj.lines);
130-
const { filename: name, 'line-rate': lineRate } = classObj['$'];
131-
132-
const isFullCoverage = lineRate === '1';
133-
const cover = isFullCoverage
134-
? '100%'
135-
: `${parseInt(parseFloat(lineRate) * 100)}%`;
144+
const { stmts, missingStmts, branches, partBranches, missing } = parseLines(
145+
classObj.lines,
146+
);
147+
const { filename: name } = classObj['$'];
148+
const cover = coveragePct(
149+
stmts + branches - missingStmts - partBranches,
150+
stmts + branches,
151+
);
136152

137-
return { name, stmts, miss, cover, missing };
153+
return {
154+
name,
155+
stmts,
156+
miss: missingStmts,
157+
branches,
158+
partBranches,
159+
cover,
160+
missing,
161+
};
138162
};
139163

140164
const parseLines = (lines) => {
141165
if (!lines || !lines.length || !lines[0].line) {
142-
return { stmts: '0', missing: '', totalMissing: '0' };
166+
return {
167+
stmts: 0,
168+
missingStmts: 0,
169+
branches: 0,
170+
partBranches: 0,
171+
missing: [],
172+
};
143173
}
144174

145-
let stmts = 0;
146-
const missingLines = [];
175+
let stmts = 0,
176+
branches = 0;
177+
const missingLines = [],
178+
partBranchesText = [];
147179

148180
lines[0].line.forEach((line) => {
149181
stmts++;
150-
const { hits, number } = line['$'];
182+
const { hits, number, branch } = line['$'];
151183

152184
if (hits === '0') {
153185
missingLines.push(parseInt(number));
154186
}
187+
if (branch === 'true') {
188+
const { 'condition-coverage': cc, 'missing-branches': mb } = line['$'];
189+
// 'condition-coverage' is usually formatted like "50% (1/2)". Parse the total number
190+
// of targets (the second number in parentheses). Default to 2 if not available.
191+
const numTargets = cc && cc.match(/\(\d+\/(\d+)\)/);
192+
branches += numTargets ? +numTargets[1] : 2;
193+
if (mb) {
194+
for (const target of mb.split(',')) {
195+
partBranchesText.push(`${number}->${target}`);
196+
}
197+
}
198+
}
155199
});
156200

157201
const missing = missingLines.reduce((arr, val, i, a) => {
@@ -168,11 +212,14 @@ const parseLines = (lines) => {
168212
missingText.push(`${m[0]}-${m[m.length - 1]}`);
169213
}
170214
});
215+
missingText.push(...partBranchesText);
171216

172217
return {
173-
stmts: stmts.toString(),
218+
stmts,
219+
missingStmts: missingLines.length,
220+
branches,
221+
partBranches: partBranchesText.length,
174222
missing: missingText,
175-
totalMissing: missingLines.length.toString(),
176223
};
177224
};
178225

0 commit comments

Comments
 (0)