c8 7.13.0 → 7.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,7 +34,8 @@ exports.outputReport = async function (argv) {
34
34
  allowExternal: argv.allowExternal,
35
35
  src: argv.src,
36
36
  skipFull: argv.skipFull,
37
- excludeNodeModules: argv.excludeNodeModules
37
+ excludeNodeModules: argv.excludeNodeModules,
38
+ mergeAsync: argv.mergeAsync
38
39
  })
39
40
  await report.run()
40
41
  if (argv.checkCoverage) await checkCoverages(argv, report)
package/lib/parse-args.js CHANGED
@@ -152,6 +152,12 @@ function buildYargs (withCommands = false) {
152
152
  describe: 'supplying --allowExternal will cause c8 to allow files from outside of your cwd. This applies both to ' +
153
153
  'files discovered in coverage temp files and also src files discovered if using the --all flag.'
154
154
  })
155
+ .options('merge-async', {
156
+ default: false,
157
+ type: 'boolean',
158
+ describe: 'supplying --merge-async will merge all v8 coverage reports asynchronously and incrementally. ' +
159
+ 'This is to avoid OOM issues with Node.js runtime.'
160
+ })
155
161
  .pkgConf('c8')
156
162
  .demandCommand(1)
157
163
  .check((argv) => {
package/lib/report.js CHANGED
@@ -2,6 +2,12 @@ const Exclude = require('test-exclude')
2
2
  const libCoverage = require('istanbul-lib-coverage')
3
3
  const libReport = require('istanbul-lib-report')
4
4
  const reports = require('istanbul-reports')
5
+ let readFile
6
+ try {
7
+ ;({ readFile } = require('fs/promises'))
8
+ } catch (err) {
9
+ ;({ readFile } = require('fs').promises)
10
+ }
5
11
  const { readdirSync, readFileSync, statSync } = require('fs')
6
12
  const { isAbsolute, resolve, extname } = require('path')
7
13
  const { pathToFileURL, fileURLToPath } = require('url')
@@ -30,7 +36,8 @@ class Report {
30
36
  src,
31
37
  allowExternal = false,
32
38
  skipFull,
33
- excludeNodeModules
39
+ excludeNodeModules,
40
+ mergeAsync
34
41
  }) {
35
42
  this.reporter = reporter
36
43
  this.reporterOptions = reporterOptions || {}
@@ -53,6 +60,7 @@ class Report {
53
60
  this.all = all
54
61
  this.src = this._getSrc(src)
55
62
  this.skipFull = skipFull
63
+ this.mergeAsync = mergeAsync
56
64
  }
57
65
 
58
66
  _getSrc (src) {
@@ -90,7 +98,13 @@ class Report {
90
98
  if (this._allCoverageFiles) return this._allCoverageFiles
91
99
 
92
100
  const map = libCoverage.createCoverageMap()
93
- const v8ProcessCov = this._getMergedProcessCov()
101
+ let v8ProcessCov
102
+
103
+ if (this.mergeAsync) {
104
+ v8ProcessCov = await this._getMergedProcessCovAsync()
105
+ } else {
106
+ v8ProcessCov = this._getMergedProcessCov()
107
+ }
94
108
  const resultCountPerPath = new Map()
95
109
  const possibleCjsEsmBridges = new Map()
96
110
 
@@ -188,43 +202,106 @@ class Report {
188
202
  }
189
203
 
190
204
  if (this.all) {
191
- const emptyReports = []
205
+ const emptyReports = this._includeUncoveredFiles(fileIndex)
192
206
  v8ProcessCovs.unshift({
193
207
  result: emptyReports
194
208
  })
195
- const workingDirs = this.src
196
- const { extension } = this.exclude
197
- for (const workingDir of workingDirs) {
198
- this.exclude.globSync(workingDir).forEach((f) => {
199
- const fullPath = resolve(workingDir, f)
200
- if (!fileIndex.has(fullPath)) {
201
- const ext = extname(fullPath)
202
- if (extension.includes(ext)) {
203
- const stat = statSync(fullPath)
204
- const sourceMap = getSourceMapFromFile(fullPath)
205
- if (sourceMap) {
206
- this.sourceMapCache[pathToFileURL(fullPath)] = { data: sourceMap }
207
- }
208
- emptyReports.push({
209
- scriptId: 0,
210
- url: resolve(fullPath),
211
- functions: [{
212
- functionName: '(empty-report)',
213
- ranges: [{
214
- startOffset: 0,
215
- endOffset: stat.size,
216
- count: 0
217
- }],
218
- isBlockCoverage: true
219
- }]
220
- })
221
- }
209
+ }
210
+
211
+ return mergeProcessCovs(v8ProcessCovs)
212
+ }
213
+
214
+ /**
215
+ * Returns the merged V8 process coverage.
216
+ *
217
+ * It asynchronously and incrementally reads and merges individual process coverages
218
+ * generated by Node. This can be used via the `--merge-async` CLI arg. It's intended
219
+ * to be used across a large multi-process test run.
220
+ *
221
+ * @return {ProcessCov} Merged V8 process coverage.
222
+ * @private
223
+ */
224
+ async _getMergedProcessCovAsync () {
225
+ const { mergeProcessCovs } = require('@bcoe/v8-coverage')
226
+ const fileIndex = new Set() // Set<string>
227
+ let mergedCov = null
228
+ for (const file of readdirSync(this.tempDirectory)) {
229
+ try {
230
+ const rawFile = await readFile(
231
+ resolve(this.tempDirectory, file),
232
+ 'utf8'
233
+ )
234
+ let report = JSON.parse(rawFile)
235
+
236
+ if (this._isCoverageObject(report)) {
237
+ if (report['source-map-cache']) {
238
+ Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(report['source-map-cache']))
222
239
  }
223
- })
240
+ report = this._normalizeProcessCov(report, fileIndex)
241
+ if (mergedCov) {
242
+ mergedCov = mergeProcessCovs([mergedCov, report])
243
+ } else {
244
+ mergedCov = mergeProcessCovs([report])
245
+ }
246
+ }
247
+ } catch (err) {
248
+ debuglog(`${err.stack}`)
224
249
  }
225
250
  }
226
251
 
227
- return mergeProcessCovs(v8ProcessCovs)
252
+ if (this.all) {
253
+ const emptyReports = this._includeUncoveredFiles(fileIndex)
254
+ const emptyReport = {
255
+ result: emptyReports
256
+ }
257
+
258
+ mergedCov = mergeProcessCovs([emptyReport, mergedCov])
259
+ }
260
+
261
+ return mergedCov
262
+ }
263
+
264
+ /**
265
+ * Adds empty coverage reports to account for uncovered/untested code.
266
+ * This is only done when the `--all` flag is present.
267
+ *
268
+ * @param {Set} fileIndex list of files that have coverage
269
+ * @returns {Array} list of empty coverage reports
270
+ */
271
+ _includeUncoveredFiles (fileIndex) {
272
+ const emptyReports = []
273
+ const workingDirs = this.src
274
+ const { extension } = this.exclude
275
+ for (const workingDir of workingDirs) {
276
+ this.exclude.globSync(workingDir).forEach((f) => {
277
+ const fullPath = resolve(workingDir, f)
278
+ if (!fileIndex.has(fullPath)) {
279
+ const ext = extname(fullPath)
280
+ if (extension.includes(ext)) {
281
+ const stat = statSync(fullPath)
282
+ const sourceMap = getSourceMapFromFile(fullPath)
283
+ if (sourceMap) {
284
+ this.sourceMapCache[pathToFileURL(fullPath)] = { data: sourceMap }
285
+ }
286
+ emptyReports.push({
287
+ scriptId: 0,
288
+ url: resolve(fullPath),
289
+ functions: [{
290
+ functionName: '(empty-report)',
291
+ ranges: [{
292
+ startOffset: 0,
293
+ endOffset: stat.size,
294
+ count: 0
295
+ }],
296
+ isBlockCoverage: true
297
+ }]
298
+ })
299
+ }
300
+ }
301
+ })
302
+ }
303
+
304
+ return emptyReports
228
305
  }
229
306
 
230
307
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8",
3
- "version": "7.13.0",
3
+ "version": "7.14.0",
4
4
  "description": "output coverage reports using Node.js' built in coverage",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",