@@ -3,8 +3,8 @@ const furi = require('furi')
33const libCoverage = require ( 'istanbul-lib-coverage' )
44const libReport = require ( 'istanbul-lib-report' )
55const 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 @c 88/v8-coverage once patch is landed.
99const { mergeProcessCovs } = require ( '@bcoe/v8-coverage' )
1010const 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 = / \/ \/ [ # @ ] ? s o u r c e M a p p i n g U R L = ( [ ^ \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 ( / ^ f i l e : \/ \/ / . 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
0 commit comments