Skip to content

Commit

Permalink
Implement --all functionality
Browse files Browse the repository at this point in the history
Implements an `--all` flag for a src directory to consider
for coverage. If supplied, c8 will glob the directory respecting the
`--include` and `--exclude` parameters for src files. All source files
will be included in the final report. If a file is not found in the
v8 coverage output, it will be initialized with an empty v8 record and
reported as 0 lines/branches/functions covered.

Note: This uses the empty v8 approach instead of the empty report approach

Fix html report

--all should be boolean

Update snapshot

fix async function

WIP - changing --all a bit to create a fake v8 coverage entry and additional args changes

WIP - read source maps for faked entries

WIP

WIP

Moved approach to empty v8 blocks
  • Loading branch information
j03m committed Nov 19, 2019
1 parent d0b2eaa commit 2062766
Show file tree
Hide file tree
Showing 23 changed files with 449 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,3 +3,4 @@ node_modules
.nyc_output
coverage
tmp
.idea
3 changes: 2 additions & 1 deletion lib/commands/report.js
Expand Up @@ -19,7 +19,8 @@ exports.outputReport = async function (argv) {
watermarks: argv.watermarks,
resolve: argv.resolve,
omitRelative: argv.omitRelative,
wrapperLength: argv.wrapperLength
wrapperLength: argv.wrapperLength,
all: argv.all
})
await report.run()
if (argv.checkCoverage) checkCoverages(argv, report)
Expand Down
6 changes: 6 additions & 0 deletions lib/parse-args.js
Expand Up @@ -83,6 +83,12 @@ function buildYargs (withCommands = false) {
type: 'boolean',
describe: 'should temp files be deleted before script execution'
})
.options('all', {
default: false,
type: 'boolean',
describe: 'supplying --all will cause c8 to consider all src files in the current working directory ' +
'when the determining coverage. Respects include/exclude.'
})
.pkgConf('c8')
.config(config)
.demandCommand(1)
Expand Down
218 changes: 211 additions & 7 deletions lib/report.js
Expand Up @@ -3,8 +3,8 @@ const furi = require('furi')
const libCoverage = require('istanbul-lib-coverage')
const libReport = require('istanbul-lib-report')
const reports = require('istanbul-reports')
const { readdirSync, readFileSync } = require('fs')
const { isAbsolute, resolve } = require('path')
const { readdirSync, readFileSync, statSync } = require('fs')
const { isAbsolute, resolve, join, relative, extname, dirname } = require('path')
// TODO: switch back to @c88/v8-coverage once patch is landed.
const { mergeProcessCovs } = require('@bcoe/v8-coverage')
const v8toIstanbul = require('v8-to-istanbul')
Expand All @@ -20,7 +20,8 @@ class Report {
watermarks,
omitRelative,
wrapperLength,
resolve: resolvePaths
resolve: resolvePaths,
all
}) {
this.reporter = reporter
this.reportsDirectory = reportsDirectory
Expand All @@ -34,6 +35,8 @@ class Report {
this.omitRelative = omitRelative
this.sourceMapCache = {}
this.wrapperLength = wrapperLength
this.all = all
this.src = process.cwd()
}

async run () {
Expand All @@ -57,8 +60,8 @@ class Report {
// use-case.
if (this._allCoverageFiles) return this._allCoverageFiles

const map = libCoverage.createCoverageMap()
const v8ProcessCov = this._getMergedProcessCov()
const map = libCoverage.createCoverageMap({})
const resultCountPerPath = new Map()
const possibleCjsEsmBridges = new Map()

Expand Down Expand Up @@ -95,11 +98,45 @@ class Report {
map.merge(converter.toIstanbul())
}
}

this._allCoverageFiles = map
return this._allCoverageFiles
}

/**
* v8toIstanbul will return full paths for js files, but in cases where a sourcemap is involved (ts etc)
* it will return a relative path. Normally this is fine, but when using the `--all` option we load files
* in advance and index them by a path. Here we need to decide in advance if we'll handle full or relative
* urls. This function gets Istanbul CoverageMapData and makes sure all paths are relative when --all is
* supplied.
* @param {V8ToIstanbul} converter coverts v8 coverage to Istanbul's format
* @param {Map<string,boolean>} allFilesMap a map of files for the project that allows us to track
* if a file has coverage
* @returns {CoverageMapData}
* @private
*/
_getIstanbulCoverageMap (converter, allFilesMap) {
const istanbulCoverage = converter.toIstanbul()
const mappedPath = Object.keys(istanbulCoverage)[0]
if (this.all && isAbsolute(mappedPath)) {
const coverageData = istanbulCoverage[mappedPath]
const relativeFile = this.relativeToSrc(mappedPath)
const relativePathClone = {
[relativeFile]: coverageData
}
allFilesMap.set(relativeFile, true)
return relativePathClone
} else if (this.all) {
allFilesMap.set(mappedPath, true)
return istanbulCoverage
} else {
return istanbulCoverage
}
}

relativeToSrc (file) {
return join(this.src, relative(this.src, file))
}

/**
* Returns source-map and fake source file, if cached during Node.js'
* execution. This is used to support tools like ts-node, which transpile
Expand Down Expand Up @@ -128,6 +165,29 @@ class Report {
return sources
}

/**
* //TODO: use https://www.npmjs.com/package/convert-source-map
* // no need to roll this ourselves this is already in the dep tree
* https://sourcemaps.info/spec.html
* @param {String} compilation target file
* @returns {String} full path to source map file
* @private
*/
_getSourceMapFromFile (file) {
const fileBody = readFileSync(file).toString()
const sourceMapLineRE = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/mg
const results = fileBody.match(sourceMapLineRE)
if (results !== null) {
const sourceMap = results[results.length - 1].split('=')[1]
if (isAbsolute(sourceMap)) {
return sourceMap
} else {
const base = dirname(file)
return join(base, sourceMap)
}
}
}

/**
* Returns the merged V8 process coverage.
*
Expand All @@ -139,17 +199,160 @@ class Report {
*/
_getMergedProcessCov () {
const v8ProcessCovs = []
const fileIndex = new Map() // Map<string, bool>
for (const v8ProcessCov of this._loadReports()) {
if (this._isCoverageObject(v8ProcessCov)) {
if (v8ProcessCov['source-map-cache']) {
Object.assign(this.sourceMapCache, v8ProcessCov['source-map-cache'])
}
v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov))
v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex))
}
}

if (this.all) {
const emptyReports = []
v8ProcessCovs.unshift({
result: emptyReports
})
const workingDir = process.cwd()
this.exclude.globSync(workingDir).forEach((f) => {
const fullPath = resolve(workingDir, f)
if (!fileIndex.has(fullPath)) {
const ext = extname(f)
if (ext === '.js' || ext === '.ts' || ext === '.mjs') {
const stat = statSync(f)
const sourceMap = this._getSourceMapFromFile(f)
if (sourceMap !== undefined) {
this.sourceMapCache[`file://${fullPath}`] = { data: JSON.parse(readFileSync(sourceMap).toString()) }
}
emptyReports.push({
scriptId: 0,
url: resolve(f),
functions: [{
functionName: '(empty-report)',
ranges: [{
startOffset: 0,
endOffset: stat.size,
count: 0
}],
isBlockCoverage: true
}]
})
}
}
})
}

return mergeProcessCovs(v8ProcessCovs)
}

/**
* If --all is supplied we need to fetch a list of files that respects
* include/exclude that will be used to see the coverage report with
* empty results for unloaded files
* @returns {Array.<string>}
*/
getFileListForAll () {
return this.exclude.globSync(this.src).reduce((allFileList, file) => {
const srcPath = join(this.src, file)
allFileList.set(srcPath, false)
return allFileList
}, new Map())
}

/**
* Iterates over the entries of `allFilesMap` and where an entries' boolean
* value is false, generate an empty coverage record for the file in question.
* @param {Map<string, boolean>} allFilesMap where the key is the path to a file
* read by `--all` and the boolean value indicates whether a coverage record
* for this file was found.
* @param {CoverageMap} coverageMap A coverage map produced from v8's output.
* If we encounter an unloaded file, it is merged into this CoverageMap
* @returns {Promise.<undefined>}
* @private
*/
async _createEmptyRecordsForUnloadedFiles (allFilesMap, coverageMap) {
for (const [path, seen] of allFilesMap.entries()) {
// if value is false, that means we didn't receive a coverage
// record. Create and merge an empty record for the file
if (seen === false) {
const emptyCoverageMap = await this._getEmpyCoverageResultForFile(path)
coverageMap.merge(emptyCoverageMap)
}
}
}

/**
* Uses `v8toIstanbul` to create CoverageMapData for a file with all statements,
* functions and branches set to unreached
* @param {string} fullPath
* @returns {Promise.<CoverageMapData>}
* @private
*/
async _getEmpyCoverageResultForFile (fullPath) {
const converter = v8toIstanbul(fullPath, this.wrapperLength)
await converter.load()
const initialCoverage = converter.toIstanbul()
this._setCoverageMapToUncovered(Object.values(initialCoverage)[0])
return initialCoverage
}

/**
* v8ToIstanbul will initialize statements to covered until demonstrated to
* be uncovered. In addition, reporters will interpret empty branch and
* function counters as 100%. Here we reset line coverage to 0% and create
* a fake stub entry for branch/functions that will be interpreted as 0%
* coverage.
* @param {CoverageMapData} coverageMap
* @private
*/
_setCoverageMapToUncovered (coverageMap) {
Object.keys(coverageMap.s).forEach((key) => {
coverageMap.s[key] = 0
})

coverageMap.b = {
0: [
0
]
}

coverageMap.branchMap = {
0: {
locations: []
}
}

coverageMap.fnMap = {
0: {
decl: {
start: {
line: 0,
column: 0
},
end: {
line: 0,
columns: 0
}
},
loc: {
start: {
line: 0,
column: 0
},
end: {
line: 0,
columns: 0
}
}
}
}

coverageMap.f = {
0: false
}
}

/**
* Make sure v8ProcessCov actually contains coverage information.
*
Expand Down Expand Up @@ -196,12 +399,13 @@ class Report {
* @return {v8ProcessCov} Normalized V8 process coverage.
* @private
*/
_normalizeProcessCov (v8ProcessCov) {
_normalizeProcessCov (v8ProcessCov, fileIndex) {
const result = []
for (const v8ScriptCov of v8ProcessCov.result) {
if (/^file:\/\//.test(v8ScriptCov.url)) {
try {
v8ScriptCov.url = furi.toSysPath(v8ScriptCov.url)
fileIndex.set(v8ScriptCov.url, true)
} catch (err) {
console.warn(err)
continue
Expand Down
19 changes: 9 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -41,7 +41,7 @@
"istanbul-reports": "^2.2.6",
"rimraf": "^3.0.0",
"test-exclude": "^5.2.3",
"v8-to-istanbul": "^3.2.6",
"v8-to-istanbul": "git+https://github.com/istanbuljs/v8-to-istanbul.git#empty-report",
"yargs": "^14.0.0",
"yargs-parser": "^15.0.0"
},
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/all/ts-compiled/dir/unloaded.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/all/ts-compiled/dir/unloaded.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions test/fixtures/all/ts-compiled/dir/unloaded.ts
@@ -0,0 +1,5 @@
export default function Unloaded(){
return 'Never loaded :('
}

console.log("This file shouldn't have been evaluated")

0 comments on commit 2062766

Please sign in to comment.