Skip to content

Commit 2062766

Browse files
committed
Implement --all functionality
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
1 parent d0b2eaa commit 2062766

23 files changed

+449
-19
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ node_modules
33
.nyc_output
44
coverage
55
tmp
6+
.idea

lib/commands/report.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ exports.outputReport = async function (argv) {
1919
watermarks: argv.watermarks,
2020
resolve: argv.resolve,
2121
omitRelative: argv.omitRelative,
22-
wrapperLength: argv.wrapperLength
22+
wrapperLength: argv.wrapperLength,
23+
all: argv.all
2324
})
2425
await report.run()
2526
if (argv.checkCoverage) checkCoverages(argv, report)

lib/parse-args.js

+6
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ function buildYargs (withCommands = false) {
8383
type: 'boolean',
8484
describe: 'should temp files be deleted before script execution'
8585
})
86+
.options('all', {
87+
default: false,
88+
type: 'boolean',
89+
describe: 'supplying --all will cause c8 to consider all src files in the current working directory ' +
90+
'when the determining coverage. Respects include/exclude.'
91+
})
8692
.pkgConf('c8')
8793
.config(config)
8894
.demandCommand(1)

lib/report.js

+211-7
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ const furi = require('furi')
33
const libCoverage = require('istanbul-lib-coverage')
44
const libReport = require('istanbul-lib-report')
55
const reports = require('istanbul-reports')
6-
const { readdirSync, readFileSync } = require('fs')
7-
const { isAbsolute, resolve } = require('path')
6+
const { readdirSync, readFileSync, statSync } = require('fs')
7+
const { isAbsolute, resolve, join, relative, extname, dirname } = require('path')
88
// TODO: switch back to @c88/v8-coverage once patch is landed.
99
const { mergeProcessCovs } = require('@bcoe/v8-coverage')
1010
const v8toIstanbul = require('v8-to-istanbul')
@@ -20,7 +20,8 @@ class Report {
2020
watermarks,
2121
omitRelative,
2222
wrapperLength,
23-
resolve: resolvePaths
23+
resolve: resolvePaths,
24+
all
2425
}) {
2526
this.reporter = reporter
2627
this.reportsDirectory = reportsDirectory
@@ -34,6 +35,8 @@ class Report {
3435
this.omitRelative = omitRelative
3536
this.sourceMapCache = {}
3637
this.wrapperLength = wrapperLength
38+
this.all = all
39+
this.src = process.cwd()
3740
}
3841

3942
async run () {
@@ -57,8 +60,8 @@ class Report {
5760
// use-case.
5861
if (this._allCoverageFiles) return this._allCoverageFiles
5962

63+
const map = libCoverage.createCoverageMap()
6064
const v8ProcessCov = this._getMergedProcessCov()
61-
const map = libCoverage.createCoverageMap({})
6265
const resultCountPerPath = new Map()
6366
const possibleCjsEsmBridges = new Map()
6467

@@ -95,11 +98,45 @@ class Report {
9598
map.merge(converter.toIstanbul())
9699
}
97100
}
98-
99101
this._allCoverageFiles = map
100102
return this._allCoverageFiles
101103
}
102104

105+
/**
106+
* v8toIstanbul will return full paths for js files, but in cases where a sourcemap is involved (ts etc)
107+
* it will return a relative path. Normally this is fine, but when using the `--all` option we load files
108+
* in advance and index them by a path. Here we need to decide in advance if we'll handle full or relative
109+
* urls. This function gets Istanbul CoverageMapData and makes sure all paths are relative when --all is
110+
* supplied.
111+
* @param {V8ToIstanbul} converter coverts v8 coverage to Istanbul's format
112+
* @param {Map<string,boolean>} allFilesMap a map of files for the project that allows us to track
113+
* if a file has coverage
114+
* @returns {CoverageMapData}
115+
* @private
116+
*/
117+
_getIstanbulCoverageMap (converter, allFilesMap) {
118+
const istanbulCoverage = converter.toIstanbul()
119+
const mappedPath = Object.keys(istanbulCoverage)[0]
120+
if (this.all && isAbsolute(mappedPath)) {
121+
const coverageData = istanbulCoverage[mappedPath]
122+
const relativeFile = this.relativeToSrc(mappedPath)
123+
const relativePathClone = {
124+
[relativeFile]: coverageData
125+
}
126+
allFilesMap.set(relativeFile, true)
127+
return relativePathClone
128+
} else if (this.all) {
129+
allFilesMap.set(mappedPath, true)
130+
return istanbulCoverage
131+
} else {
132+
return istanbulCoverage
133+
}
134+
}
135+
136+
relativeToSrc (file) {
137+
return join(this.src, relative(this.src, file))
138+
}
139+
103140
/**
104141
* Returns source-map and fake source file, if cached during Node.js'
105142
* execution. This is used to support tools like ts-node, which transpile
@@ -128,6 +165,29 @@ class Report {
128165
return sources
129166
}
130167

168+
/**
169+
* //TODO: use https://www.npmjs.com/package/convert-source-map
170+
* // no need to roll this ourselves this is already in the dep tree
171+
* https://sourcemaps.info/spec.html
172+
* @param {String} compilation target file
173+
* @returns {String} full path to source map file
174+
* @private
175+
*/
176+
_getSourceMapFromFile (file) {
177+
const fileBody = readFileSync(file).toString()
178+
const sourceMapLineRE = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/mg
179+
const results = fileBody.match(sourceMapLineRE)
180+
if (results !== null) {
181+
const sourceMap = results[results.length - 1].split('=')[1]
182+
if (isAbsolute(sourceMap)) {
183+
return sourceMap
184+
} else {
185+
const base = dirname(file)
186+
return join(base, sourceMap)
187+
}
188+
}
189+
}
190+
131191
/**
132192
* Returns the merged V8 process coverage.
133193
*
@@ -139,17 +199,160 @@ class Report {
139199
*/
140200
_getMergedProcessCov () {
141201
const v8ProcessCovs = []
202+
const fileIndex = new Map() // Map<string, bool>
142203
for (const v8ProcessCov of this._loadReports()) {
143204
if (this._isCoverageObject(v8ProcessCov)) {
144205
if (v8ProcessCov['source-map-cache']) {
145206
Object.assign(this.sourceMapCache, v8ProcessCov['source-map-cache'])
146207
}
147-
v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov))
208+
v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex))
148209
}
149210
}
211+
212+
if (this.all) {
213+
const emptyReports = []
214+
v8ProcessCovs.unshift({
215+
result: emptyReports
216+
})
217+
const workingDir = process.cwd()
218+
this.exclude.globSync(workingDir).forEach((f) => {
219+
const fullPath = resolve(workingDir, f)
220+
if (!fileIndex.has(fullPath)) {
221+
const ext = extname(f)
222+
if (ext === '.js' || ext === '.ts' || ext === '.mjs') {
223+
const stat = statSync(f)
224+
const sourceMap = this._getSourceMapFromFile(f)
225+
if (sourceMap !== undefined) {
226+
this.sourceMapCache[`file://${fullPath}`] = { data: JSON.parse(readFileSync(sourceMap).toString()) }
227+
}
228+
emptyReports.push({
229+
scriptId: 0,
230+
url: resolve(f),
231+
functions: [{
232+
functionName: '(empty-report)',
233+
ranges: [{
234+
startOffset: 0,
235+
endOffset: stat.size,
236+
count: 0
237+
}],
238+
isBlockCoverage: true
239+
}]
240+
})
241+
}
242+
}
243+
})
244+
}
245+
150246
return mergeProcessCovs(v8ProcessCovs)
151247
}
152248

249+
/**
250+
* If --all is supplied we need to fetch a list of files that respects
251+
* include/exclude that will be used to see the coverage report with
252+
* empty results for unloaded files
253+
* @returns {Array.<string>}
254+
*/
255+
getFileListForAll () {
256+
return this.exclude.globSync(this.src).reduce((allFileList, file) => {
257+
const srcPath = join(this.src, file)
258+
allFileList.set(srcPath, false)
259+
return allFileList
260+
}, new Map())
261+
}
262+
263+
/**
264+
* Iterates over the entries of `allFilesMap` and where an entries' boolean
265+
* value is false, generate an empty coverage record for the file in question.
266+
* @param {Map<string, boolean>} allFilesMap where the key is the path to a file
267+
* read by `--all` and the boolean value indicates whether a coverage record
268+
* for this file was found.
269+
* @param {CoverageMap} coverageMap A coverage map produced from v8's output.
270+
* If we encounter an unloaded file, it is merged into this CoverageMap
271+
* @returns {Promise.<undefined>}
272+
* @private
273+
*/
274+
async _createEmptyRecordsForUnloadedFiles (allFilesMap, coverageMap) {
275+
for (const [path, seen] of allFilesMap.entries()) {
276+
// if value is false, that means we didn't receive a coverage
277+
// record. Create and merge an empty record for the file
278+
if (seen === false) {
279+
const emptyCoverageMap = await this._getEmpyCoverageResultForFile(path)
280+
coverageMap.merge(emptyCoverageMap)
281+
}
282+
}
283+
}
284+
285+
/**
286+
* Uses `v8toIstanbul` to create CoverageMapData for a file with all statements,
287+
* functions and branches set to unreached
288+
* @param {string} fullPath
289+
* @returns {Promise.<CoverageMapData>}
290+
* @private
291+
*/
292+
async _getEmpyCoverageResultForFile (fullPath) {
293+
const converter = v8toIstanbul(fullPath, this.wrapperLength)
294+
await converter.load()
295+
const initialCoverage = converter.toIstanbul()
296+
this._setCoverageMapToUncovered(Object.values(initialCoverage)[0])
297+
return initialCoverage
298+
}
299+
300+
/**
301+
* v8ToIstanbul will initialize statements to covered until demonstrated to
302+
* be uncovered. In addition, reporters will interpret empty branch and
303+
* function counters as 100%. Here we reset line coverage to 0% and create
304+
* a fake stub entry for branch/functions that will be interpreted as 0%
305+
* coverage.
306+
* @param {CoverageMapData} coverageMap
307+
* @private
308+
*/
309+
_setCoverageMapToUncovered (coverageMap) {
310+
Object.keys(coverageMap.s).forEach((key) => {
311+
coverageMap.s[key] = 0
312+
})
313+
314+
coverageMap.b = {
315+
0: [
316+
0
317+
]
318+
}
319+
320+
coverageMap.branchMap = {
321+
0: {
322+
locations: []
323+
}
324+
}
325+
326+
coverageMap.fnMap = {
327+
0: {
328+
decl: {
329+
start: {
330+
line: 0,
331+
column: 0
332+
},
333+
end: {
334+
line: 0,
335+
columns: 0
336+
}
337+
},
338+
loc: {
339+
start: {
340+
line: 0,
341+
column: 0
342+
},
343+
end: {
344+
line: 0,
345+
columns: 0
346+
}
347+
}
348+
}
349+
}
350+
351+
coverageMap.f = {
352+
0: false
353+
}
354+
}
355+
153356
/**
154357
* Make sure v8ProcessCov actually contains coverage information.
155358
*
@@ -196,12 +399,13 @@ class Report {
196399
* @return {v8ProcessCov} Normalized V8 process coverage.
197400
* @private
198401
*/
199-
_normalizeProcessCov (v8ProcessCov) {
402+
_normalizeProcessCov (v8ProcessCov, fileIndex) {
200403
const result = []
201404
for (const v8ScriptCov of v8ProcessCov.result) {
202405
if (/^file:\/\//.test(v8ScriptCov.url)) {
203406
try {
204407
v8ScriptCov.url = furi.toSysPath(v8ScriptCov.url)
408+
fileIndex.set(v8ScriptCov.url, true)
205409
} catch (err) {
206410
console.warn(err)
207411
continue

package-lock.json

+9-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"istanbul-reports": "^2.2.6",
4242
"rimraf": "^3.0.0",
4343
"test-exclude": "^5.2.3",
44-
"v8-to-istanbul": "^3.2.6",
44+
"v8-to-istanbul": "git+https://github.com/istanbuljs/v8-to-istanbul.git#empty-report",
4545
"yargs": "^14.0.0",
4646
"yargs-parser": "^15.0.0"
4747
},

test/fixtures/all/ts-compiled/dir/unloaded.js

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/all/ts-compiled/dir/unloaded.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default function Unloaded(){
2+
return 'Never loaded :('
3+
}
4+
5+
console.log("This file shouldn't have been evaluated")

0 commit comments

Comments
 (0)