UNPKG

9.68 kBJavaScriptView Raw
1const Exclude = require('test-exclude')
2const libCoverage = require('istanbul-lib-coverage')
3const libReport = require('istanbul-lib-report')
4const reports = require('istanbul-reports')
5const { readdirSync, readFileSync, statSync } = require('fs')
6const { isAbsolute, resolve, extname } = require('path')
7const { pathToFileURL, fileURLToPath } = require('url')
8const getSourceMapFromFile = require('./source-map-from-file')
9// TODO: switch back to @c88/v8-coverage once patch is landed.
10const v8toIstanbul = require('v8-to-istanbul')
11const isCjsEsmBridgeCov = require('./is-cjs-esm-bridge')
12const util = require('util')
13const debuglog = util.debuglog('c8')
14
15class Report {
16 constructor ({
17 exclude,
18 excludeAfterRemap,
19 include,
20 reporter,
21 reportsDirectory,
22 tempDirectory,
23 watermarks,
24 omitRelative,
25 wrapperLength,
26 resolve: resolvePaths,
27 all,
28 src,
29 allowExternal = false,
30 skipFull
31 }) {
32 this.reporter = reporter
33 this.reportsDirectory = reportsDirectory
34 this.tempDirectory = tempDirectory
35 this.watermarks = watermarks
36 this.resolve = resolvePaths
37 this.exclude = new Exclude({
38 exclude: exclude,
39 include: include,
40 relativePath: !allowExternal
41 })
42 this.excludeAfterRemap = excludeAfterRemap
43 this.omitRelative = omitRelative
44 this.sourceMapCache = {}
45 this.wrapperLength = wrapperLength
46 this.all = all
47 this.src = this._getSrc(src)
48 this.skipFull = skipFull
49 }
50
51 _getSrc (src) {
52 if (typeof src === 'string') {
53 return [src]
54 } else if (Array.isArray(src)) {
55 return src
56 } else {
57 return [process.cwd()]
58 }
59 }
60
61 async run () {
62 const context = libReport.createContext({
63 dir: this.reportsDirectory,
64 watermarks: this.watermarks,
65 coverageMap: await this.getCoverageMapFromAllCoverageFiles()
66 })
67
68 for (const _reporter of this.reporter) {
69 reports.create(_reporter, {
70 skipEmpty: false,
71 skipFull: this.skipFull,
72 maxCols: 100
73 }).execute(context)
74 }
75 }
76
77 async getCoverageMapFromAllCoverageFiles () {
78 // the merge process can be very expensive, and it's often the case that
79 // check-coverage is called immediately after a report. We memoize the
80 // result from getCoverageMapFromAllCoverageFiles() to address this
81 // use-case.
82 if (this._allCoverageFiles) return this._allCoverageFiles
83
84 const map = libCoverage.createCoverageMap()
85 const v8ProcessCov = this._getMergedProcessCov()
86 const resultCountPerPath = new Map()
87 const possibleCjsEsmBridges = new Map()
88
89 for (const v8ScriptCov of v8ProcessCov.result) {
90 try {
91 const sources = this._getSourceMap(v8ScriptCov)
92 const path = resolve(this.resolve, v8ScriptCov.url)
93 const converter = v8toIstanbul(path, this.wrapperLength, sources, (path) => {
94 if (this.excludeAfterRemap) {
95 return !this.exclude.shouldInstrument(path)
96 }
97 })
98 await converter.load()
99
100 if (resultCountPerPath.has(path)) {
101 resultCountPerPath.set(path, resultCountPerPath.get(path) + 1)
102 } else {
103 resultCountPerPath.set(path, 0)
104 }
105
106 if (isCjsEsmBridgeCov(v8ScriptCov)) {
107 possibleCjsEsmBridges.set(converter, {
108 path,
109 functions: v8ScriptCov.functions
110 })
111 } else {
112 converter.applyCoverage(v8ScriptCov.functions)
113 map.merge(converter.toIstanbul())
114 }
115 } catch (err) {
116 debuglog(`file: ${v8ScriptCov.url} error: ${err.stack}`)
117 }
118 }
119
120 for (const [converter, { path, functions }] of possibleCjsEsmBridges) {
121 if (resultCountPerPath.get(path) <= 1) {
122 converter.applyCoverage(functions)
123 map.merge(converter.toIstanbul())
124 }
125 }
126 this._allCoverageFiles = map
127 return this._allCoverageFiles
128 }
129
130 /**
131 * Returns source-map and fake source file, if cached during Node.js'
132 * execution. This is used to support tools like ts-node, which transpile
133 * using runtime hooks.
134 *
135 * Note: requires Node.js 13+
136 *
137 * @return {Object} sourceMap and fake source file (created from line #s).
138 * @private
139 */
140 _getSourceMap (v8ScriptCov) {
141 const sources = {}
142 const sourceMapAndLineLengths = this.sourceMapCache[pathToFileURL(v8ScriptCov.url).href]
143 if (sourceMapAndLineLengths) {
144 // See: https://github.com/nodejs/node/pull/34305
145 if (!sourceMapAndLineLengths.data) return
146 sources.sourceMap = {
147 sourcemap: sourceMapAndLineLengths.data
148 }
149 if (sourceMapAndLineLengths.lineLengths) {
150 let source = ''
151 sourceMapAndLineLengths.lineLengths.forEach(length => {
152 source += `${''.padEnd(length, '.')}\n`
153 })
154 sources.source = source
155 }
156 }
157 return sources
158 }
159
160 /**
161 * Returns the merged V8 process coverage.
162 *
163 * The result is computed from the individual process coverages generated
164 * by Node. It represents the sum of their counts.
165 *
166 * @return {ProcessCov} Merged V8 process coverage.
167 * @private
168 */
169 _getMergedProcessCov () {
170 const { mergeProcessCovs } = require('@bcoe/v8-coverage')
171 const v8ProcessCovs = []
172 const fileIndex = new Set() // Set<string>
173 for (const v8ProcessCov of this._loadReports()) {
174 if (this._isCoverageObject(v8ProcessCov)) {
175 if (v8ProcessCov['source-map-cache']) {
176 Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(v8ProcessCov['source-map-cache']))
177 }
178 v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex))
179 }
180 }
181
182 if (this.all) {
183 const emptyReports = []
184 v8ProcessCovs.unshift({
185 result: emptyReports
186 })
187 const workingDirs = this.src
188 for (const workingDir of workingDirs) {
189 this.exclude.globSync(workingDir).forEach((f) => {
190 const fullPath = resolve(workingDir, f)
191 if (!fileIndex.has(fullPath)) {
192 const ext = extname(fullPath)
193 if (ext === '.js' || ext === '.ts' || ext === '.mjs') {
194 const stat = statSync(fullPath)
195 const sourceMap = getSourceMapFromFile(fullPath)
196 if (sourceMap) {
197 this.sourceMapCache[pathToFileURL(fullPath)] = { data: sourceMap }
198 }
199 emptyReports.push({
200 scriptId: 0,
201 url: resolve(fullPath),
202 functions: [{
203 functionName: '(empty-report)',
204 ranges: [{
205 startOffset: 0,
206 endOffset: stat.size,
207 count: 0
208 }],
209 isBlockCoverage: true
210 }]
211 })
212 }
213 }
214 })
215 }
216 }
217
218 return mergeProcessCovs(v8ProcessCovs)
219 }
220
221 /**
222 * Make sure v8ProcessCov actually contains coverage information.
223 *
224 * @return {boolean} does it look like v8ProcessCov?
225 * @private
226 */
227 _isCoverageObject (maybeV8ProcessCov) {
228 return maybeV8ProcessCov && Array.isArray(maybeV8ProcessCov.result)
229 }
230
231 /**
232 * Returns the list of V8 process coverages generated by Node.
233 *
234 * @return {ProcessCov[]} Process coverages generated by Node.
235 * @private
236 */
237 _loadReports () {
238 const reports = []
239 for (const file of readdirSync(this.tempDirectory)) {
240 try {
241 reports.push(JSON.parse(readFileSync(
242 resolve(this.tempDirectory, file),
243 'utf8'
244 )))
245 } catch (err) {
246 debuglog(`${err.stack}`)
247 }
248 }
249 return reports
250 }
251
252 /**
253 * Normalizes a process coverage.
254 *
255 * This function replaces file URLs (`url` property) by their corresponding
256 * system-dependent path and applies the current inclusion rules to filter out
257 * the excluded script coverages.
258 *
259 * The result is a copy of the input, with script coverages filtered based
260 * on their `url` and the current inclusion rules.
261 * There is no deep cloning.
262 *
263 * @param v8ProcessCov V8 process coverage to normalize.
264 * @param fileIndex a Set<string> of paths discovered in coverage
265 * @return {v8ProcessCov} Normalized V8 process coverage.
266 * @private
267 */
268 _normalizeProcessCov (v8ProcessCov, fileIndex) {
269 const result = []
270 for (const v8ScriptCov of v8ProcessCov.result) {
271 // https://github.com/nodejs/node/pull/35498 updates Node.js'
272 // builtin module filenames:
273 if (/^node:/.test(v8ScriptCov.url)) {
274 v8ScriptCov.url = `${v8ScriptCov.url.replace(/^node:/, '')}.js`
275 }
276 if (/^file:\/\//.test(v8ScriptCov.url)) {
277 try {
278 v8ScriptCov.url = fileURLToPath(v8ScriptCov.url)
279 fileIndex.add(v8ScriptCov.url)
280 } catch (err) {
281 debuglog(`${err.stack}`)
282 continue
283 }
284 }
285 if ((!this.omitRelative || isAbsolute(v8ScriptCov.url))) {
286 if (this.excludeAfterRemap || this.exclude.shouldInstrument(v8ScriptCov.url)) {
287 result.push(v8ScriptCov)
288 }
289 }
290 }
291 return { result }
292 }
293
294 /**
295 * Normalizes a V8 source map cache.
296 *
297 * This function normalizes file URLs to a system-independent format.
298 *
299 * @param v8SourceMapCache V8 source map cache to normalize.
300 * @return {v8SourceMapCache} Normalized V8 source map cache.
301 * @private
302 */
303 _normalizeSourceMapCache (v8SourceMapCache) {
304 const cache = {}
305 for (const fileURL of Object.keys(v8SourceMapCache)) {
306 cache[pathToFileURL(fileURLToPath(fileURL)).href] = v8SourceMapCache[fileURL]
307 }
308 return cache
309 }
310}
311
312module.exports = function (opts) {
313 return new Report(opts)
314}