UNPKG

monocart-coverage-reports

Version:

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

344 lines (287 loc) 9.92 kB
const EC = require('eight-colors'); const Util = require('../utils/util.js'); const { getV8Summary } = require('./v8-summary.js'); const { dedupeFlatRanges } = require('../utils/dedupe.js'); const { resolveAnonymousUrl, normalizeSourcePath, getSourcePathReplacer } = require('../utils/source-path.js'); const { collectSourceMaps } = require('../converter/collect-source-maps.js'); const { v8Report } = require('../reports/v8.js'); const { v8JsonReport } = require('../reports/v8-json.js'); const { customReport } = require('../reports/custom.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 (mcr, v8list) => { const { options, fileCache } = mcr; // entry filter const entryFilter = Util.getEntryFilter(options); const lengthBefore = v8list.length; v8list = v8list.filter(entryFilter); const lengthAfter = v8list.length; Util.logFilter('entry filter (add):', lengthBefore, lengthAfter); // filter no source or text // init type and css source from text // keep functions v8list = v8list.filter((entry) => { // console.log(entry.url); if (typeof entry.source === 'string' && entry.functions) { entry.type = 'js'; return true; } if (typeof entry.text === 'string' && entry.ranges) { entry.type = 'css'; return true; } // allow empty coverage if (entry.empty) { if (!['js', 'css'].includes(entry.type)) { entry.type = 'js'; } return true; } // 404 css, text will be empty Util.logDebug(EC.red(`Invalid source or coverage data: ${entry.url}`)); // console.log(entry); }); // onEntry hook // after filter but before init, because id depends source and sourcePath const onEntry = options.onEntry; if (typeof onEntry === 'function') { for (const entry of v8list) { await onEntry(entry); } } const sourcePathReplacer = getSourcePathReplacer(options); const baseDir = options.baseDir; // init id, sourcePath v8list = v8list.map((entry, i) => { // it could be a empty file let source = `${entry.source || entry.text || ''}`; // fix source with script offset let scriptOffset; const offset = Util.toNum(entry.scriptOffset, true); if (offset > 0) { scriptOffset = offset; source = getWrapperSource(offset, source); } const data = { url: entry.url, type: entry.type, source, // script offset >= 0, for vm script scriptOffset, // Manually Resolve the Sourcemap sourceMap: entry.sourceMap, // fake source with `lineLengths` fake: entry.fake, // empty coverage empty: entry.empty, // match source if using empty coverage distFile: entry.distFile }; // coverage info if (data.type === 'css') { data.ranges = entry.ranges; } else { data.functions = entry.functions; } // resolve source path const sourceUrl = resolveAnonymousUrl(data.url, i + 1, data.type); let sourcePath = normalizeSourcePath(sourceUrl, baseDir); if (sourcePathReplacer) { const newSourcePath = sourcePathReplacer(sourcePath, data); if (typeof newSourcePath === 'string' && newSourcePath) { sourcePath = newSourcePath; } } data.sourcePath = sourcePath; // console.log(data.url, sourcePath); // calculate source id data.id = Util.calculateSha1(sourcePath + data.source); return data; }); // collect sourcemap first const time_start = Date.now(); const { sourceList, sourcemapList } = await collectSourceMaps(v8list, options); if (sourcemapList.length) { // debug level time Util.logTime(`loaded sourcemaps: ${sourcemapList.length}`, time_start); // console.log(smList); } // save source list for (const sourceData of sourceList) { await Util.saveSourceCacheFile(sourceData, options, fileCache); } return v8list; }; // ======================================================================================================== // force to async const mergeCssRanges = (itemList) => { return new Promise((resolve) => { let concatRanges = []; itemList.forEach((item) => { concatRanges = concatRanges.concat(item.ranges); }); // ranges: [ {start, end} ] const ranges = dedupeFlatRanges(concatRanges); resolve(ranges); }); }; const mergeJsFunctions = async (itemList) => { const res = await Util.mergeV8Coverage(itemList); return res.functions; }; const mergeV8DataList = async (dataList, options) => { const allList = dataList.map((d) => d.data).flat(); // separate coverage and empty items const coverageList = []; const allEmptyList = []; allList.forEach((it) => { if (it.empty) { allEmptyList.push(it); } else { coverageList.push(it); } }); // connect all functions and ranges const itemMap = {}; const sourcePathMap = {}; const mergeMap = {}; coverageList.forEach((item) => { const { id, sourcePath } = item; const prev = itemMap[id]; if (prev) { if (mergeMap[id]) { mergeMap[id].push(item); } else { mergeMap[id] = [prev, item]; } } else { itemMap[id] = item; sourcePathMap[sourcePath] = item; } }); // merge functions and ranges const mergeIds = Object.keys(mergeMap); for (const id of mergeIds) { const itemList = mergeMap[id]; const item = itemMap[id]; if (item.type === 'css') { item.ranges = await mergeCssRanges(itemList); } else { item.functions = await mergeJsFunctions(itemList); } } // first time filter for empty, (not for sources) // empty and not in item map const emptyList = allEmptyList.filter((it) => { if (itemMap[it.id]) { return false; } if (sourcePathMap[it.sourcePath]) { return false; } return true; }); // merged list + empty list const mergedList = Object.values(itemMap).concat(emptyList); return mergedList; }; const mergeV8Coverage = async (dataList, sourceCache, options) => { const mergedList = await mergeV8DataList(dataList, options); // try to load coverage and source by id for (const entry of mergedList) { const json = sourceCache.get(entry.id); if (json) { entry.source = json.source; entry.sourceMap = json.sourceMap; } else { Util.logError(`Not found source data: ${Util.relativePath(entry.sourcePath)}`); entry.source = ''; } // add empty coverage after source init if (entry.empty) { Util.setEmptyV8Coverage(entry); } } // GC sourceCache.clear(); return mergedList; }; // ============================================================ const saveV8Report = async (v8list, options, istanbulReportPath) => { const defaultWatermarks = { bytes: [50, 80], statements: [50, 80], branches: [50, 80], functions: [50, 80], lines: [50, 80] }; const watermarks = Util.resolveWatermarks(defaultWatermarks, options.watermarks); const summary = getV8Summary(v8list, watermarks); const reportData = { type: 'v8', // override with saved file path reportPath: '', version: Util.version, name: options.name, watermarks, summary, files: v8list }; // v8 only reports const buildInV8Reports = { 'v8': v8Report, 'v8-json': v8JsonReport }; const outputs = {}; const v8Group = options.reportGroup.get('v8'); // could be no v8 only reports, but have `both` data reports if (v8Group) { for (const [reportName, reportOptions] of v8Group) { const buildInHandler = buildInV8Reports[reportName]; const t1 = Date.now(); if (buildInHandler) { outputs[reportName] = await buildInHandler(reportData, reportOptions, options); } else { outputs[reportName] = await customReport(reportName, reportData, reportOptions, options); } Util.logTime(`${EC.magenta('├')} [generate] saved report: ${reportName}`, t1); } } if (istanbulReportPath) { outputs.istanbul = istanbulReportPath; } // outputPath, should be html or json const reportPath = Util.resolveReportPath(options, () => { return outputs.v8 || outputs.istanbul || Object.values(outputs).filter((it) => it && typeof it === 'string').shift(); }); const coverageResults = { type: 'v8', // html or json reportPath, version: Util.version, name: options.name, watermarks, summary, files: v8list }; return coverageResults; }; module.exports = { initV8ListAndSourcemap, mergeV8DataList, mergeV8Coverage, saveV8Report };