Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(test-runner): fix error when function metadata varies between tests #1436

Merged
merged 3 commits into from
May 5, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/many-pans-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@web/test-runner-core": patch
"@web/test-runner": patch
---

fix(test-runner): fix error when function metadata varies between tests, as seen in [https://github.com/modernweb-dev/web/issues/689](https://github.com/modernweb-dev/web/issues/689) and [https://github.com/istanbuljs/v8-to-istanbul/issues/121](https://github.com/istanbuljs/v8-to-istanbul/issues/121).
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 branches 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