Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/core/coverageProcessor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import test from "ava";
import {
extractLineCoverage,
filterScriptUrl,
takeAndProcessSnapshot,
V8CoverageData,
Expand Down Expand Up @@ -216,3 +217,93 @@ class Example {}
t.truthy(ast);
t.is(ast.type, "File");
});

// --- extractLineCoverage ---

test("extractLineCoverage: expands multi-line statements to all lines", (t) => {
// res.json({ ... }) spanning lines 14-20, covered
const statementMap = {
"0": { start: { line: 14, column: 2 }, end: { line: 20, column: 4 } },
};
const statementCounts = { "0": 1 };

const lines = extractLineCoverage(statementMap, statementCounts);

t.is(lines["14"], 1);
t.is(lines["15"], 1);
t.is(lines["16"], 1);
t.is(lines["17"], 1);
t.is(lines["18"], 1);
t.is(lines["19"], 1);
t.is(lines["20"], 1);
});

test("extractLineCoverage: single-line statement works", (t) => {
const statementMap = {
"0": { start: { line: 5, column: 0 }, end: { line: 5, column: 30 } },
};
const statementCounts = { "0": 3 };

const lines = extractLineCoverage(statementMap, statementCounts);

t.is(lines["5"], 3);
t.false("4" in lines);
t.false("6" in lines);
});

test("extractLineCoverage: inner uncovered statement overrides outer covered statement", (t) => {
// Simulates: try { ... } catch (error) { res.status(500).json({...}) }
// Outer try-catch block (lines 25-40) is covered (count=1)
// Inner catch body (lines 36-39) is uncovered (count=0)
const statementMap = {
"0": { start: { line: 25, column: 0 }, end: { line: 40, column: 1 } },
"1": { start: { line: 26, column: 4 }, end: { line: 27, column: 5 } },
"2": { start: { line: 36, column: 4 }, end: { line: 39, column: 5 } },
};
const statementCounts = {
"0": 1, // try-catch executed
"1": 1, // try body executed
"2": 0, // catch body NOT executed
};

const lines = extractLineCoverage(statementMap, statementCounts);

// Try body lines should be covered
t.is(lines["26"], 1);
t.is(lines["27"], 1);

// Catch body lines should be uncovered (inner statement overrides outer)
t.is(lines["36"], 0);
t.is(lines["37"], 0);
t.is(lines["38"], 0);
t.is(lines["39"], 0);

// Outer try-catch structural lines are covered
t.is(lines["25"], 1);
t.is(lines["40"], 1);
});

test("extractLineCoverage: same-size statements use max count (covered wins)", (t) => {
// Two single-line statements on the same line: if (x) foo(); else bar();
// foo() was called (count=1), bar() was not (count=0)
const statementMap = {
"0": { start: { line: 10, column: 0 }, end: { line: 10, column: 40 } },
"1": { start: { line: 10, column: 7 }, end: { line: 10, column: 20 } },
"2": { start: { line: 10, column: 26 }, end: { line: 10, column: 40 } },
};
const statementCounts = {
"0": 1, // if statement executed
"1": 1, // foo() called
"2": 0, // bar() not called
};

const lines = extractLineCoverage(statementMap, statementCounts);

// Line 10 should be covered because at least one statement on it was executed
t.is(lines["10"], 1);
});

test("extractLineCoverage: handles empty statement map", (t) => {
const lines = extractLineCoverage({}, {});
t.deepEqual(lines, {});
});
67 changes: 57 additions & 10 deletions src/core/coverageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,60 @@ export interface V8CoverageData {
result: V8ScriptCoverage[];
}

interface IstanbulStatementMap {
start: { line: number; column: number };
end: { line: number; column: number };
}

/**
* Extract per-line execution counts from Istanbul's statement map.
*
* Multi-line statements (e.g. res.json({...}) spanning lines 14-20) are
* expanded to all lines in the range. When statements overlap (e.g. a
* try-catch block contains a catch body), the innermost (most specific)
* statement wins — so the catch body stays uncovered even though the
* outer try-catch block is covered.
*/
export function extractLineCoverage(
statementMap: Record<string, IstanbulStatementMap>,
statementCounts: Record<string, number>,
): Record<string, number> {
const stmtEntries = Object.entries(statementCounts)
.map(([stmtId, count]) => ({
stmtMap: statementMap[stmtId],
count,
}))
.filter((e) => e.stmtMap)
.sort((a, b) => {
const sizeA = a.stmtMap.end.line - a.stmtMap.start.line;
const sizeB = b.stmtMap.end.line - b.stmtMap.start.line;
return sizeB - sizeA; // largest first
});

const lines: Record<string, number> = {};
// Track the size of the statement that last wrote each line, so
// same-size statements use Math.max (preserving covered status)
// while strictly smaller statements override (inner wins over outer).
const lineStmtSize: Record<string, number> = {};
for (const { stmtMap, count } of stmtEntries) {
const size = stmtMap.end.line - stmtMap.start.line;
for (let lineNum = stmtMap.start.line; lineNum <= stmtMap.end.line; lineNum++) {
const key = String(lineNum);
const prevSize = lineStmtSize[key];
if (prevSize === undefined || size < prevSize) {
// Strictly smaller (more specific) statement — override
lines[key] = count;
lineStmtSize[key] = size;
} else if (size === prevSize) {
// Same size — take max (covered wins)
lines[key] = Math.max(lines[key] ?? 0, count);
}
// size > prevSize: larger statement, skip — a more specific one already wrote this line
}
}
return lines;
}

/**
* Filter a script URL to determine if it's a user source file.
*/
Expand Down Expand Up @@ -286,17 +340,10 @@ export async function processV8CoverageFile(
if (!fileKey) continue;
const fileCov = istanbulData[fileKey];

// Extract line coverage from Istanbul statement map
const lines: Record<string, number> = {};
for (const [stmtId, count] of Object.entries(
const lines = extractLineCoverage(
fileCov.statementMap,
fileCov.s as Record<string, number>,
)) {
const stmtMap = fileCov.statementMap[stmtId];
if (stmtMap) {
const line = String(stmtMap.start.line);
lines[line] = Math.max(lines[line] ?? 0, count);
}
}
);

// Extract branch coverage from Istanbul branch map
let totalBranches = 0;
Expand Down
Loading