diff --git a/packages/test-runner-core/src/coverage/getTestCoverage.ts b/packages/test-runner-core/src/coverage/getTestCoverage.ts index ffeca3dea..d8a0110d4 100644 --- a/packages/test-runner-core/src/coverage/getTestCoverage.ts +++ b/packages/test-runner-core/src/coverage/getTestCoverage.ts @@ -4,6 +4,7 @@ import { CoverageMap, CoverageMapData, BranchMapping, + FunctionMapping, Location, Range, } from 'istanbul-lib-coverage'; @@ -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, branch: BranchMapping) { - for (const [key, m] of Object.entries(branches)) { - if (rangeEquals(m.loc, branch.loc)) { +function findKey(items: Record, item: T) { + for (const [key, m] of Object.entries(items)) { + if (rangeEquals(m.loc, item.loc)) { return key; } } } +function collectCoverageItems( + filePath: string, + itemsPerFile: Map>, + itemMap: Record, +) { + 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( + filePath: string, + itemsPerFile: Map>, + itemMap: Record, + itemIndex: Record, + 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>(); + const functionsPerFile = new Map>(); - // 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; } } } @@ -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);