diff --git a/src/test-explorer/lcov_parser.ts b/src/test-explorer/lcov_parser.ts index 44608883..2801bd31 100644 --- a/src/test-explorer/lcov_parser.ts +++ b/src/test-explorer/lcov_parser.ts @@ -48,6 +48,8 @@ export function parseLcov( class FileCoverageInfo { functionsByLine: Map = new Map(); lineCoverage: Map = new Map(); + coverageByLineAndBranch: Map> = + new Map(); } const infosByFile: Map = new Map(); for (const block of lcov.split(/end_of_record(\n|$)/)) { @@ -191,12 +193,54 @@ export function parseLcov( // Ignored. Reconstructed from DA entries break; case "BRDA": { - // branch coverage: ,[],, + // branch coverage: ,[],, + // Note that the might contain commas, which requires being + // a bit careful while parsing. const match = value.match(/(\d+),(e?)(\d+),(.+)/); if (!match) { throw new Error(`Invalid FNDA entry`); } - // TODO: Add support for branch coverage + const lineNumber = Number.parseInt(match[1], 10) - 1; + if (lineNumber < 0) { + throw new Error("Negative line number in DA entry"); + } + const isException = match[2] === "e"; + const blockId = Number.parseInt(match[3], 10); + const rest = match[4]; + const commaOffset = rest.lastIndexOf(","); + if (commaOffset === undefined) { + throw new Error(`Invalid FNDA entry`); + } + const label = rest.substring(0, commaOffset); + const hitCountStr = rest.substring(commaOffset + 1); + const hitCount = + hitCountStr === "-" ? 0 : Number.parseInt(hitCountStr, 10); + if (hitCount < 0) { + throw new Error("Negative hit count in DA entry"); + } + + if (info === undefined) { + throw new Error(`Missing filename`); + } + + // We don't want to display coverage for exception edges. + if (isException) break; + + // Insert into `branchByLineAndBranch` + if (!info.coverageByLineAndBranch.has(lineNumber)) { + info.coverageByLineAndBranch.set(lineNumber, new Map()); + } + const coverageByBranch = info.coverageByLineAndBranch.get(lineNumber); + const branchId = `${blockId}:${label}`; + if (!coverageByBranch.has(branchId)) { + coverageByBranch.set( + branchId, + new vscode.BranchCoverage(0, undefined, label), + ); + } + const branchCoverage = coverageByBranch.get(branchId); + assert(typeof branchCoverage.executed == "number"); + branchCoverage.executed += hitCount; break; } case "BRF": // branches found @@ -216,7 +260,16 @@ export function parseLcov( Array.from(info.functionsByLine.values()), ); detailedCoverage = detailedCoverage.concat( - Array.from(info.lineCoverage.values()), + Array.from(info.lineCoverage.values()).map((c) => { + assert("line" in c.location); + const branchCoverage = info.coverageByLineAndBranch.get( + c.location.line, + ); + if (branchCoverage) { + c.branches = Array.from(branchCoverage.values()); + } + return c; + }), ); fileCoverages.push( BazelFileCoverage.fromDetails( diff --git a/test/lcov_parser.test.ts b/test/lcov_parser.test.ts index 15c3cf92..642d9407 100644 --- a/test/lcov_parser.test.ts +++ b/test/lcov_parser.test.ts @@ -89,7 +89,9 @@ describe("The lcov parser", () => { assert.strictEqual(fileCov.statementCoverage.covered, 11); }); it("branch coverage", () => { - assert(fileCov.branchCoverage === undefined); + assert(fileCov.branchCoverage !== undefined); + assert.strictEqual(fileCov.branchCoverage.total, 6); + assert.strictEqual(fileCov.branchCoverage.covered, 3); }); it("function coverage details", () => { const initFunc = getFunctionByLine(fileCov, 24); @@ -109,6 +111,12 @@ describe("The lcov parser", () => { assert.equal(getLineCoverageForLine(fileCov, 38).executed, 0); assert.equal(getLineCoverageForLine(fileCov, 40).executed, 1); }); + it("branch coverage data", () => { + const branchCoverage = getLineCoverageForLine(fileCov, 37).branches; + assert.equal(branchCoverage.length, 2); + assert.equal(branchCoverage[0].executed, 1); + assert.equal(branchCoverage[1].executed, 0); + }); }); describe("parses C++ coverage data", () => { @@ -128,7 +136,9 @@ describe("The lcov parser", () => { assert.strictEqual(fileCov.statementCoverage.covered, 505); }); it("branch coverage", () => { - assert(fileCov.branchCoverage === undefined); + assert(fileCov.branchCoverage !== undefined); + assert.strictEqual(fileCov.branchCoverage.total, 2560); + assert.strictEqual(fileCov.branchCoverage.covered, 843); }); it("function coverage details", () => { const initFunc = getFunctionByLine(fileCov, 71); @@ -141,6 +151,14 @@ describe("The lcov parser", () => { assert.equal(getLineCoverageForLine(fileCov, 178).executed, 0); assert.equal(getLineCoverageForLine(fileCov, 193).executed, 4); }); + it("branch coverage data", () => { + const branchCoverage = getLineCoverageForLine(fileCov, 479).branches; + assert.equal(branchCoverage.length, 2); + assert.equal(branchCoverage[0].executed, 1); + assert.equal(branchCoverage[1].executed, 0); + const branchCoverage2 = getLineCoverageForLine(fileCov, 481).branches; + assert.equal(branchCoverage2.length, 12); + }); }); describe("parses Rust coverage data", () => { @@ -161,6 +179,8 @@ describe("The lcov parser", () => { assert.strictEqual(fileCov.statementCoverage.covered, 426); }); it("branch coverage", () => { + // Rust has no branch coverage data, as of writing this test case. + // Also see https://github.com/rust-lang/rust/issues/79649 assert(fileCov.branchCoverage === undefined); }); it("function coverage details", () => { @@ -194,6 +214,7 @@ describe("The lcov parser", () => { assert.strictEqual(fileCov.statementCoverage.covered, 133); }); it("branch coverage", () => { + // Go has no branch coverage data. assert(fileCov.branchCoverage === undefined); }); it("line coverage details", () => {