Skip to content

Commit

Permalink
Collect and merge function definitions from all runs in coverage mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
Stuk committed May 4, 2021
1 parent 50e9f1a commit 66f2437
Showing 1 changed file with 82 additions and 37 deletions.
119 changes: 82 additions & 37 deletions packages/test-runner-core/src/coverage/getTestCoverage.ts
Expand Up @@ -6,6 +6,7 @@ import {
BranchMapping,
Location,
Range,
FunctionMapping,
} from 'istanbul-lib-coverage';
import { TestSession } from '../test-session/TestSession';
import { CoverageConfig } from '../config/TestRunnerCoreConfig';
Expand All @@ -26,61 +27,105 @@ export interface TestCoverage {
const locEquals = (a: Location, b: Location) => a.column === b.column && a.line === b.line;
const rangeEquals = (a: Range, b: Range) => locEquals(a.start, b.start) && locEquals(a.end, b.end);

function findBranchKey(branches: Record<string, BranchMapping>, branch: BranchMapping) {
for (const [key, m] of Object.entries(branches)) {
if (rangeEquals(m.loc, branch.loc)) {
function findKey<T extends BranchMapping | FunctionMapping>(items: Record<string, T>, item: T) {
for (const [key, m] of Object.entries(items)) {
if (rangeEquals(m.loc, item.loc)) {
return key;
}
}
}

function collectCoverageItems<T extends BranchMapping | FunctionMapping>(
filePath: string,
itemsPerFile: Map<string, Record<string, T>>,
itemMap: Record<string, T>,
) {
let items = itemsPerFile.get(filePath);
if (!items) {
items = {};
itemsPerFile.set(filePath, items);
}

for (const item of Object.values(itemMap)) {
if (findKey(items, item) == null) {
const key = Object.keys(items).length;
items[key] = item;
}
}
}

function patchCoverageItems<T extends BranchMapping | FunctionMapping, U extends number | number[]>(
filePath: string,
itemsPerFile: Map<string, Record<string, T>>,
itemMap: Record<string, T>,
itemIndex: Record<string, U>,
defaultIndex: () => U,
) {
const items = itemsPerFile.get(filePath)!;
const originalItems = itemMap;
const originalIndex = itemIndex;
itemMap = items;
itemIndex = {};

for (const [key, mapping] of Object.entries(items)) {
const originalKey = findKey(originalItems, mapping);
if (originalKey != null) {
itemIndex[key] = originalIndex[originalKey];
} else {
itemIndex[key] = defaultIndex();
}
}

return { itemMap, itemIndex };
}

/**
* Cross references coverage mapping data, looking for missing code branches
* and adding empty entries for them if found. This is necessary because istanbul
* expects code branch data to be equal for all coverage entries, while v8 only
* outputs actual covered code branches.
* Cross references coverage mapping data, looking for missing code branches and
* functions and adding empty entries for them if found. This is necessary
* because istanbul expects code branch and function data to be equal for all
* coverage entries. V8 only outputs actual covered code branchesm and functions
* that are defined at runtime (for example methods defined in a constructor
* that isn't run will not be included).
*
* See https://github.com/istanbuljs/istanbuljs/issues/531 for more.
* See https://github.com/istanbuljs/istanbuljs/issues/531,
* https://github.com/istanbuljs/v8-to-istanbul/issues/121 and
* https://github.com/modernweb-dev/web/issues/689 for more.
* @param coverages
*/
function addingMissingCoverageBranches(coverages: CoverageMapData[]) {
function addingMissingCoverageItems(coverages: CoverageMapData[]) {
const branchesPerFile = new Map<string, Record<string, BranchMapping>>();
const functionsPerFile = new Map<string, Record<string, FunctionMapping>>();

// collect code branches from all code coverage entries
// collect functions and code branches from all code coverage entries
for (const coverage of coverages) {
for (const [filePath, fileCoverage] of Object.entries(coverage)) {
let branches = branchesPerFile.get(filePath);
if (!branches) {
branches = {};
branchesPerFile.set(filePath, branches);
}

for (const branch of Object.values(fileCoverage.branchMap)) {
if (findBranchKey(branches, branch) == null) {
const key = Object.keys(branches).length;
branches[key] = branch;
}
}
collectCoverageItems(filePath, branchesPerFile, fileCoverage.branchMap);
collectCoverageItems(filePath, functionsPerFile, fileCoverage.fnMap);
}
}

// patch coverage entries to add missing code branches
for (const coverage of coverages) {
for (const [filePath, fileCoverage] of Object.entries(coverage)) {
const branches = branchesPerFile.get(filePath)!;
const originalBranches = fileCoverage.branchMap;
const originalB = fileCoverage.b;
fileCoverage.branchMap = branches;
fileCoverage.b = {};

for (const [key, mapping] of Object.entries(branches)) {
const originalKey = findBranchKey(originalBranches, mapping);
if (originalKey != null) {
fileCoverage.b[key] = originalB[originalKey];
} else {
fileCoverage.b[key] = [0];
}
}
const patchedBranches = patchCoverageItems(
filePath,
branchesPerFile,
fileCoverage.branchMap,
fileCoverage.b,
() => [0],
);
fileCoverage.branchMap = patchedBranches.itemMap;
fileCoverage.b = patchedBranches.itemIndex;

const patchedFunctions = patchCoverageItems(
filePath,
functionsPerFile,
fileCoverage.fnMap,
fileCoverage.f,
() => 0,
);
fileCoverage.fnMap = patchedFunctions.itemMap;
fileCoverage.f = patchedFunctions.itemIndex;
}
}
}
Expand All @@ -98,7 +143,7 @@ export function getTestCoverage(
// because we're only working with objects and arrays
coverages = JSON.parse(JSON.stringify(coverages));

addingMissingCoverageBranches(coverages);
addingMissingCoverageItems(coverages);

for (const coverage of coverages) {
coverageMap.merge(coverage);
Expand Down

0 comments on commit 66f2437

Please sign in to comment.