UNPKG

monocart-coverage-reports

Version:

A code coverage tool to generate native V8 reports or Istanbul reports.

1,719 lines (1,347 loc) 48 kB
/** * V8 Coverage Data Converter * @copyright https://github.com/cenfun/monocart-coverage-reports * @author cenfun@gmail.com */ const EC = require('eight-colors'); const { Locator } = require('monocart-locator'); const Util = require('../utils/util.js'); const findOriginalRange = require('./find-original-range.js'); const { getJsAstInfo, getCssAstInfo } = require('./ast.js'); const { getIgnoredRanges } = require('./ignore.js'); const { getUntestedList } = require('./untested.js'); const { sortRanges, dedupeCountRanges, mergeRangesWith } = require('../utils/dedupe.js'); const { getSourceType, initSourceMapSourcesPath } = require('../utils/source-path.js'); const { decode } = require('../packages/monocart-coverage-vendor.js'); const InfoBranch = require('./info-branch.js'); const InfoFunction = require('./info-function.js'); const InfoStatement = require('./info-statement.js'); // ======================================================================================================== // debug info const logMappingErrors = (type, result) => { // const { // errors, start, end, sourcePath // } = result; // // if (!sourcePath.endsWith('monocart-v8.js') || type !== 'byte') { // // return; // // } // const list = [ // Util.EC.red('[Mapping]'), // Util.EC.magenta(type), // errors.join(' -> '), // Util.EC.blue(`${start} ~ ${end}`), // sourcePath // ]; // console.log(list.join(' ')); }; // ======================================================================================================== const handleIgnoredRanges = (list, ignoredRanges) => { list.forEach((item) => { const range = Util.findInRanges(item.start, item.end, ignoredRanges); if (range) { // console.log(item, range); item.ignored = true; } }); }; const applyBytesToLines = (bytes, locator, lineMap) => { bytes.forEach((range) => { const { start, end, count, ignored } = range; // no need handle ignored byte if (ignored) { return; } const sLoc = locator.offsetToLocation(start); const eLoc = locator.offsetToLocation(end); // update lines coverage const lines = Util.getRangeLines(sLoc, eLoc); Util.updateLinesCoverage(lines, count, lineMap); }); // no ignore items lineMap.forEach((lineItem, line) => { const { uncoveredEntire, uncoveredPieces, coveredCount } = lineItem; // default count to 1, both js and css let count = 1; // full covered true/false for entire line let covered = true; if (uncoveredEntire) { count = 0; covered = false; } else { count = coveredCount; const uncoveredLen = uncoveredPieces.length; if (uncoveredLen > 0) { covered = false; // uncovered count = `1/${uncoveredLen + 1}`; } } lineItem.covered = covered; lineItem.count = count; }); }; const handleLinesCoverage = (bytes, locator, ignoredRanges) => { // init lines let blankCount = 0; let commentCount = 0; const dataExtras = {}; // line 1 based const lineMap = new Map(); locator.lines.forEach((lineItem) => { // line 1-base const line = lineItem.line + 1; // exclude blank and comment if (lineItem.blank) { blankCount += 1; dataExtras[line] = 'b'; return; } if (lineItem.comment) { commentCount += 1; dataExtras[line] = 'c'; return; } const ignored = Util.findInRanges(lineItem.start, lineItem.end, ignoredRanges); if (ignored) { dataExtras[line] = 'i'; return; } Util.initLineCoverage(lineItem); lineMap.set(line, lineItem); }); applyBytesToLines(bytes, locator, lineMap); const summaryLines = { total: 0, covered: 0, blank: blankCount, comment: commentCount }; // data lines const dataLines = {}; // no ignore items lineMap.forEach((lineItem, line) => { const { count, covered } = lineItem; // data lines dataLines[line] = count; summaryLines.total += 1; if (covered) { summaryLines.covered += 1; } }); return { dataLines, dataExtras, summaryLines }; }; // ======================================================================================================== const calculateV8Summary = (list) => { const summary = { total: 0, covered: 0 }; list.forEach((item) => { if (item.ignored) { return; } summary.total += 1; if (item.count > 0) { summary.covered += 1; } }); return summary; }; // ======================================================================================================== // istanbul coverage format // https://github.com/istanbuljs/istanbuljs/blob/master/docs/raw-output.md /** * * `path` - the file path for which coverage is being tracked * * `statementMap` - map of statement locations keyed by statement index * * `fnMap` - map of function metadata keyed by function index * * `branchMap` - map of branch metadata keyed by branch index * * `s` - hit counts for statements * * `f` - hit count for functions * * `b` - hit count for branches */ const collectFileCoverage = (v8Data, state, options) => { const { bytes, functions, branches, statements, sourcePath, locator } = state; // ========================================== // v8 data const data = { bytes: dedupeCountRanges(bytes), functions: [], branches: [], statements: [] }; // ========================================== // ignore const ignoredRanges = getIgnoredRanges(locator, options); if (ignoredRanges) { data.ignores = ignoredRanges; // data bytes is start/end/count object handleIgnoredRanges(data.bytes, ignoredRanges); // functions start/end/count instance handleIgnoredRanges(functions, ignoredRanges); // branches start/end/count instance handleIgnoredRanges(branches, ignoredRanges); // branch locations start/end/count object branches.forEach((group) => { if (group.ignored) { // all branch group ignored group.locations.forEach((it) => { it.ignored = true; }); } else { handleIgnoredRanges(group.locations, ignoredRanges); } }); // statements start/end/count instance handleIgnoredRanges(statements, ignoredRanges); // console.log(ignoredRanges); } // ========================================== // lines // after bytes with ignored, before calculateV8Lines const { dataLines, dataExtras, summaryLines } = handleLinesCoverage(data.bytes, locator, ignoredRanges); data.lines = dataLines; data.extras = dataExtras; // console.log('statements', state.sourcePath, statements.length); // ========================================== data.functions = functions.map((info, i) => { return info.getRange(i); }); sortRanges(data.functions); // branch group with locations to flat branches data.branches = branches.map((info) => { return info.getRanges(); }).flat(); sortRanges(data.branches); data.statements = statements.map((info) => { return info.getRange(); }); sortRanges(data.statements); // ========================================== const summary = { functions: calculateV8Summary(data.functions), branches: calculateV8Summary(data.branches), statements: calculateV8Summary(data.statements), lines: summaryLines }; // ========================================== // v8 data and summary v8Data.data = data; v8Data.summary = summary; // ========================================== // istanbul const istanbulData = { path: sourcePath, statementMap: {}, fnMap: {}, branchMap: {}, s: {}, f: {}, b: {} }; statements.filter((it) => !it.ignored).forEach((statement, index) => { istanbulData.statementMap[`${index}`] = statement.generate(locator); istanbulData.s[`${index}`] = statement.count; }); functions.filter((it) => !it.ignored).forEach((fn, index) => { istanbulData.fnMap[`${index}`] = fn.generate(locator, index); istanbulData.f[`${index}`] = fn.count; }); branches.filter((it) => !it.ignored).forEach((branch, index) => { const { map, counts } = branch.generate(locator); istanbulData.branchMap[`${index}`] = map; istanbulData.b[`${index}`] = counts; }); return istanbulData; }; // ======================================================================================================== const addJsBytesCoverage = (state, range) => { const { startOffset, endOffset, count } = range; // add bytes range const byte = { start: startOffset, // the end could be > source.length end: Math.min(endOffset, state.maxContentLength), count }; // for debug if (Util.isDebug() && state.original) { byte.generatedStart = range.generatedStart; byte.generatedEnd = range.generatedEnd; } state.bytes.push(byte); }; const addCssBytesCoverage = (state, range) => { const { start, end, count } = range; // add css bytes range, already start, end state.bytes.push({ start, end, count }); }; // ======================================================================================================== const checkOriginalRangeCode = (range, state, startEndMap, type) => { // only for original if (!state.original) { return true; } const { start, end } = range; if (start >= end) { // console.log('invalid branch', state.sourcePath, range); return false; } // could be vue code const text = state.locator.getSlice(start, end).trim(); if (!text) { return false; } // do not check text for `bytes` // it could be `} ` uncovered in `try { } catch` // instead, use `dedupeCountRanges` to accumulate count if (type === 'bytes') { return true; } // invalid original code // Matches any character that is not a word character from the basic Latin alphabet. // Equivalent to [^A-Za-z0-9_] if (text.length === 1 && (/\W/).test(text)) { // ; } = // console.log(text, type, state.sourcePath, range); return false; } // type: bytes, functions, branches, statements // check repeated range const key = `${start}_${end}`; if (startEndMap.has(key)) { return false; } startEndMap.set(key, range); return true; }; const handleFunctionsCoverage = (state) => { // functions only for js if (!state.js) { return; } const startEndMap = new Map(); const { functions, astInfo } = state; astInfo.functions.forEach((it) => { if (!checkOriginalRangeCode(it, state, startEndMap, 'functions')) { return; } functions.push(new InfoFunction(it, state.original)); }); }; const handleBranchesCoverage = (state) => { // functions only for js if (!state.js) { return; } const startEndMap = new Map(); const { branches, astInfo } = state; astInfo.branches.forEach((it) => { if (!checkOriginalRangeCode(it, state, startEndMap, 'branches')) { return; } branches.push(new InfoBranch(it, state.original)); }); }; const handleStatementsCoverage = (state) => { // statement only for js if (!state.js) { return; } const startEndMap = new Map(); const { statements, astInfo } = state; astInfo.statements.forEach((it) => { if (!checkOriginalRangeCode(it, state, startEndMap, 'statements')) { return; } statements.push(new InfoStatement(it, state.original)); }); }; const handleOriginalBytesCoverage = (state) => { const startEndMap = new Map(); state.bytes = state.bytes.filter((it) => { return checkOriginalRangeCode(it, state, startEndMap, 'bytes'); }); }; const handleGeneratedBytesCoverage = (state) => { // it could be a dist file, do not handle twice if (state.addedGeneratedBytes) { return; } const { js, coverageList } = state; if (js) { coverageList.forEach((block) => { block.ranges.forEach((range) => { const { fixedStart, fixedEnd } = Util.fixSourceRange(state.locator, range.startOffset, range.endOffset); range.startOffset = fixedStart; range.endOffset = fixedEnd; addJsBytesCoverage(state, range); }); }); } else { coverageList.forEach((range) => { addCssBytesCoverage(state, range); }); } state.addedGeneratedBytes = true; }; // ======================================================================================================== const handleOriginalFunctionsCoverage = (state, originalStateMap) => { // functions only for js if (!state.js) { return; } // console.log(state.astInfo.functions); const updateFunctionBodyRange = (start, end, bodyStart, bodyEnd, originalFunction) => { if (bodyStart !== start || bodyEnd !== end) { const result = findOriginalRange(bodyStart, bodyEnd, state, originalStateMap); if (result.error) { logMappingErrors('function body', result); } else { originalFunction.bodyStart = result.start; originalFunction.bodyEnd = result.end; } } }; // function count state.astInfo.functions.forEach((it) => { // remove webpack wrap functions for functions count, not for ranges here if (it.generatedOnly) { return; } const { start, end, bodyStart, bodyEnd } = it; const result = findOriginalRange(start, end, state, originalStateMap, { checkName: true }); if (result.error) { logMappingErrors('function', result); return; } const originalFunction = { ... it, generatedStart: start, generatedEnd: end, start: result.start, end: result.end, bodyStart: result.start, bodyEnd: result.end }; if (result.name) { originalFunction.functionName = result.name; } // body start and end updateFunctionBodyRange(start, end, bodyStart, bodyEnd, originalFunction); // add back to original ast result.originalState.astInfo.functions.push(originalFunction); }); }; const handleOriginalBranchesCoverage = (state, originalStateMap) => { // branches only for js if (!state.js) { return; } // console.log(state.astInfo.branches); // function count state.astInfo.branches.forEach((group) => { if (group.generatedOnly) { return; } const { type, locations } = group; // start const result = findOriginalRange(group.start, group.end, state, originalStateMap); if (result.error) { logMappingErrors('branch group', result); return; } // new group start and end const groupStart = result.start; const groupEnd = result.end; let hasError; const newLocations = locations.map((oLoc) => { const newLoc = { ... oLoc }; if (newLoc.none) { newLoc.start = groupStart; newLoc.end = groupEnd; return newLoc; } const locResult = findOriginalRange(newLoc.start, newLoc.end, state, originalStateMap); if (locResult.error) { // It should not happen unless it is minify files, the SourceMap has some order problems logMappingErrors('branch', locResult); hasError = true; return newLoc; } // before new range newLoc.generatedStart = newLoc.start; newLoc.generatedEnd = newLoc.end; // mapping to new range newLoc.start = locResult.start; newLoc.end = locResult.end; return newLoc; }); // ignored group when found error if (hasError) { return; } // add back to original ast result.originalState.astInfo.branches.push({ type, start: groupStart, end: groupEnd, locations: newLocations }); }); }; const handleOriginalStatementsCoverage = (state, originalStateMap) => { // statements only for js if (!state.js) { return; } // statement count state.astInfo.statements.forEach((it) => { if (it.generatedOnly) { return; } const { start, end } = it; const result = findOriginalRange(start, end, state, originalStateMap); if (result.error) { logMappingErrors('statement', result); return; } // add back to original ast result.originalState.astInfo.statements.push({ ... it, generatedStart: start, generatedEnd: end, start: result.start, end: result.end }); }); originalStateMap.forEach((originalState) => { const statements = originalState.astInfo.statements; if (Util.isList(statements)) { return; } // fake source nothing matched statements.push({ start: 0, end: originalState.source.length, count: 1 }); }); }; // ======================================================================================================== const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => { const checkList = []; // check original bytes if fully in a wrapper range originalStateMap.forEach((originalState) => { const bytes = originalState.bytes; if (Util.isList(bytes)) { return; } const { decodedMappings } = originalState; const len = decodedMappings.length; if (len < 2) { return; } // sort by original line/column const startMapping = decodedMappings[0]; const endMapping = decodedMappings[len - 1]; // console.log(startMapping, endMapping); const startOffset = startMapping.generatedOffset; if (!endMapping.generatedEndOffset) { // line last one const line = state.locator.getLine(endMapping.generatedLine + 1); // could be no line found if (line) { // last column endMapping.generatedEndOffset = line.end; } else { endMapping.generatedEndOffset = endMapping.generatedOffset; } } const endOffset = endMapping.generatedEndOffset; // console.log('===========================================================', originalState.sourcePath); // console.log(originalState.source.length, endMapping); checkList.push({ originalState, startOffset, endOffset }); }); // no file to handle if (!checkList.length) { return; } // there is no state.bytes if not in debug // should using coverageList to generate bytes first handleGeneratedBytesCoverage(state); checkList.forEach((it) => { const { originalState, startOffset, endOffset } = it; // console.log('=============', 'no bytes', originalState.sourcePath); // only check uncovered range // because a uncovered range could be in a covered wrapper // { start: 0, end: 12137, count: 1 }, could be { start: > 0, end: < 12137, count: 0 } for (const range of state.bytes) { if (range.count > 0) { continue; } if (startOffset >= range.start && endOffset <= range.end) { // console.log('------------', 'added'); originalState.bytes.push({ start: 0, end: originalState.source.length, count: 0 }); break; } } }); }; const handleAllOriginalBytesCoverage = (state, originalStateMap) => { const { js, coverageList } = state; // only for js, no sourcemap for css for now if (!js) { return; } // v8 coverage coverageList.forEach((block) => { block.ranges.forEach((range) => { // remove wrap functions for original files if (range.generatedOnly) { return; } const { startOffset, endOffset, count } = range; const result = findOriginalRange(startOffset, endOffset, state, originalStateMap, { fixOriginalRange: true }); if (result.error) { logMappingErrors('byte', result); return; } addJsBytesCoverage(result.originalState, { generatedStart: startOffset, generatedEnd: endOffset, startOffset: result.start, endOffset: result.end, count }); }); }); handleOriginalEmptyBytesCoverage(state, originalStateMap); }; // ======================================================================================================== const decodeSourceMappings = (state, originalDecodedMap) => { const generatedLocator = state.locator; const { sources, mappings, decodedMappings } = state.sourceMap; const decodedList = decodedMappings || decode(mappings); // console.log(decodedList); sources.forEach((source, i) => { originalDecodedMap.set(i, []); }); const allDecodedMappings = []; decodedList.forEach((segments, generatedLine) => { const lastIndex = segments.length - 1; segments.forEach((segment, i) => { // const COLUMN = 0; // const SOURCES_INDEX = 1; // const SOURCE_LINE = 2; // const SOURCE_COLUMN = 3; // const NAMES_INDEX = 4; const [generatedColumn, sourceIndex, originalLine, originalColumn, nameIndex] = segment; // the segment length could be 1, 4 or 5 if (typeof sourceIndex === 'undefined') { return; } const generatedOffset = generatedLocator.locationToOffset({ // 1-base line: generatedLine + 1, column: generatedColumn }); const info = { generatedOffset, generatedLine, generatedColumn, sourceIndex, originalLine, originalColumn, nameIndex }; // first and last column if (i === 0) { info.first = true; } if (i === lastIndex) { info.last = true; } allDecodedMappings.push(info); originalDecodedMap.get(sourceIndex).push(info); }); }); // defaults to sort by generated offset, not need sort // allDecodedMappings.sort((a, b) => { // return a.generatedOffset - b.generatedOffset; // }); return allDecodedMappings; }; const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) => { // all mappings for the original file sorted const decodedMappings = originalDecodedMap.get(sourceIndex); if (!decodedMappings) { return []; } // sort by original line/column decodedMappings.sort((a, b) => { if (a.originalLine === b.originalLine) { return a.originalColumn - b.originalColumn; } return a.originalLine - b.originalLine; }); // add offset and index decodedMappings.forEach((item, i) => { item.originalOffset = locator.locationToOffset({ line: item.originalLine + 1, column: item.originalColumn }); }); return decodedMappings; }; // ======================================================================================================== const initOriginalList = (state, originalDecodedMap, options) => { // source filter const sourceFilter = Util.getSourceFilter(options); // create original content mappings const originalStateMap = new Map(); const { sources, sourcesContent } = state.sourceMap; const lengthBefore = sources.length; let lengthAfter = 0; sources.forEach((sourcePath, sourceIndex) => { // filter // do not change for sourceIndex if (!sourceFilter(sourcePath)) { // console.log('-', sourcePath); return; } // console.log(sourcePath); // console.log(`add source: ${k}`); const sourceContent = sourcesContent[sourceIndex]; if (typeof sourceContent !== 'string') { Util.logError(`not found source content: ${sourcePath}`); return; } const locator = new Locator(sourceContent); const maxContentLength = sourceContent.length; const decodedMappings = getOriginalDecodedMappings(originalDecodedMap, sourceIndex, locator); // unpacked file always is js const type = getSourceType(sourcePath); const originalState = { original: true, // original file is js js: true, type, source: sourceContent, sourcePath, locator, maxContentLength, decodedMappings, // coverage info bytes: [], functions: [], branches: [], statements: [], astInfo: { functions: [], branches: [], statements: [] }, // coverage data v8Data: {} }; originalStateMap.set(sourceIndex, originalState); lengthAfter += 1; }); Util.logFilter(`source filter (${state.sourcePath}):`, lengthBefore, lengthAfter); return originalStateMap; }; const collectOriginalList = (state, originalStateMap) => { const { fileUrls } = state; const distFile = state.sourcePath; let added = 0; // collect original files originalStateMap.forEach((originalState) => { const { js, type, sourcePath, source } = originalState; // add file item const url = fileUrls[sourcePath] || sourcePath; // add dist for id const id = Util.calculateSha1(sourcePath + source); const sourceItem = { url, id, js, type, sourcePath, distFile, source }; // save v8 data and add to originalList originalState.v8Data = sourceItem; state.originalList.push(originalState); added += 1; }); Util.logDebug(`added source files: ${EC.yellow(added)}`); }; // ======================================================================================================== const generateCoverageForDist = (state) => { handleFunctionsCoverage(state); handleBranchesCoverage(state); handleStatementsCoverage(state); handleGeneratedBytesCoverage(state); }; const unpackSourceMap = (state, options) => { const { sourceMap, sourcePath } = state; // keep original urls const fileUrls = {}; initSourceMapSourcesPath(fileUrls, sourceMap, sourcePath, options); state.fileUrls = fileUrls; // for function names state.sourceMapNames = sourceMap.names || []; // =============================================== // decode mappings for each original file const originalDecodedMap = new Map(); // for find-original-range state.decodedMappings = decodeSourceMappings(state, originalDecodedMap); // filter original list and init list const originalStateMap = initOriginalList(state, originalDecodedMap, options); originalDecodedMap.clear(); // =============================================== // handle functions before handle original state functions handleOriginalFunctionsCoverage(state, originalStateMap); handleOriginalBranchesCoverage(state, originalStateMap); handleOriginalStatementsCoverage(state, originalStateMap); // handle lines info before handle ranges to update line count originalStateMap.forEach((originalState) => { handleFunctionsCoverage(originalState); handleBranchesCoverage(originalState); handleStatementsCoverage(originalState); // if (originalState.sourcePath.endsWith('demo.js')) { // console.log('=================================', originalState.sourcePath); // } }); // handle bytes ranges handleAllOriginalBytesCoverage(state, originalStateMap); originalStateMap.forEach((originalState) => { handleOriginalBytesCoverage(originalState); }); // collect coverage for original list collectOriginalList(state, originalStateMap); }; const unpackDistFile = (item, state, options) => { if (state.sourceMap) { if (Util.isDebug()) { // js self item.debug = true; generateCoverageForDist(state); } else { item.dedupe = true; } // unpack source map unpackSourceMap(state, options); } else { // css/js self generateCoverageForDist(state); } }; // ======================================================================================================== const filterCoverageList = (item) => { const { functions, scriptOffset, source } = item; // no script offset if (!scriptOffset) { return functions; } // vm script offset const minOffset = scriptOffset; // the inline sourcemap could be removed const maxOffset = source.length; const rootFunctionInfo = { root: true, ranges: [{ startOffset: minOffset, endOffset: maxOffset, count: 1 }] }; const coverageList = functions.filter((block) => { const { ranges } = block; // first one is function coverage info const functionRange = ranges[0]; const { startOffset, endOffset } = functionRange; if (startOffset >= minOffset && endOffset <= maxOffset) { return true; } // blocks const len = ranges.length; if (len > 1) { for (let i = 1; i < len; i++) { const range = ranges[i]; if (range.startOffset >= minOffset && range.endOffset <= maxOffset) { rootFunctionInfo.ranges.push(range); } } } return false; }); // first one for root function if (rootFunctionInfo.ranges.length > 1) { coverageList.unshift(rootFunctionInfo); } return coverageList; }; const initJsCoverageList = (item) => { const coverageList = filterCoverageList(item); // function could be covered even it is defined after an uncovered return, see case closures.js // fix uncovered range if there are covered ranges in uncovered range const uncoveredBlocks = []; const uncoveredList = []; coverageList.forEach((block) => { block.ranges.forEach((range, i) => { const { count, startOffset, endOffset } = range; if (i === 0) { // check only first level if (count > 0) { const inUncoveredRange = Util.findInRanges(startOffset, endOffset, uncoveredBlocks, 'startOffset', 'endOffset'); if (inUncoveredRange) { if (!inUncoveredRange.coveredList) { inUncoveredRange.coveredList = []; uncoveredList.push(inUncoveredRange); } inUncoveredRange.coveredList.push(range); } } return; } if (count === 0) { uncoveredBlocks.push({ ... range, index: i, ranges: block.ranges }); } }); }); if (uncoveredList.length) { uncoveredList.forEach((it) => { const { ranges, index, count, coveredList } = it; // remove previous range first const args = [index, 1]; Util.sortOffsetRanges(coveredList); let startOffset = it.startOffset; coveredList.forEach((cov) => { // ignore sub functions in the function if (cov.startOffset > startOffset) { args.push({ startOffset, endOffset: cov.startOffset, count }); startOffset = cov.endOffset; } }); if (it.endOffset > startOffset) { args.push({ startOffset, endOffset: it.endOffset, count }); } ranges.splice(... args); }); } return coverageList; }; const logConvertTime = (msg, time_start, untested) => { if (untested) { return; } Util.logTime(msg, time_start); }; const convertCoverages = (list, options, untested) => { const stateList = []; for (const item of list) { // console.log([item.id]); const time_start_ast = Date.now(); const { type, source, fake, sourcePath } = item; // for source file, type could be ts or vue as extname, but js = true const js = type === 'js'; item.js = js; // source mapping const locator = new Locator(source); const maxContentLength = source.length; // ============================ // move sourceMap const sourceMap = item.sourceMap; if (sourceMap) { delete item.sourceMap; } // ============================ // move functions and ranges to coverageList let coverageList = []; let astInfo; if (js) { coverageList = initJsCoverageList(item); // remove original functions if (!Util.isDebug()) { delete item.functions; } astInfo = getJsAstInfo(item, coverageList); } else { // convent css covered ranges to rules ranges and include uncovered ranges astInfo = getCssAstInfo(item, coverageList); // remove original ranges if (!Util.isDebug()) { delete item.ranges; } } logConvertTime(` ┌ [convert] parsed ast: ${sourcePath} (${EC.cyan(Util.BSF(maxContentLength))})`, time_start_ast, untested); // console.log(sourcePath, astInfo.statements.length); // ============================ const time_start_unpack = Date.now(); // current file and it's sources from sourceMap // see const originalState const state = { js, type, source, fake, sourcePath, sourceMap, locator, maxContentLength, decodedMappings: [], rangeCache: new Map(), diffCache: new Map(), // alignTextList: [], // coverage info bytes: [], functions: [], branches: [], statements: [], astInfo, // for sub source files coverageList, originalList: [], // coverage data v8Data: item }; unpackDistFile(item, state, options); const unpackedFiles = EC.cyan(`${state.originalList.length} files`); stateList.push(state); logConvertTime(` ┌ [convert] unpacked sourcemap: ${sourcePath} (${unpackedFiles})`, time_start_unpack, untested); } return stateList; }; // ======================================================================================================== const isUncoveredRange = (range, key, uncoveredGroups) => { let uncovered = true; uncoveredGroups.forEach((group) => { const { groupMap, state } = group; // ======================================================== // self key if (groupMap.has(key)) { return; } // ======================================================== // in all group for (const item of groupMap.values()) { if (range.start >= item.start && range.end <= item.end) { return; } } // ======================================================== // no sourcemap mappings in range // check original range in decodedMappings const { decodedMappings } = state; const item = decodedMappings.find((it) => it.originalOffset >= range.start && it.originalOffset <= range.end); if (!item) { return; } // ======================================================== uncovered = false; }); return uncovered; }; const mergeV8Data = (state, stateList) => { // console.log(stateList); // let debug = false; // if (state.sourcePath.endsWith('counter.tsx')) { // debug = true; // console.log('merge v8 data ===================================', state.sourcePath); // console.log('bytes before ============', stateList.map((it) => it.bytes)); // // console.log('statements ==============', stateList.map((it) => it.statements)); // // console.log('functions ==============', stateList.map((it) => it.functions)); // // console.log('branches ======================', stateList.map((it) => it.branches.map((b) => [`${b.start}-${b.end}`, JSON.stringify(b.locations.map((l) => l.count))]))); // } // =========================================================== // bytes const mergedBytes = []; const coveredMap = new Map(); const uncoveredMap = new Map(); const uncoveredGroups = []; stateList.forEach((st) => { const groupMap = new Map(); const bytes = dedupeCountRanges(st.bytes); bytes.forEach((range) => { const key = `${range.start}_${range.end}`; if (range.count) { mergedBytes.push(range); coveredMap.set(key, true); } else { uncoveredMap.set(key, range); groupMap.set(key, range); } }); uncoveredGroups.push({ groupMap, state: st }); }); // if (debug) { // console.log('coveredMap ============', coveredMap); // console.log('uncoveredMap ============', uncoveredMap); // console.log('uncoveredGroups ============', uncoveredGroups); // } uncoveredMap.forEach((range, key) => { // in covered range if (coveredMap.has(key)) { return; } if (isUncoveredRange(range, key, uncoveredGroups)) { mergedBytes.push(range); } }); // will be dedupeCountRanges in collectFileCoverage state.bytes = mergedBytes; // if (debug) { // console.log('bytes after ============', mergedBytes); // } // =========================================================== // functions const allFunctions = stateList.map((it) => it.functions).flat(); const functionComparer = (lastRange, range) => { // if (lastRange.start === range.start && lastRange.end === range.end) { // return true; // } // function range could be from sourcemap, not exact matched // end is same // {start: 2017, end: 2315, count: 481} // {start: 2018, end: 2315, count: 14} // start is same // {start: 10204, end: 10379, count: 0} // {start: 10204, end: 10393, count: 5} // only one position matched could be same if (lastRange.start === range.start || lastRange.end === range.end) { // console.log(lastRange.start, range.start, lastRange.end, range.end); // if (lastRange.start === range.start) { // console.log(range.end - lastRange.end, lastRange.start, lastRange.end, 'end', range.end, state.sourcePath); // } else { // console.log(range.start - lastRange.start, lastRange.start, lastRange.end, 'start', range.start, state.sourcePath); // } return true; } return false; }; const functionHandler = (lastRange, range) => { lastRange.count += range.count; }; const mergedFunctions = mergeRangesWith(allFunctions, functionComparer, functionHandler); state.functions = mergedFunctions; // =========================================================== // statements const allStatements = stateList.map((it) => it.statements).flat(); const statementComparer = (lastRange, range) => { // exact matched because the statement range is generated from ast return lastRange.start === range.start && lastRange.end === range.end; }; const statementHandler = (lastRange, range) => { // merge statements count lastRange.count += range.count; }; const mergedStatements = mergeRangesWith(allStatements, statementComparer, statementHandler); state.statements = mergedStatements; // =========================================================== // branches const allBranches = stateList.map((it) => it.branches).flat(); const branchComparer = (lastRange, range) => { // exact matched because the branch range is generated from ast return lastRange.start === range.start && lastRange.end === range.end; }; const branchHandler = (lastRange, range) => { // merge locations count lastRange.locations.forEach((item, i) => { const loc = range.locations[i]; if (loc) { item.count += loc.count; } }); }; const mergedBranches = mergeRangesWith(allBranches, branchComparer, branchHandler); state.branches = mergedBranches; // if (sourcePath.endsWith('scroll_zoom.ts')) { // console.log(mergedBytes); // console.log(mergedFunctions); // console.log(mergedBranches.map((b) => [`${b.start}-${b.end}`, JSON.stringify(b.locations.map((l) => l.count))])); // } }; const addUntestedFiles = async (stateList, options) => { const time_start_untested = Date.now(); const testedMap = new Map(); stateList.forEach((state) => { const { v8Data, originalList } = state; // dedupe dist file if not debug if (!v8Data.dedupe) { testedMap.set(state.sourcePath, true); } originalList.forEach((originalState) => { testedMap.set(originalState.sourcePath, true); }); }); // console.log('testedMap', testedMap); const untestedList = await getUntestedList(testedMap, options, 'v8'); if (!untestedList) { return; } const untestedStateList = convertCoverages(untestedList, options, true); // console.log(untestedStateList); untestedStateList.forEach((state) => { stateList.push(state); }); // console.log('untestedList', untestedList); Util.logTime(` ┌ [convert] added untested files: ${EC.yellow(untestedList.length)}`, time_start_untested); }; const generateV8DataList = (stateList, options) => { const stateMap = new Map(); // all original files from dist const allOriginalList = []; stateList.forEach((state) => { const { v8Data, originalList } = state; // dedupe dist file if not debug if (!v8Data.dedupe) { stateMap.set(v8Data.id, state); } allOriginalList.push(originalList); }); // merge istanbul and v8(converted) const mergeMap = new Map(); allOriginalList.flat().forEach((originalState) => { const { v8Data } = originalState; const id = v8Data.id; // exists item const prevState = stateMap.get(id); if (prevState) { // ignore empty item, just override it if (!prevState.v8Data.empty) { if (mergeMap.has(id)) { mergeMap.get(id).push(originalState); } else { mergeMap.set(id, [prevState, originalState]); } return; } } stateMap.set(id, originalState); }); const mergeIds = mergeMap.keys(); for (const id of mergeIds) { const state = stateMap.get(id); // for source the type could be ts, so just use js (boolean) if (state.js) { mergeV8Data(state, mergeMap.get(id)); } else { // should no css here, css can not be in sources } } // new v8 data list (includes sources) const v8DataList = []; // global file sources and istanbul coverage data const fileSources = {}; const coverageData = {}; stateMap.forEach((state) => { const { v8Data } = state; const istanbulData = collectFileCoverage(v8Data, state, options); const { sourcePath, source } = v8Data; v8DataList.push(v8Data); fileSources[sourcePath] = source; coverageData[sourcePath] = istanbulData; }); // sort v8DataList (files) v8DataList.sort((a, b) => { if (a.sourcePath > b.sourcePath) { return 1; } return -1; }); return { v8DataList, fileSources, coverageData }; }; const convertV8List = async (v8list, options) => { // for tested files const stateList = convertCoverages(v8list, options); // empty coverage handler await addUntestedFiles(stateList, options); const time_start_convert = Date.now(); const dataList = generateV8DataList(stateList, options); const dataFiles = EC.cyan(`${dataList.v8DataList.length} files`); Util.logTime(` ┌ [convert] converted data list (${dataFiles})`, time_start_convert); return dataList; }; module.exports = { convertV8List };