|
|
const Exclude = require('test-exclude') |
|
|
const libCoverage = require('istanbul-lib-coverage') |
|
|
const libReport = require('istanbul-lib-report') |
|
|
const reports = require('istanbul-reports') |
|
|
let readFile |
|
|
try { |
|
|
;({ readFile } = require('fs/promises')) |
|
|
} catch (err) { |
|
|
;({ readFile } = require('fs').promises) |
|
|
} |
|
|
const { readdirSync, readFileSync, statSync } = require('fs') |
|
|
const { isAbsolute, resolve, extname } = require('path') |
|
|
const { pathToFileURL, fileURLToPath } = require('url') |
|
|
const getSourceMapFromFile = require('./source-map-from-file') |
|
|
|
|
|
const v8toIstanbul = require('v8-to-istanbul') |
|
|
const util = require('util') |
|
|
const debuglog = util.debuglog('c8') |
|
|
|
|
|
class Report { |
|
|
constructor ({ |
|
|
exclude, |
|
|
extension, |
|
|
excludeAfterRemap, |
|
|
include, |
|
|
reporter, |
|
|
reporterOptions, |
|
|
reportsDirectory, |
|
|
tempDirectory, |
|
|
watermarks, |
|
|
omitRelative, |
|
|
wrapperLength, |
|
|
resolve: resolvePaths, |
|
|
all, |
|
|
src, |
|
|
allowExternal = false, |
|
|
skipFull, |
|
|
excludeNodeModules, |
|
|
mergeAsync |
|
|
}) { |
|
|
this.reporter = reporter |
|
|
this.reporterOptions = reporterOptions || {} |
|
|
this.reportsDirectory = reportsDirectory |
|
|
this.tempDirectory = tempDirectory |
|
|
this.watermarks = watermarks |
|
|
this.resolve = resolvePaths |
|
|
this.exclude = new Exclude({ |
|
|
exclude: exclude, |
|
|
include: include, |
|
|
extension: extension, |
|
|
relativePath: !allowExternal, |
|
|
excludeNodeModules: excludeNodeModules |
|
|
}) |
|
|
this.excludeAfterRemap = excludeAfterRemap |
|
|
this.shouldInstrumentCache = new Map() |
|
|
this.omitRelative = omitRelative |
|
|
this.sourceMapCache = {} |
|
|
this.wrapperLength = wrapperLength |
|
|
this.all = all |
|
|
this.src = this._getSrc(src) |
|
|
this.skipFull = skipFull |
|
|
this.mergeAsync = mergeAsync |
|
|
} |
|
|
|
|
|
_getSrc (src) { |
|
|
if (typeof src === 'string') { |
|
|
return [src] |
|
|
} else if (Array.isArray(src)) { |
|
|
return src |
|
|
} else { |
|
|
return [process.cwd()] |
|
|
} |
|
|
} |
|
|
|
|
|
async run () { |
|
|
const context = libReport.createContext({ |
|
|
dir: this.reportsDirectory, |
|
|
watermarks: this.watermarks, |
|
|
coverageMap: await this.getCoverageMapFromAllCoverageFiles() |
|
|
}) |
|
|
|
|
|
for (const _reporter of this.reporter) { |
|
|
reports.create(_reporter, { |
|
|
skipEmpty: false, |
|
|
skipFull: this.skipFull, |
|
|
maxCols: process.stdout.columns || 100, |
|
|
...this.reporterOptions[_reporter] |
|
|
}).execute(context) |
|
|
} |
|
|
} |
|
|
|
|
|
async getCoverageMapFromAllCoverageFiles () { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this._allCoverageFiles) return this._allCoverageFiles |
|
|
|
|
|
const map = libCoverage.createCoverageMap() |
|
|
let v8ProcessCov |
|
|
|
|
|
if (this.mergeAsync) { |
|
|
v8ProcessCov = await this._getMergedProcessCovAsync() |
|
|
} else { |
|
|
v8ProcessCov = this._getMergedProcessCov() |
|
|
} |
|
|
const resultCountPerPath = new Map() |
|
|
|
|
|
for (const v8ScriptCov of v8ProcessCov.result) { |
|
|
try { |
|
|
const sources = this._getSourceMap(v8ScriptCov) |
|
|
const path = resolve(this.resolve, v8ScriptCov.url) |
|
|
const converter = v8toIstanbul(path, this.wrapperLength, sources, (path) => { |
|
|
if (this.excludeAfterRemap) { |
|
|
return !this._shouldInstrument(path) |
|
|
} |
|
|
}) |
|
|
await converter.load() |
|
|
|
|
|
if (resultCountPerPath.has(path)) { |
|
|
resultCountPerPath.set(path, resultCountPerPath.get(path) + 1) |
|
|
} else { |
|
|
resultCountPerPath.set(path, 0) |
|
|
} |
|
|
|
|
|
converter.applyCoverage(v8ScriptCov.functions) |
|
|
map.merge(converter.toIstanbul()) |
|
|
} catch (err) { |
|
|
debuglog(`file: ${v8ScriptCov.url} error: ${err.stack}`) |
|
|
} |
|
|
} |
|
|
|
|
|
this._allCoverageFiles = map |
|
|
return this._allCoverageFiles |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_getSourceMap (v8ScriptCov) { |
|
|
const sources = {} |
|
|
const sourceMapAndLineLengths = this.sourceMapCache[pathToFileURL(v8ScriptCov.url).href] |
|
|
if (sourceMapAndLineLengths) { |
|
|
|
|
|
if (!sourceMapAndLineLengths.data) return |
|
|
sources.sourceMap = { |
|
|
sourcemap: sourceMapAndLineLengths.data |
|
|
} |
|
|
if (sourceMapAndLineLengths.lineLengths) { |
|
|
let source = '' |
|
|
sourceMapAndLineLengths.lineLengths.forEach(length => { |
|
|
source += `${''.padEnd(length, '.')}\n` |
|
|
}) |
|
|
sources.source = source |
|
|
} |
|
|
} |
|
|
return sources |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_getMergedProcessCov () { |
|
|
const { mergeProcessCovs } = require('@bcoe/v8-coverage') |
|
|
const v8ProcessCovs = [] |
|
|
const fileIndex = new Set() |
|
|
for (const v8ProcessCov of this._loadReports()) { |
|
|
if (this._isCoverageObject(v8ProcessCov)) { |
|
|
if (v8ProcessCov['source-map-cache']) { |
|
|
Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(v8ProcessCov['source-map-cache'])) |
|
|
} |
|
|
v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex)) |
|
|
} |
|
|
} |
|
|
|
|
|
if (this.all) { |
|
|
const emptyReports = this._includeUncoveredFiles(fileIndex) |
|
|
v8ProcessCovs.unshift({ |
|
|
result: emptyReports |
|
|
}) |
|
|
} |
|
|
|
|
|
return mergeProcessCovs(v8ProcessCovs) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async _getMergedProcessCovAsync () { |
|
|
const { mergeProcessCovs } = require('@bcoe/v8-coverage') |
|
|
const fileIndex = new Set() |
|
|
let mergedCov = null |
|
|
for (const file of readdirSync(this.tempDirectory)) { |
|
|
try { |
|
|
const rawFile = await readFile( |
|
|
resolve(this.tempDirectory, file), |
|
|
'utf8' |
|
|
) |
|
|
let report = JSON.parse(rawFile) |
|
|
|
|
|
if (this._isCoverageObject(report)) { |
|
|
if (report['source-map-cache']) { |
|
|
Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(report['source-map-cache'])) |
|
|
} |
|
|
report = this._normalizeProcessCov(report, fileIndex) |
|
|
if (mergedCov) { |
|
|
mergedCov = mergeProcessCovs([mergedCov, report]) |
|
|
} else { |
|
|
mergedCov = mergeProcessCovs([report]) |
|
|
} |
|
|
} |
|
|
} catch (err) { |
|
|
debuglog(`${err.stack}`) |
|
|
} |
|
|
} |
|
|
|
|
|
if (this.all) { |
|
|
const emptyReports = this._includeUncoveredFiles(fileIndex) |
|
|
const emptyReport = { |
|
|
result: emptyReports |
|
|
} |
|
|
|
|
|
mergedCov = mergeProcessCovs([emptyReport, mergedCov]) |
|
|
} |
|
|
|
|
|
return mergedCov |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_includeUncoveredFiles (fileIndex) { |
|
|
const emptyReports = [] |
|
|
const workingDirs = this.src |
|
|
const { extension } = this.exclude |
|
|
for (const workingDir of workingDirs) { |
|
|
this.exclude.globSync(workingDir).forEach((f) => { |
|
|
const fullPath = resolve(workingDir, f) |
|
|
if (!fileIndex.has(fullPath)) { |
|
|
const ext = extname(fullPath) |
|
|
if (extension.includes(ext)) { |
|
|
const stat = statSync(fullPath) |
|
|
const sourceMap = getSourceMapFromFile(fullPath) |
|
|
if (sourceMap) { |
|
|
this.sourceMapCache[pathToFileURL(fullPath)] = { data: sourceMap } |
|
|
} |
|
|
emptyReports.push({ |
|
|
scriptId: 0, |
|
|
url: resolve(fullPath), |
|
|
functions: [{ |
|
|
functionName: '(empty-report)', |
|
|
ranges: [{ |
|
|
startOffset: 0, |
|
|
endOffset: stat.size, |
|
|
count: 0 |
|
|
}], |
|
|
isBlockCoverage: true |
|
|
}] |
|
|
}) |
|
|
} |
|
|
} |
|
|
}) |
|
|
} |
|
|
|
|
|
return emptyReports |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_isCoverageObject (maybeV8ProcessCov) { |
|
|
return maybeV8ProcessCov && Array.isArray(maybeV8ProcessCov.result) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_loadReports () { |
|
|
const reports = [] |
|
|
for (const file of readdirSync(this.tempDirectory)) { |
|
|
try { |
|
|
reports.push(JSON.parse(readFileSync( |
|
|
resolve(this.tempDirectory, file), |
|
|
'utf8' |
|
|
))) |
|
|
} catch (err) { |
|
|
debuglog(`${err.stack}`) |
|
|
} |
|
|
} |
|
|
return reports |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_normalizeProcessCov (v8ProcessCov, fileIndex) { |
|
|
const result = [] |
|
|
for (const v8ScriptCov of v8ProcessCov.result) { |
|
|
|
|
|
|
|
|
if (/^node:/.test(v8ScriptCov.url)) { |
|
|
v8ScriptCov.url = `${v8ScriptCov.url.replace(/^node:/, '')}.js` |
|
|
} |
|
|
if (/^file:\/\//.test(v8ScriptCov.url)) { |
|
|
try { |
|
|
v8ScriptCov.url = fileURLToPath(v8ScriptCov.url) |
|
|
fileIndex.add(v8ScriptCov.url) |
|
|
} catch (err) { |
|
|
debuglog(`${err.stack}`) |
|
|
continue |
|
|
} |
|
|
} |
|
|
if ((!this.omitRelative || isAbsolute(v8ScriptCov.url))) { |
|
|
if (this.excludeAfterRemap || this._shouldInstrument(v8ScriptCov.url)) { |
|
|
result.push(v8ScriptCov) |
|
|
} |
|
|
} |
|
|
} |
|
|
return { result } |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_normalizeSourceMapCache (v8SourceMapCache) { |
|
|
const cache = {} |
|
|
for (const fileURL of Object.keys(v8SourceMapCache)) { |
|
|
cache[pathToFileURL(fileURLToPath(fileURL)).href] = v8SourceMapCache[fileURL] |
|
|
} |
|
|
return cache |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_shouldInstrument (filename) { |
|
|
const cacheResult = this.shouldInstrumentCache.get(filename) |
|
|
if (cacheResult !== undefined) { |
|
|
return cacheResult |
|
|
} |
|
|
|
|
|
const result = this.exclude.shouldInstrument(filename) |
|
|
this.shouldInstrumentCache.set(filename, result) |
|
|
return result |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = function (opts) { |
|
|
return new Report(opts) |
|
|
} |
|
|
|