Skip to content

Commit

Permalink
fix(test-runner): fix error when function metadata varies between tests
Browse files Browse the repository at this point in the history
Collect and merge function definitions from all runs in coverage mapping

Fixes modernweb-dev#689

See also istanbuljs/v8-to-istanbul#121
  • Loading branch information
Stuk committed May 4, 2021
1 parent 50e9f1a commit 4800aa5
Showing 1 changed file with 82 additions and 37 deletions.
119 changes: 82 additions & 37 deletions packages/test-runner-core/src/coverage/getTestCoverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CoverageMap,
CoverageMapData,
BranchMapping,
FunctionMapping,
Location,
Range,
} from 'istanbul-lib-coverage';
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 4800aa5

Please sign in to comment.