monocart-coverage-reports
Advanced tools
Comparing version 2.2.2 to 2.3.0
@@ -39,8 +39,2 @@ #!/usr/bin/env node | ||
// init reports to list with `,` | ||
const reports = cliOptions.reports; | ||
if (reports && typeof reports === 'string') { | ||
cliOptions.reports = reports.split(','); | ||
} | ||
// report options | ||
@@ -47,0 +41,0 @@ const options = { |
@@ -39,2 +39,13 @@ const { | ||
// root function branches | ||
const functionRoot = state.functionRoot; | ||
if (functionRoot) { | ||
const rootFunctionInfo = { | ||
count: functionRoot.count, | ||
range: functionRoot | ||
}; | ||
createBranches(ast, rootFunctionInfo, branchMap); | ||
} | ||
// functions branches | ||
Util.visitAst(ast, { | ||
@@ -105,4 +116,7 @@ | ||
const functionNameMap = new Map(); | ||
let functionRoot; | ||
coverageList.forEach((block) => { | ||
const { functionName, ranges } = block; | ||
const { | ||
functionName, ranges, root | ||
} = block; | ||
@@ -112,5 +126,2 @@ // first one is function coverage info | ||
functionRange.functionName = functionName; | ||
if (functionName) { | ||
functionNameMap.set(functionRange.startOffset + functionName.length, functionRange); | ||
} | ||
@@ -134,2 +145,11 @@ // blocks | ||
// root function | ||
if (root) { | ||
functionRoot = functionRange; | ||
return; | ||
} | ||
if (functionName) { | ||
functionNameMap.set(functionRange.startOffset + functionName.length, functionRange); | ||
} | ||
functionRanges.push(functionRange); | ||
@@ -153,3 +173,4 @@ | ||
functionNameMap, | ||
functionRanges | ||
functionRanges, | ||
functionRoot | ||
}; | ||
@@ -156,0 +177,0 @@ |
@@ -84,2 +84,10 @@ const fs = require('fs'); | ||
await Util.writeFile(filePath, JSON.stringify(data)); | ||
// save source and sourcemap file for debug | ||
// https://evanw.github.io/source-map-visualization | ||
// if (data.sourceMap) { | ||
// await Util.writeFile(`${filePath}.js`, data.source); | ||
// await Util.writeFile(`${filePath}.js.map`, JSON.stringify(data.sourceMap)); | ||
// } | ||
}; | ||
@@ -109,11 +117,15 @@ | ||
// save source and sourceMap to separated json file | ||
const sourceData = { | ||
url, | ||
id, | ||
// source, | ||
source: convertSourceMap.removeComments(source), | ||
// could be existed | ||
source, | ||
sourceMap | ||
}; | ||
// remove comments if not debug | ||
if (Util.loggingType !== 'debug') { | ||
sourceData.source = convertSourceMap.removeComments(source); | ||
} | ||
// check sourceMap only for js | ||
@@ -120,0 +132,0 @@ if (type === 'js' && !sourceData.sourceMap) { |
@@ -1,25 +0,10 @@ | ||
// https://github.com/demurgos/v8-coverage | ||
/** | ||
* @ranges is always non-empty. The first range is called the "root range". | ||
* @isBlockCoverage indicates if the function has block coverage information | ||
* @false means that there is a single range and its count is the number of times the function was called. | ||
* @true means that the ranges form a tree of blocks representing how many times each statement or expression inside was executed. | ||
* It detects skipped or repeated statements. The root range counts the number of function calls. | ||
* @functionName can be an empty string. This is common for the FunctionCov representing the whole module. | ||
* V8 Coverage Data Converter | ||
* @copyright https://github.com/cenfun/monocart-coverage-reports | ||
* @author cenfun@gmail.com | ||
*/ | ||
// https://github.com/bcoe/v8-coverage | ||
/** | ||
* @ranges is always non-empty. The first range is called the "root range". | ||
* @isBlockCoverage indicates if the function has block coverage information. | ||
If this is false, it usually means that the functions was never called. | ||
It seems to be equivalent to ranges.length === 1 && ranges[0].count === 0. | ||
* @functionName can be an empty string. This is common for the FunctionCov representing the whole module. | ||
*/ | ||
// if you have a line of code that says `var x= 10; console.log(x);` that's one line and 2 statements. | ||
const path = require('path'); | ||
const Util = require('../utils/util.js'); | ||
const decodeMappings = require('../utils/decode-mappings.js'); | ||
@@ -32,4 +17,7 @@ // position mapping for conversion between offset and line/column | ||
const { dedupeCountRanges } = require('../utils/dedupe.js'); | ||
const { | ||
sortRanges, dedupeCountRanges, mergeRangesWith | ||
} = require('../utils/dedupe.js'); | ||
const { getSourceType, initSourceMapSourcePath } = require('../utils/source-path.js'); | ||
const { decode } = require('../packages/monocart-coverage-vendor.js'); | ||
@@ -230,5 +218,4 @@ const InfoLine = require('./info-line.js'); | ||
*/ | ||
const collectFileCoverage = (item, state, coverageData, options) => { | ||
const collectFileCoverage = (v8Data, state, options) => { | ||
const { sourcePath } = item; | ||
const { | ||
@@ -238,2 +225,4 @@ bytes, | ||
branches, | ||
sourcePath, | ||
locator | ||
@@ -283,2 +272,3 @@ } = state; | ||
}); | ||
sortRanges(data.functions); | ||
@@ -289,2 +279,3 @@ // branch group with locations to flat branches | ||
}).flat(); | ||
sortRanges(data.branches); | ||
@@ -300,4 +291,4 @@ // ========================================== | ||
// v8 data and summary | ||
item.data = data; | ||
item.summary = { | ||
v8Data.data = data; | ||
v8Data.summary = { | ||
functions: calculateV8Functions(data.functions), | ||
@@ -310,3 +301,3 @@ branches: calculateV8Branches(data.branches), | ||
// istanbul | ||
const istanbulCoverage = { | ||
const istanbulData = { | ||
path: sourcePath, | ||
@@ -327,4 +318,4 @@ | ||
lines.filter((it) => !it.ignored).forEach((line, index) => { | ||
istanbulCoverage.statementMap[`${index}`] = line.generate(); | ||
istanbulCoverage.s[`${index}`] = line.count; | ||
istanbulData.statementMap[`${index}`] = line.generate(); | ||
istanbulData.s[`${index}`] = line.count; | ||
@@ -342,4 +333,4 @@ let count = 0; | ||
functions.filter((it) => !it.ignored).forEach((fn, index) => { | ||
istanbulCoverage.fnMap[`${index}`] = fn.generate(locator); | ||
istanbulCoverage.f[`${index}`] = fn.count; | ||
istanbulData.fnMap[`${index}`] = fn.generate(locator); | ||
istanbulData.f[`${index}`] = fn.count; | ||
}); | ||
@@ -349,8 +340,8 @@ | ||
const { map, counts } = branch.generate(locator); | ||
istanbulCoverage.branchMap[`${index}`] = map; | ||
istanbulCoverage.b[`${index}`] = counts; | ||
istanbulData.branchMap[`${index}`] = map; | ||
istanbulData.b[`${index}`] = counts; | ||
}); | ||
// append to dist file state | ||
coverageData[sourcePath] = istanbulCoverage; | ||
// istanbul data | ||
return istanbulData; | ||
@@ -625,3 +616,3 @@ }; | ||
const decodeSourceMappings = async (state, originalDecodedMap) => { | ||
const decodeSourceMappings = (state, originalDecodedMap) => { | ||
@@ -632,3 +623,3 @@ const generatedLocator = state.locator; | ||
const decodedList = await decodeMappings(mappings); | ||
const decodedList = decode(mappings); | ||
@@ -694,3 +685,3 @@ sources.forEach((source, i) => { | ||
if (!decodeMappings) { | ||
if (!decodedMappings) { | ||
return []; | ||
@@ -729,4 +720,2 @@ } | ||
const fileSources = state.fileSources; | ||
// create original content mappings | ||
@@ -751,5 +740,2 @@ const originalMap = new Map(); | ||
// keep original formatted content | ||
fileSources[sourcePath] = sourceContent; | ||
const locator = new Locator(sourceContent); | ||
@@ -765,7 +751,7 @@ | ||
original: true, | ||
// only js sourceMap for now | ||
// original file is js | ||
js: true, | ||
type, | ||
source: sourceContent, | ||
sourcePath, | ||
source: sourceContent, | ||
locator, | ||
@@ -780,3 +766,5 @@ decodedMappings, | ||
branches: [] | ||
} | ||
}, | ||
// coverage data | ||
v8Data: {} | ||
}; | ||
@@ -790,10 +778,8 @@ | ||
const collectOriginalList = (item, state, originalMap, options) => { | ||
const collectOriginalList = (state, originalMap) => { | ||
const { fileUrls, sourceMap } = state; | ||
const distFile = sourceMap.file || path.basename(item.sourcePath); | ||
const distFile = sourceMap.file || path.basename(state.sourcePath); | ||
// collect original files | ||
const sourceList = []; | ||
originalMap.forEach((originalState) => { | ||
@@ -809,3 +795,3 @@ | ||
// add dist for id | ||
const id = Util.calculateSha1(distFile + sourcePath + source); | ||
const id = Util.calculateSha1(sourcePath + source); | ||
@@ -822,9 +808,7 @@ const sourceItem = { | ||
// generate coverage for current file, state for dist file | ||
collectFileCoverage(sourceItem, originalState, state.coverageData, options); | ||
sourceList.push(sourceItem); | ||
// save v8 data and add to originalList | ||
originalState.v8Data = sourceItem; | ||
state.originalList.push(originalState); | ||
}); | ||
return sourceList; | ||
}; | ||
@@ -834,3 +818,3 @@ | ||
const generateCoverageForDist = (item, state, options) => { | ||
const generateCoverageForDist = (state) => { | ||
@@ -841,10 +825,7 @@ handleFunctionsCoverage(state); | ||
collectFileCoverage(item, state, state.coverageData, options); | ||
}; | ||
const unpackSourceMap = async (item, state, options) => { | ||
const unpackSourceMap = (state, options) => { | ||
const { sourcePath } = item; | ||
const sourceMap = state.sourceMap; | ||
const { sourceMap } = state; | ||
@@ -859,8 +840,6 @@ // keep original urls | ||
// decode mappings for each original file | ||
const time_start_decode = Date.now(); | ||
const originalDecodedMap = new Map(); | ||
// for find-original-range | ||
state.decodedMappings = await decodeSourceMappings(state, originalDecodedMap); | ||
// only debug level | ||
Util.logTime(`decode source mappings: ${sourcePath}`, time_start_decode); | ||
state.decodedMappings = decodeSourceMappings(state, originalDecodedMap); | ||
@@ -894,7 +873,7 @@ // filter original list and init list | ||
// collect coverage for original list | ||
state.sourceList = collectOriginalList(item, state, originalMap, options); | ||
collectOriginalList(state, originalMap); | ||
}; | ||
const unpackDistFile = async (item, state, options) => { | ||
const unpackDistFile = (item, state, options) => { | ||
@@ -905,3 +884,3 @@ if (state.sourceMap) { | ||
item.debug = true; | ||
generateCoverageForDist(item, state, options); | ||
generateCoverageForDist(state); | ||
} else { | ||
@@ -912,3 +891,3 @@ item.dedupe = true; | ||
// unpack source map | ||
await unpackSourceMap(item, state, options); | ||
unpackSourceMap(state, options); | ||
@@ -918,3 +897,3 @@ } else { | ||
// css/js self | ||
generateCoverageForDist(item, state, options); | ||
generateCoverageForDist(state); | ||
@@ -927,24 +906,257 @@ } | ||
const dedupeV8List = (v8list) => { | ||
const indexes = []; | ||
v8list.forEach((item, i) => { | ||
if (item.dedupe) { | ||
indexes.push(i); | ||
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; | ||
}); | ||
if (indexes.length) { | ||
indexes.reverse(); | ||
indexes.forEach((i) => { | ||
v8list.splice(i, 1); | ||
}); | ||
// first one for root function | ||
if (rootFunctionInfo.ranges.length > 1) { | ||
coverageList.unshift(rootFunctionInfo); | ||
} | ||
return coverageList; | ||
}; | ||
const convertV8List = async (v8list, options) => { | ||
// ======================================================================================================== | ||
// global file sources and coverage | ||
const isCoveredInRanges = (range, uncoveredBytes) => { | ||
if (!uncoveredBytes.length) { | ||
return true; | ||
} | ||
for (const item of uncoveredBytes) { | ||
if (range.start >= item.start && range.end <= item.end) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}; | ||
const mergeV8Data = (state, stateList) => { | ||
// console.log(stateList); | ||
// const sourcePath = state.sourcePath; | ||
// console.log(sourcePath); | ||
// if (sourcePath.endsWith('scroll_zoom.ts')) { | ||
// console.log('merge v8 data ===================================', sourcePath); | ||
// console.log(stateList.map((it) => it.bytes)); | ||
// console.log(stateList.map((it) => it.functions)); | ||
// console.log(stateList.map((it) => it.branches.map((b) => [`${b.start}-${b.end}`, JSON.stringify(b.locations.map((l) => l.count))]))); | ||
// } | ||
// =========================================================== | ||
// bytes | ||
const mergedBytes = []; | ||
const uncoveredList = []; | ||
stateList.forEach((st) => { | ||
const bytes = dedupeCountRanges(st.bytes); | ||
const uncoveredBytes = []; | ||
bytes.forEach((range) => { | ||
if (range.count) { | ||
mergedBytes.push(range); | ||
} else { | ||
uncoveredBytes.push(range); | ||
} | ||
}); | ||
uncoveredList.push(uncoveredBytes); | ||
}); | ||
// just remove uncovered range | ||
uncoveredList.forEach((currentBytes) => { | ||
currentBytes.forEach((range) => { | ||
for (const targetBytes of uncoveredList) { | ||
if (targetBytes === currentBytes) { | ||
continue; | ||
} | ||
if (isCoveredInRanges(range, targetBytes)) { | ||
return; | ||
} | ||
} | ||
mergedBytes.push(range); | ||
}); | ||
}); | ||
// will be dedupeCountRanges in collectFileCoverage | ||
state.bytes = 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; | ||
// =========================================================== | ||
// 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 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 = {}; | ||
let sourceList = []; | ||
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; | ||
}); | ||
return { | ||
v8DataList, | ||
fileSources, | ||
coverageData | ||
}; | ||
}; | ||
const convertV8List = (v8list, options) => { | ||
const stateList = []; | ||
for (const item of v8list) { | ||
@@ -961,5 +1173,2 @@ // console.log([item.id]); | ||
// append file source | ||
fileSources[sourcePath] = source; | ||
// source mapping | ||
@@ -969,3 +1178,3 @@ const locator = new Locator(source); | ||
// ============================ | ||
// move sourceMap | ||
// move sourceMap | ||
const sourceMap = item.sourceMap; | ||
@@ -981,3 +1190,3 @@ if (sourceMap) { | ||
if (js) { | ||
coverageList = item.functions; | ||
coverageList = filterCoverageList(item); | ||
// remove original functions | ||
@@ -1000,9 +1209,11 @@ if (Util.loggingType !== 'debug') { | ||
// current file and it's sources from sourceMap | ||
// see const originalState | ||
const state = { | ||
js, | ||
type, | ||
source, | ||
sourcePath, | ||
sourceMap, | ||
coverageList, | ||
locator, | ||
decodedMappings: [], | ||
// coverage info | ||
@@ -1013,42 +1224,17 @@ bytes: [], | ||
astInfo, | ||
// for istanbul | ||
fileSources: {}, | ||
coverageData: {} | ||
// for sub source files | ||
coverageList, | ||
originalList: [], | ||
// coverage data | ||
v8Data: item | ||
}; | ||
await unpackDistFile(item, state, options); | ||
unpackDistFile(item, state, options); | ||
// merge state | ||
Object.assign(fileSources, state.fileSources); | ||
Object.assign(coverageData, state.coverageData); | ||
stateList.push(state); | ||
if (Util.isList(state.sourceList)) { | ||
sourceList = sourceList.concat(state.sourceList); | ||
} | ||
} | ||
// add all sources | ||
if (sourceList.length) { | ||
sourceList.forEach((item) => { | ||
return generateV8DataList(stateList, options); | ||
// second time filter for empty source | ||
// exists same id, mark previous item as dedupe | ||
const prevItem = v8list.find((it) => it.id === item.id); | ||
if (prevItem) { | ||
prevItem.dedupe = true; | ||
} | ||
v8list.push(item); | ||
}); | ||
} | ||
// dedupe | ||
dedupeV8List(v8list); | ||
return { | ||
fileSources, | ||
coverageData | ||
}; | ||
}; | ||
@@ -1055,0 +1241,0 @@ |
@@ -548,2 +548,7 @@ | ||
// if (typeof range.startOffset !== 'number') { | ||
// console.log('=========================================================================', state.sourcePath); | ||
// console.log(range); | ||
// } | ||
// startOffset: inclusive | ||
@@ -550,0 +555,0 @@ // endOffset: exclusive |
@@ -36,7 +36,15 @@ const Util = require('../utils/util.js'); | ||
// remove ignored | ||
const locations = this.locations.filter((it) => !it.ignored); | ||
// do NOT change previous number type to object, need used for sourcemap | ||
const newLocations = this.locations.filter((it) => !it.ignored).map((it) => { | ||
const item = { | ||
start: it.start, | ||
end: it.end, | ||
none: it.none, | ||
count: it.count | ||
}; | ||
// [ { start:{line,column}, end:{line,column}, count }, ...] | ||
locations.forEach((item) => { | ||
// [ { start:{line,column}, end:{line,column}, count }, ...] | ||
Util.updateOffsetToLocation(locator, item); | ||
return item; | ||
}); | ||
@@ -47,3 +55,3 @@ | ||
type: this.type, | ||
locations: locations.map((item) => { | ||
locations: newLocations.map((item) => { | ||
const { | ||
@@ -68,3 +76,3 @@ start, end, none | ||
const counts = locations.map((item) => item.count); | ||
const counts = newLocations.map((item) => item.count); | ||
@@ -71,0 +79,0 @@ return { |
@@ -9,2 +9,5 @@ module.exports = { | ||
// {string|string[]} input raw dir(s) | ||
inputDir: null, | ||
// {string} v8 or html for istanbul by default | ||
@@ -11,0 +14,0 @@ // {array} multiple reports with options |
@@ -47,27 +47,26 @@ const fs = require('fs'); | ||
const defaultReport = dataType === 'v8' ? 'v8' : 'html'; | ||
if (Util.isList(reports)) { | ||
reports.forEach((it) => { | ||
if (Util.isList(it)) { | ||
// ["v8"] | ||
reportMap[it[0]] = { | ||
const reportList = Util.toList(reports, ','); | ||
reportList.forEach((it) => { | ||
if (Util.isList(it)) { | ||
// ["v8"], ["v8", {}] | ||
const id = it[0]; | ||
if (typeof id === 'string' && id) { | ||
reportMap[id] = { | ||
... it[1] | ||
}; | ||
return; | ||
} | ||
if (typeof it === 'string') { | ||
reportMap[it] = {}; | ||
return; | ||
} | ||
reportMap[defaultReport] = {}; | ||
}); | ||
} else if (typeof reports === 'string' && reports) { | ||
reportMap[reports] = {}; | ||
} else { | ||
return; | ||
} | ||
if (typeof it === 'string' && it) { | ||
reportMap[it] = {}; | ||
} | ||
}); | ||
// using default report if no reports | ||
if (!Object.keys(reportMap).length) { | ||
const defaultReport = dataType === 'v8' ? 'v8' : 'html'; | ||
reportMap[defaultReport] = {}; | ||
} | ||
const allSupportedReports = { | ||
const allBuildInReports = { | ||
// v8 | ||
@@ -78,5 +77,2 @@ 'v8': 'v8', | ||
// both | ||
'console-summary': 'both', | ||
// istanbul | ||
@@ -95,3 +91,7 @@ 'clover': 'istanbul', | ||
'text-lcov': 'istanbul', | ||
'text-summary': 'istanbul' | ||
'text-summary': 'istanbul', | ||
// both | ||
'console-summary': 'both', | ||
'raw': 'both' | ||
}; | ||
@@ -102,19 +102,27 @@ | ||
Object.keys(reportMap).forEach((k) => { | ||
const options = reportMap[k]; | ||
const groupName = allSupportedReports[k]; | ||
if (!groupName) { | ||
Util.logError(`Unsupported report: ${k}`); | ||
return; | ||
let type = allBuildInReports[k]; | ||
if (!type) { | ||
// for custom reporter | ||
type = options.type || 'v8'; | ||
} | ||
let group = reportGroup[groupName]; | ||
let group = reportGroup[type]; | ||
if (!group) { | ||
group = {}; | ||
reportGroup[groupName] = group; | ||
reportGroup[type] = group; | ||
} | ||
group[k] = reportMap[k]; | ||
group[k] = options; | ||
}); | ||
// requires a default istanbul report if data is istanbul | ||
if (dataType === 'istanbul' && !reportGroup.istanbul) { | ||
reportGroup.istanbul = { | ||
html: {} | ||
}; | ||
} | ||
return reportGroup; | ||
@@ -152,19 +160,10 @@ }; | ||
const showConsoleSummary = (coverageResults, options) => { | ||
const showConsoleSummary = (reportData, reportOptions, options) => { | ||
const bothGroup = options.reportGroup.both; | ||
if (!bothGroup) { | ||
return; | ||
} | ||
const { metrics } = reportOptions; | ||
const consoleOptions = bothGroup['console-summary']; | ||
if (!consoleOptions) { | ||
return; | ||
} | ||
const { metrics } = consoleOptions; | ||
const { | ||
summary, name, type | ||
} = coverageResults; | ||
} = reportData; | ||
if (name) { | ||
@@ -225,6 +224,32 @@ EC.logCyan(name); | ||
const saveRawReport = (reportData, reportOptions, options) => { | ||
const rawOptions = { | ||
outputDir: 'raw', | ||
... reportOptions | ||
}; | ||
const cacheDir = options.cacheDir; | ||
const rawDir = path.resolve(options.outputDir, rawOptions.outputDir); | ||
// console.log(rawDir, cacheDir); | ||
if (fs.existsSync(rawDir)) { | ||
Util.logError(`Failed to save raw report because the dir already exists: ${Util.relativePath(rawDir)}`); | ||
return; | ||
} | ||
const rawParent = path.dirname(rawDir); | ||
if (!fs.existsSync(rawParent)) { | ||
fs.mkdirSync(rawParent, { | ||
recursive: true | ||
}); | ||
} | ||
// just rename the cache folder name | ||
fs.renameSync(cacheDir, rawDir); | ||
}; | ||
// ======================================================================================================== | ||
const getCoverageResults = async (dataList, options) => { | ||
const getCoverageResults = async (dataList, sourceCache, options) => { | ||
// get first and check v8list or istanbul data | ||
@@ -242,12 +267,18 @@ const firstData = dataList[0]; | ||
// merge v8list first | ||
const v8list = await mergeV8Coverage(dataList, options); | ||
const v8list = await mergeV8Coverage(dataList, sourceCache, options); | ||
// console.log('after merge', v8list.map((it) => it.url)); | ||
const { coverageData, fileSources } = await convertV8List(v8list, options); | ||
return generateV8ListReports(v8list, coverageData, fileSources, options); | ||
// only debug level | ||
const time_start = Date.now(); | ||
const results = convertV8List(v8list, options); | ||
const { | ||
v8DataList, coverageData, fileSources | ||
} = results; | ||
Util.logTime(`converted v8 data: ${v8DataList.length}`, time_start); | ||
return generateV8ListReports(v8DataList, coverageData, fileSources, options); | ||
} | ||
// istanbul data | ||
const istanbulData = mergeIstanbulCoverage(dataList, options); | ||
const istanbulData = mergeIstanbulCoverage(dataList); | ||
const { coverageData, fileSources } = initIstanbulData(istanbulData, options); | ||
@@ -257,4 +288,4 @@ return saveIstanbulReports(coverageData, fileSources, options); | ||
const generateCoverageReports = async (dataList, options) => { | ||
const coverageResults = await getCoverageResults(dataList, options); | ||
const generateCoverageReports = async (dataList, sourceCache, options) => { | ||
const coverageResults = await getCoverageResults(dataList, sourceCache, options); | ||
@@ -264,4 +295,21 @@ // [ 'type', 'reportPath', 'name', 'watermarks', 'summary', 'files' ] | ||
showConsoleSummary(coverageResults, options); | ||
const buildInBothReports = { | ||
'console-summary': showConsoleSummary, | ||
'raw': saveRawReport | ||
}; | ||
const bothGroup = options.reportGroup.both; | ||
if (bothGroup) { | ||
const bothReports = Object.keys(bothGroup); | ||
for (const reportName of bothReports) { | ||
const reportOptions = bothGroup[reportName]; | ||
const buildInHandler = buildInBothReports[reportName]; | ||
if (buildInHandler) { | ||
await buildInHandler(coverageResults, reportOptions, options); | ||
} else { | ||
await Util.runCustomReporter(reportName, coverageResults, reportOptions, options); | ||
} | ||
} | ||
} | ||
return coverageResults; | ||
@@ -273,24 +321,43 @@ }; | ||
const getCoverageDataList = async (cacheDir) => { | ||
const files = fs.readdirSync(cacheDir).filter((f) => f.startsWith('coverage-')); | ||
if (!files.length) { | ||
return; | ||
} | ||
const getInputData = async (inputList) => { | ||
const dataList = []; | ||
const sourceCache = new Map(); | ||
const dataList = []; | ||
for (const item of files) { | ||
const content = await Util.readFile(path.resolve(cacheDir, item)); | ||
if (content) { | ||
dataList.push(JSON.parse(content)); | ||
const addJsonData = async (dir, filename) => { | ||
const isCoverage = filename.startsWith('coverage-'); | ||
const isSource = filename.startsWith('source-'); | ||
if (isCoverage || isSource) { | ||
const content = await Util.readFile(path.resolve(dir, filename)); | ||
if (content) { | ||
const json = JSON.parse(content); | ||
if (isCoverage) { | ||
dataList.push(json); | ||
} else { | ||
sourceCache.set(json.id, json); | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
if (dataList.length) { | ||
return dataList; | ||
for (const dir of inputList) { | ||
const allFiles = fs.readdirSync(dir); | ||
if (!allFiles.length) { | ||
continue; | ||
} | ||
for (const filename of allFiles) { | ||
// only json file | ||
if (filename.endsWith('.json')) { | ||
await addJsonData(dir, filename); | ||
} | ||
} | ||
} | ||
return { | ||
dataList, | ||
sourceCache | ||
}; | ||
}; | ||
module.exports = { | ||
getCoverageDataList, | ||
getInputData, | ||
generateCoverageReports | ||
}; |
@@ -40,5 +40,2 @@ declare namespace MCR { | ||
}] | | ||
['console-summary'] | ['console-summary', { | ||
metrics?: Array<"bytes" | "functions" | "branches" | "lines" | "statements">; | ||
}] | | ||
['clover'] | ['clover', { | ||
@@ -95,2 +92,12 @@ file?: string; | ||
file?: string; | ||
}] | | ||
['console-summary'] | ['console-summary', { | ||
metrics?: Array<"bytes" | "functions" | "branches" | "lines" | "statements">; | ||
}] | | ||
['raw'] | ['raw', { | ||
outputDir?: string; | ||
}] | | ||
[string] | [string, { | ||
type?: "v8" | "istanbul" | "both"; | ||
[key: string]: any; | ||
}]; | ||
@@ -177,2 +184,5 @@ | ||
/** {string|string[]} input raw dir(s) */ | ||
inputDir?: string | string[]; | ||
/** {string} v8 or html for istanbul by default | ||
@@ -179,0 +189,0 @@ * {array} multiple reports with options |
@@ -7,3 +7,3 @@ const fs = require('fs'); | ||
const { initV8ListAndSourcemap } = require('./v8/v8.js'); | ||
const { getCoverageDataList, generateCoverageReports } = require('./generate.js'); | ||
const { getInputData, generateCoverageReports } = require('./generate.js'); | ||
@@ -45,6 +45,8 @@ class CoverageReport { | ||
const time_start = Date.now(); | ||
this.initOptions(); | ||
if (!Util.checkCoverageData(data)) { | ||
Util.logError(`The coverage data must be Array(V8) or Object(Istanbul): ${this.options.name}`); | ||
Util.logError(`${this.options.name}: The added coverage data must be Array(V8) or Object(Istanbul)`); | ||
return; | ||
@@ -74,2 +76,4 @@ } | ||
Util.logTime(`added coverage data: ${results.type}`, time_start); | ||
return results; | ||
@@ -81,11 +85,28 @@ } | ||
if (!this.hasCache()) { | ||
Util.logError(`Not found coverage cache: ${this.options.cacheDir}`); | ||
const time_start = Date.now(); | ||
this.initOptions(); | ||
const { inputDir, cacheDir } = this.options; | ||
const inputDirs = Util.toList(inputDir, ','); | ||
if (this.hasCache()) { | ||
inputDirs.push(cacheDir); | ||
} | ||
const inputList = inputDirs.filter((dir) => { | ||
const hasDir = fs.existsSync(dir); | ||
if (!hasDir) { | ||
Util.logError(`Not found coverage data dir: ${Util.relativePath(dir)}`); | ||
} | ||
return hasDir; | ||
}); | ||
if (!inputList.length) { | ||
return; | ||
} | ||
const cacheDir = this.options.cacheDir; | ||
const dataList = await getCoverageDataList(cacheDir); | ||
if (!dataList) { | ||
Util.logError(`Not found coverage data in cache dir: ${cacheDir}`); | ||
const { dataList, sourceCache } = await getInputData(inputList); | ||
if (!dataList.length) { | ||
const dirs = inputList.map((dir) => Util.relativePath(dir)); | ||
Util.logError(`Not found coverage data in dir(s): ${dirs.join(', ')}`); | ||
return; | ||
@@ -96,11 +117,18 @@ } | ||
const outputDir = this.options.outputDir; | ||
if (fs.existsSync(outputDir)) { | ||
// if assets dir is out of output dir will be ignore | ||
fs.readdirSync(outputDir).forEach((itemName) => { | ||
if (itemName === this.cacheDirName) { | ||
return; | ||
} | ||
Util.rmSync(path.resolve(outputDir, itemName)); | ||
}); | ||
fs.readdirSync(outputDir).forEach((itemName) => { | ||
if (itemName === this.cacheDirName) { | ||
return; | ||
} | ||
Util.rmSync(path.resolve(outputDir, itemName)); | ||
}); | ||
} else { | ||
fs.mkdirSync(outputDir, { | ||
recursive: true | ||
}); | ||
} | ||
const coverageResults = await generateCoverageReports(dataList, this.options); | ||
const coverageResults = await generateCoverageReports(dataList, sourceCache, this.options); | ||
@@ -111,2 +139,4 @@ if (this.logging !== 'debug') { | ||
Util.logTime(`generated coverage reports: ${coverageResults.reportPath}`, time_start); | ||
const onEnd = this.options.onEnd; | ||
@@ -113,0 +143,0 @@ if (typeof onEnd === 'function') { |
@@ -55,3 +55,3 @@ const fs = require('fs'); | ||
// values can be nested/flat/pkg. Defaults to 'pkg' | ||
defaultSummarizer: options.defaultSummarizer || 'nested', | ||
defaultSummarizer: options.defaultSummarizer || 'pkg', | ||
@@ -93,3 +93,4 @@ dir: options.outputDir, | ||
Object.keys(istanbulGroup).forEach((reportName) => { | ||
const report = istanbulReports.create(reportName, istanbulGroup[reportName]); | ||
const reportOptions = istanbulGroup[reportName]; | ||
const report = istanbulReports.create(reportName, reportOptions); | ||
report.execute(context); | ||
@@ -120,10 +121,9 @@ }); | ||
const mergeIstanbulCoverage = (dataList, options) => { | ||
const mergeIstanbulCoverage = (dataList) => { | ||
const istanbulCoverageList = dataList.map((it) => it.data); | ||
const coverageMap = istanbulLibCoverage.createCoverageMap(); | ||
dataList.forEach((d) => { | ||
coverageMap.merge(d.data); | ||
istanbulCoverageList.forEach((coverage) => { | ||
coverageMap.merge(coverage); | ||
}); | ||
const istanbulData = coverageMap.toJSON(); | ||
return istanbulData; | ||
@@ -130,0 +130,0 @@ }; |
@@ -112,2 +112,15 @@ const Util = { | ||
toList: function(data, separator) { | ||
if (data instanceof Array) { | ||
return data; | ||
} | ||
if (typeof data === 'string' && (typeof separator === 'string' || separator instanceof RegExp)) { | ||
return data.split(separator).map((str) => str.trim()).filter((str) => str); | ||
} | ||
if (typeof data === 'undefined' || data === null) { | ||
return []; | ||
} | ||
return [data]; | ||
}, | ||
forEach: function(rootList, callback) { | ||
@@ -114,0 +127,0 @@ const isBreak = (res) => { |
@@ -17,3 +17,3 @@ | ||
// apply directly to css ranges | ||
const dedupeRanges = (ranges) => { | ||
const dedupeFlatRanges = (ranges) => { | ||
@@ -63,7 +63,6 @@ ranges = filterRanges(ranges); | ||
// apply to js count ranges | ||
const dedupeCountRanges = (ranges) => { | ||
const mergeRangesWith = (ranges, comparer, handler) => { | ||
// count ranges format | ||
// { start: 0, end: 6, count: 0 } | ||
// ranges format | ||
// { start: 0, end: 6, ... } | ||
@@ -82,6 +81,7 @@ ranges = filterRanges(ranges); | ||
ranges.reduce((lastRange, range) => { | ||
if (range.start === lastRange.start && range.end === lastRange.end) { | ||
if (comparer(lastRange, range)) { | ||
range.dedupe = true; | ||
lastRange.count += range.count; | ||
handler(lastRange, range); | ||
hasDedupe = true; | ||
@@ -104,6 +104,19 @@ | ||
// apply to js count ranges | ||
const dedupeCountRanges = (ranges) => { | ||
const comparer = (lastRange, range) => { | ||
return lastRange.start === range.start && lastRange.end === range.end; | ||
}; | ||
const handler = (lastRange, range) => { | ||
lastRange.count += range.count; | ||
}; | ||
return mergeRangesWith(ranges, comparer, handler); | ||
}; | ||
module.exports = { | ||
dedupeRanges, | ||
sortRanges, | ||
dedupeFlatRanges, | ||
mergeRangesWith, | ||
dedupeCountRanges | ||
}; |
@@ -42,2 +42,7 @@ const path = require('path'); | ||
// url could be a absolute path | ||
if (path.isAbsolute(url)) { | ||
url = pathToFileURL(url).toString(); | ||
} | ||
if (url.startsWith('file:')) { | ||
@@ -44,0 +49,0 @@ const relPath = Util.relativePath(fileURLToPath(url)); |
const fs = require('fs'); | ||
const { writeFile, readFile } = require('fs/promises'); | ||
const path = require('path'); | ||
const { pathToFileURL } = require('url'); | ||
const os = require('os'); | ||
@@ -51,3 +52,3 @@ const crypto = require('crypto'); | ||
calculateSha1(buffer) { | ||
calculateSha1: (buffer) => { | ||
const hash = crypto.createHash('sha1'); | ||
@@ -208,2 +209,30 @@ hash.update(buffer); | ||
runCustomReporter: async (reportName, reportData, reportOptions, globalOptions) => { | ||
let CustomReporter; | ||
let err; | ||
try { | ||
CustomReporter = await import(reportName); | ||
} catch (e) { | ||
err = e; | ||
try { | ||
CustomReporter = await import(pathToFileURL(reportName)); | ||
} catch (ee) { | ||
err = ee; | ||
} | ||
} | ||
if (!CustomReporter) { | ||
Util.logError(err.message); | ||
return; | ||
} | ||
CustomReporter = CustomReporter.default || CustomReporter; | ||
const reporter = new CustomReporter(reportOptions, globalOptions); | ||
const results = await reporter.generate(reportData); | ||
return results; | ||
}, | ||
getEOL: function(content) { | ||
@@ -210,0 +239,0 @@ if (!content) { |
171
lib/v8/v8.js
const path = require('path'); | ||
const Util = require('../utils/util.js'); | ||
const { getV8Summary } = require('./v8-summary.js'); | ||
const { dedupeRanges } = require('../utils/dedupe.js'); | ||
const { dedupeFlatRanges } = require('../utils/dedupe.js'); | ||
const { getSourcePath } = require('../utils/source-path.js'); | ||
@@ -10,2 +10,16 @@ const { mergeScriptCovs } = require('../packages/monocart-coverage-vendor.js'); | ||
const getWrapperSource = (offset, source) => { | ||
// NO \n because it break line number in mappings | ||
let startStr = ''; | ||
if (offset >= 4) { | ||
// fill comments | ||
const spaces = ''.padEnd(offset - 4, '*'); | ||
startStr = `/*${spaces}*/`; | ||
} else { | ||
// fill spaces (should no chance) | ||
startStr += ''.padEnd(offset, ' '); | ||
} | ||
return startStr + source; | ||
}; | ||
const initV8ListAndSourcemap = async (v8list, options) => { | ||
@@ -22,3 +36,2 @@ | ||
// keep functions | ||
v8list = v8list.filter((item) => { | ||
@@ -53,34 +66,34 @@ if (typeof item.source === 'string' && item.functions) { | ||
const type = item.type; | ||
let source = item.source || item.text; | ||
// fix source with script offset | ||
let scriptOffset; | ||
const offset = Util.toNum(item.scriptOffset, true); | ||
if (offset > 0) { | ||
scriptOffset = offset; | ||
source = getWrapperSource(offset, source); | ||
} | ||
const data = { | ||
url: item.url, | ||
type | ||
type: item.type, | ||
source, | ||
// script offset >= 0, for vm script | ||
scriptOffset, | ||
// Manually Resolve the Sourcemap | ||
sourceMap: item.sourceMap, | ||
// empty coverage | ||
empty: item.empty, | ||
// match source if using empty coverage | ||
distFile: item.distFile | ||
}; | ||
if (type === 'js') { | ||
// coverage | ||
if (data.type === 'js') { | ||
data.functions = item.functions; | ||
data.source = item.source; | ||
// could be existed | ||
data.sourceMap = item.sourceMap; | ||
} else { | ||
// css | ||
data.ranges = item.ranges; | ||
data.source = item.text; | ||
} | ||
// no functions and ranges | ||
if (item.empty) { | ||
data.empty = true; | ||
} | ||
const idList = []; | ||
// could be existed | ||
const distFile = item.distFile; | ||
if (distFile) { | ||
data.distFile = distFile; | ||
idList.push(distFile); | ||
} | ||
// resolve source path | ||
let sourcePath = getSourcePath(data.url, i + 1, data.type); | ||
@@ -93,10 +106,7 @@ if (typeof sourcePathHandler === 'function') { | ||
} | ||
data.sourcePath = sourcePath; | ||
idList.push(sourcePath); | ||
idList.push(data.source); | ||
// calculate source id | ||
data.id = Util.calculateSha1(sourcePath + data.source); | ||
data.id = Util.calculateSha1(idList.join('')); | ||
return data; | ||
@@ -110,3 +120,3 @@ }); | ||
// debug level time | ||
Util.logTime(`loaded ${count} sourcemaps`, time_start); | ||
Util.logTime(`loaded sourcemaps: ${count}`, time_start); | ||
} | ||
@@ -129,3 +139,3 @@ | ||
// ranges: [ {start, end} ] | ||
const ranges = dedupeRanges(concatRanges); | ||
const ranges = dedupeFlatRanges(concatRanges); | ||
@@ -146,3 +156,3 @@ resolve(ranges || []); | ||
const mergeV8Coverage = async (dataList, options) => { | ||
const mergeV8Coverage = async (dataList, sourceCache, options) => { | ||
@@ -154,2 +164,3 @@ let allList = []; | ||
// remove empty items | ||
const coverageList = allList.filter((it) => !it.empty); | ||
@@ -187,2 +198,3 @@ | ||
// first time filter for empty, (not for sources) | ||
// empty and not in item map | ||
const emptyList = allList.filter((it) => it.empty).filter((it) => !itemMap[it.id]); | ||
@@ -198,10 +210,8 @@ | ||
} = item; | ||
const sourcePath = Util.resolveCacheSourcePath(options.cacheDir, id); | ||
const content = await Util.readFile(sourcePath); | ||
if (content) { | ||
const json = JSON.parse(content); | ||
const json = sourceCache.get(id); | ||
if (json) { | ||
item.source = json.source; | ||
item.sourceMap = json.sourceMap; | ||
} else { | ||
Util.logError(`failed to read source: ${item.url}`); | ||
Util.logError(`Not found source data: ${Util.relativePath(item.sourcePath)}`); | ||
item.source = ''; | ||
@@ -234,10 +244,9 @@ } | ||
const saveCodecovReport = async (reportData, options, codecovOptions) => { | ||
const mergedOptions = { | ||
const saveCodecovReport = async (reportData, reportOptions, options) => { | ||
const codecovOptions = { | ||
outputFile: 'codecov.json', | ||
... codecovOptions | ||
... reportOptions | ||
}; | ||
// console.log(mergedOptions); | ||
const jsonPath = path.resolve(options.outputDir, mergedOptions.outputFile); | ||
const jsonPath = path.resolve(options.outputDir, codecovOptions.outputFile); | ||
@@ -258,10 +267,10 @@ // https://docs.codecov.com/docs/codecov-custom-coverage-format | ||
const saveV8JsonReport = async (reportData, options, v8JsonOptions) => { | ||
const mergedOptions = { | ||
const saveV8JsonReport = async (reportData, reportOptions, options) => { | ||
const v8JsonOptions = { | ||
outputFile: 'coverage-report.json', | ||
... v8JsonOptions | ||
... reportOptions | ||
}; | ||
// console.log(mergedOptions); | ||
const jsonPath = path.resolve(options.outputDir, mergedOptions.outputFile); | ||
const jsonPath = path.resolve(options.outputDir, v8JsonOptions.outputFile); | ||
await Util.writeFile(jsonPath, JSON.stringify(reportData)); | ||
@@ -272,4 +281,16 @@ return Util.relativePath(jsonPath); | ||
const saveV8HtmlReport = async (reportData, options, v8HtmlOptions) => { | ||
const saveV8HtmlReport = async (reportData, reportOptions, options) => { | ||
// V8 only options, merged with root options | ||
const v8HtmlOptions = { | ||
outputFile: options.outputFile, | ||
inline: options.inline, | ||
assetsPath: options.assetsPath, | ||
metrics: options.metrics, | ||
... reportOptions | ||
}; | ||
// add metrics to data for UI | ||
reportData.metrics = v8HtmlOptions.metrics; | ||
const { | ||
@@ -330,47 +351,25 @@ outputFile, inline, assetsPath | ||
let outputPath; | ||
// v8 reports | ||
const buildInV8Reports = { | ||
'v8': saveV8HtmlReport, | ||
'v8-json': saveV8JsonReport, | ||
'codecov': saveCodecovReport | ||
}; | ||
// v8 reports | ||
const outputs = {}; | ||
const v8Group = options.reportGroup.v8; | ||
// v8 html | ||
const v8Options = v8Group.v8; | ||
if (v8Options) { | ||
// V8 only options, merged with root options | ||
const v8HtmlOptions = { | ||
outputFile: options.outputFile, | ||
inline: options.inline, | ||
assetsPath: options.assetsPath, | ||
metrics: options.metrics, | ||
... v8Options | ||
}; | ||
// add metrics to data | ||
reportData.metrics = v8HtmlOptions.metrics; | ||
outputPath = await saveV8HtmlReport(reportData, options, v8HtmlOptions); | ||
} | ||
// v8 json (after html for metrics data) | ||
const v8JsonOptions = v8Group['v8-json']; | ||
if (v8JsonOptions) { | ||
const jsonPath = await saveV8JsonReport(reportData, options, v8JsonOptions); | ||
if (!outputPath) { | ||
outputPath = jsonPath; | ||
const v8Reports = Object.keys(v8Group); | ||
for (const reportName of v8Reports) { | ||
const reportOptions = v8Group[reportName]; | ||
const buildInHandler = buildInV8Reports[reportName]; | ||
if (buildInHandler) { | ||
outputs[reportName] = await buildInHandler(reportData, reportOptions, options); | ||
} else { | ||
outputs[reportName] = await Util.runCustomReporter(reportName, reportData, reportOptions, options); | ||
} | ||
} | ||
// codecov | ||
const codecovOptions = v8Group.codecov; | ||
if (codecovOptions) { | ||
const jsonPath = await saveCodecovReport(reportData, options, codecovOptions); | ||
if (!outputPath) { | ||
outputPath = jsonPath; | ||
} | ||
} | ||
// outputPath last one is html | ||
// outputPath, should be html or json | ||
const reportPath = Util.resolveReportPath(options, () => { | ||
return outputPath; | ||
return outputs.v8 || outputs['v8-json'] || Object.values(outputs).filter((it) => it && typeof it === 'string').shift(); | ||
}); | ||
@@ -377,0 +376,0 @@ |
{ | ||
"name": "monocart-coverage-reports", | ||
"version": "2.2.2", | ||
"version": "2.3.0", | ||
"description": "Monocart coverage reports", | ||
@@ -23,3 +23,3 @@ "main": "./lib/index.js", | ||
"build": "sf lint && sf b -p && npm run build-test", | ||
"test-node": "npm run test-node-env && npm run test-node-api && npm run test-node-ins && npm run test-node-cdp && npm run test-node-koa", | ||
"test-node": "npm run test-node-env && npm run test-node-api && npm run test-node-ins && npm run test-node-cdp && npm run test-node-koa && npm run test-vm", | ||
"test-node-env": "cross-env NODE_V8_COVERAGE=.temp/v8-coverage-env node ./test/test-node-env.js && node ./test/generate-node-report.js", | ||
@@ -31,5 +31,7 @@ "test-node-api": "cross-env NODE_V8_COVERAGE=.temp/v8-coverage-api node ./test/test-node-api.js", | ||
"test-node-koa": "node ./test/test-node-koa.js", | ||
"test-vm": "node ./test/test-vm.js", | ||
"test-browser": "node ./test/test.js", | ||
"test-cli": "npx mcr \"node ./test/test-node-env.js\" -o docs/cli -c test/cli-options.js", | ||
"test": "npm run test-browser && npm run test-node && npm run test-cli && npm run build-docs", | ||
"test-merge": "node ./test/test-merge.js", | ||
"test": "npm run test-browser && npm run test-node && npm run test-cli && npm run test-merge && npm run build-docs", | ||
"dev": "sf d v8", | ||
@@ -36,0 +38,0 @@ "open": "node ./scripts/open.js", |
166
README.md
@@ -21,4 +21,7 @@ # Monocart Coverage Reports | ||
* [Collecting V8 Coverage Data](#collecting-v8-coverage-data) | ||
* [Manually Resolve the Sourcemap](#manually-resolve-the-sourcemap) | ||
* [Collecting Raw V8 Coverage Data with Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer) | ||
* [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side) | ||
* [Multiprocessing Support](#multiprocessing-support) | ||
* [Merge Coverage Reports](#merge-coverage-reports) | ||
* [Integration](#integration) | ||
@@ -70,7 +73,6 @@ * [Ignoring Uncovered Codes](#ignoring-uncovered-codes) | ||
- coverage data for [Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format), see [example](https://app.codecov.io/github/cenfun/monocart-coverage-reports) | ||
- `console-summary` shows coverage summary in console | ||
![](test/console-summary.png) | ||
> Following are [istanbul reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib) | ||
> Istanbul [build-in reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib) | ||
- `clover` | ||
@@ -95,2 +97,31 @@ - `cobertura` | ||
> Other reports | ||
- `console-summary` shows coverage summary in console | ||
- `raw` only keep all original data, which can be used for other reports input with `inputDir` | ||
- see [Merge Coverage Reports](#merge-coverage-reports) | ||
- Custom Reporter | ||
```js | ||
{ | ||
reports: [ | ||
[path.resolve('./test/custom-istanbul-reporter.js'), { | ||
type: 'istanbul', | ||
file: 'custom-istanbul-coverage.text' | ||
}], | ||
[path.resolve('./test/custom-v8-reporter.js'), { | ||
type: 'v8', | ||
outputFile: 'custom-v8-coverage.json' | ||
}], | ||
[path.resolve('./test/custom-v8-reporter.mjs'), { | ||
type: 'both' | ||
}] | ||
] | ||
} | ||
``` | ||
- istanbul custom reporter | ||
> example: [./test/custom-istanbul-reporter.js](./test/custom-istanbul-reporter.js), see [istanbul built-in reporters' implementation](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib) for reference, | ||
- v8 custom reporter | ||
> example: [./test/custom-v8-reporter.js](./test/custom-v8-reporter.js) | ||
### Multiple Reports: | ||
```js | ||
@@ -101,2 +132,3 @@ const MCR = require('monocart-coverage-reports'); | ||
reports: [ | ||
// build-in reports | ||
['console-summary'], | ||
@@ -110,3 +142,14 @@ ['v8'], | ||
}], | ||
'lcovonly' | ||
'lcovonly', | ||
// custom reports | ||
// Specify reporter name with the NPM package | ||
["custom-reporter-1"], | ||
["custom-reporter-2", { | ||
type: "istanbul", | ||
option: "value" | ||
}], | ||
// Specify reporter name with local path | ||
['/absolute/path/to/custom-reporter.js'] | ||
] | ||
@@ -258,10 +301,45 @@ } | ||
- [esbuild](https://esbuild.github.io/api/): `sourcemap: true` and `minify: false` | ||
- [Manually Resolve the Sourcemap](#manually-resolve-the-sourcemap) | ||
- Browser (Chromium Only) | ||
> Collecting coverage data with [Chromium Coverage API](#chromium-coverage-api): | ||
- [Playwright example](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-v8.js), and [anonymous](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-anonymous.js), [css](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-css.js) | ||
- [Puppeteer example](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-puppeteer.js) | ||
- see [Collecting Raw V8 Coverage Data with Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer) | ||
- Node.js | ||
- see following [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side) | ||
- see [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side) | ||
## Manually Resolve the Sourcemap | ||
> If the `js` file is loaded with `addScriptTag` [API](https://playwright.dev/docs/api/class-page#page-add-script-tag), then its sourcemap file may not work. You can try to manually read the sourcemap file before the coverage data is added to the report. | ||
```js | ||
const jsCoverage = await page.coverage.stopJSCoverage(); | ||
jsCoverage.forEach((entry) => { | ||
// read sourcemap for the my-dist.js manually | ||
if (entry.url.endsWith('my-dist.js')) { | ||
entry.sourceMap = JSON.parse(fs.readFileSync('dist/my-dist.js.map').toString('utf-8')); | ||
} | ||
}); | ||
await MCR(coverageOptions).add(jsCoverage); | ||
``` | ||
## Collecting Raw V8 Coverage Data with Puppeteer | ||
> Puppeteer does not provide raw v8 coverage data by default. A simple conversion is required, see example: [./test/test-puppeteer.js](./test/test-puppeteer.js) | ||
```js | ||
await page.coverage.startJSCoverage({ | ||
// provide raw v8 coverage data | ||
includeRawScriptCoverage: true | ||
}); | ||
await page.goto(url); | ||
const jsCoverage = await page.coverage.stopJSCoverage(); | ||
const rawV8CoverageData = jsCoverage.map((it) => { | ||
// Convert to raw v8 coverage format | ||
return { | ||
source: it.text, | ||
... it.rawScriptCoverage | ||
}; | ||
} | ||
``` | ||
## Node.js V8 Coverage Report for Server Side | ||
@@ -328,5 +406,61 @@ Possible solutions: | ||
## Merge Coverage Reports | ||
The following usage scenarios may require merging coverage reports: | ||
- When the code is executed in different environments, like Node.js Server Side and browser Client Side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage. | ||
- When the code is subjected to different kinds of testing. For example, unit tests with Jest might cover certain parts of the code, while end-to-end tests with Playwright might cover other parts. Merging these different coverage reports can provide a holistic view of what code has been tested. | ||
- When tests are run on different machines or different shards, each might produce its own coverage report. Merging these can give a complete picture of the test coverage across all machines or shards. | ||
First, using the `raw` report to export the original coverage data to the specified directory. | ||
```js | ||
const coverageOptions = { | ||
name: 'My Unit Test Coverage Report', | ||
outputDir: "./coverage-reports/unit", | ||
reports: [ | ||
['raw', { | ||
// relative path will be "./coverage-reports/unit/raw" | ||
outputDir: "raw" | ||
}], | ||
['v8'], | ||
['console-summary'] | ||
] | ||
}; | ||
``` | ||
Then, after all the tests are completed, generate a merged report with option `inputDir`: | ||
```js | ||
import {CoverageReport} from 'monocart-coverage-reports'; | ||
const coverageOptions = { | ||
name: 'My Merged Coverage Report', | ||
inputDir: [ | ||
'./coverage-reports/unit/raw', | ||
'./coverage-reports/e2e/raw' | ||
], | ||
outputDir: './coverage-reports/merged', | ||
reports: [ | ||
['v8'], | ||
['console-summary'] | ||
] | ||
}; | ||
await new CoverageReport(coverageOptions).generate(); | ||
``` | ||
If the source file comes from the sourcemap, then its path is a virtual path. Using the `sourcePath` option to convert it. | ||
```js | ||
const coverageOptions = { | ||
sourcePath: (filePath) => { | ||
// Remove the virtual prefix | ||
const list = ['my-dist-file1/', 'my-dist-file2/']; | ||
for (const str of list) { | ||
if (filePath.startsWith(str)) { | ||
return filePath.slice(str.length); | ||
} | ||
} | ||
return filePath; | ||
} | ||
}; | ||
``` | ||
see example: [./test/test-merge.js](./test/test-merge.js) | ||
## Integration | ||
- [monocart-reporter](https://cenfun.github.io/monocart-reporter/) - Test reporter for [Playwright](https://github.com/microsoft/playwright) | ||
- [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - Integration with [Vitest](https://github.com/vitest-dev/vitest) coverage | ||
- [jest-monocart-coverage](https://github.com/cenfun/jest-monocart-coverage) - Integration with [Jest](https://github.com/jestjs/jest/) for coverage reports | ||
- [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - Integration with [Vitest](https://github.com/vitest-dev/vitest) for coverage reports | ||
@@ -404,22 +538,2 @@ ## Ignoring Uncovered Codes | ||
### Collect raw v8 coverage data with Puppeteer | ||
```js | ||
await page.coverage.startJSCoverage({ | ||
// provide raw v8 coverage data | ||
includeRawScriptCoverage: true | ||
}); | ||
await page.goto(url); | ||
const jsCoverage = await page.coverage.stopJSCoverage(); | ||
const rawV8CoverageData = jsCoverage.map((it) => { | ||
// Convert to raw v8 coverage format | ||
return { | ||
source: it.text, | ||
... it.rawScriptCoverage | ||
}; | ||
} | ||
``` | ||
see example: [./test/test-puppeteer.js](./test/test-puppeteer.js) | ||
## How to convert V8 to Istanbul | ||
@@ -426,0 +540,0 @@ ### Using [v8-to-istanbul](https://github.com/istanbuljs/v8-to-istanbul) |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
889196
239764
6887
584
1
31