1 | 'use strict'
|
2 |
|
3 |
|
4 |
|
5 | const cachingTransform = require('caching-transform')
|
6 | const cpFile = require('cp-file')
|
7 | const findCacheDir = require('find-cache-dir')
|
8 | const fs = require('fs')
|
9 | const glob = require('glob')
|
10 | const Hash = require('./lib/hash')
|
11 | const libCoverage = require('istanbul-lib-coverage')
|
12 | const libHook = require('istanbul-lib-hook')
|
13 | const libReport = require('istanbul-lib-report')
|
14 | const mkdirp = require('make-dir')
|
15 | const Module = require('module')
|
16 | const onExit = require('signal-exit')
|
17 | const path = require('path')
|
18 | const reports = require('istanbul-reports')
|
19 | const resolveFrom = require('resolve-from')
|
20 | const rimraf = require('rimraf')
|
21 | const SourceMaps = require('./lib/source-maps')
|
22 | const testExclude = require('test-exclude')
|
23 | const util = require('util')
|
24 | const uuid = require('uuid/v4')
|
25 |
|
26 | const debugLog = util.debuglog('nyc')
|
27 |
|
28 | const ProcessInfo = require('./lib/process.js')
|
29 |
|
30 |
|
31 | if (/self-coverage/.test(__dirname)) {
|
32 | require('../self-coverage-helper')
|
33 | }
|
34 |
|
35 | function coverageFinder () {
|
36 | var coverage = global.__coverage__
|
37 | if (typeof __coverage__ === 'object') coverage = __coverage__
|
38 | if (!coverage) coverage = global['__coverage__'] = {}
|
39 | return coverage
|
40 | }
|
41 |
|
42 | class NYC {
|
43 | constructor (config) {
|
44 | config = config || {}
|
45 | this.config = config
|
46 |
|
47 | this.subprocessBin = config.subprocessBin || path.resolve(__dirname, './bin/nyc.js')
|
48 | this._tempDirectory = config.tempDirectory || config.tempDir || './.nyc_output'
|
49 | this._instrumenterLib = require(config.instrumenter || './lib/instrumenters/istanbul')
|
50 | this._reportDir = config.reportDir || 'coverage'
|
51 | this._sourceMap = typeof config.sourceMap === 'boolean' ? config.sourceMap : true
|
52 | this._showProcessTree = config.showProcessTree || false
|
53 | this._eagerInstantiation = config.eager || false
|
54 | this.cwd = config.cwd || process.cwd()
|
55 | this.reporter = [].concat(config.reporter || 'text')
|
56 |
|
57 | this.cacheDirectory = (config.cacheDir && path.resolve(config.cacheDir)) || findCacheDir({ name: 'nyc', cwd: this.cwd })
|
58 | this.cache = Boolean(this.cacheDirectory && config.cache)
|
59 |
|
60 | this.extensions = [].concat(config.extension || [])
|
61 | .concat('.js')
|
62 | .map(ext => ext.toLowerCase())
|
63 | .filter((item, pos, arr) => arr.indexOf(item) === pos)
|
64 |
|
65 | this.exclude = testExclude({
|
66 | cwd: this.cwd,
|
67 | include: config.include,
|
68 | exclude: config.exclude,
|
69 | excludeNodeModules: config.excludeNodeModules !== false,
|
70 | extension: this.extensions
|
71 | })
|
72 |
|
73 | this.sourceMaps = new SourceMaps({
|
74 | cache: this.cache,
|
75 | cacheDirectory: this.cacheDirectory
|
76 | })
|
77 |
|
78 |
|
79 | this.require = [].concat(config.require || [])
|
80 |
|
81 | this.transforms = this.extensions.reduce((transforms, ext) => {
|
82 | transforms[ext] = this._createTransform(ext)
|
83 | return transforms
|
84 | }, {})
|
85 |
|
86 | this.hookRequire = config.hookRequire
|
87 | this.hookRunInContext = config.hookRunInContext
|
88 | this.hookRunInThisContext = config.hookRunInThisContext
|
89 | this.fakeRequire = null
|
90 |
|
91 | this.processInfo = new ProcessInfo(config && config._processInfo)
|
92 | this.rootId = this.processInfo.root || uuid()
|
93 |
|
94 | this.hashCache = {}
|
95 | }
|
96 |
|
97 | _createTransform (ext) {
|
98 | var opts = {
|
99 | salt: Hash.salt(this.config),
|
100 | hashData: (input, metadata) => [metadata.filename],
|
101 | onHash: (input, metadata, hash) => {
|
102 | this.hashCache[metadata.filename] = hash
|
103 | },
|
104 | cacheDir: this.cacheDirectory,
|
105 |
|
106 |
|
107 | disableCache: this._disableCachingTransform(),
|
108 | ext: ext
|
109 | }
|
110 | if (this._eagerInstantiation) {
|
111 | opts.transform = this._transformFactory(this.cacheDirectory)
|
112 | } else {
|
113 | opts.factory = this._transformFactory.bind(this)
|
114 | }
|
115 | return cachingTransform(opts)
|
116 | }
|
117 |
|
118 | _disableCachingTransform () {
|
119 | return !(this.cache && this.config.isChildProcess)
|
120 | }
|
121 |
|
122 | _loadAdditionalModules () {
|
123 | this.require.forEach(requireModule => {
|
124 |
|
125 |
|
126 | require(resolveFrom.silent(this.cwd, requireModule) || requireModule)
|
127 | })
|
128 | }
|
129 |
|
130 | instrumenter () {
|
131 | return this._instrumenter || (this._instrumenter = this._createInstrumenter())
|
132 | }
|
133 |
|
134 | _createInstrumenter () {
|
135 | return this._instrumenterLib({
|
136 | ignoreClassMethods: [].concat(this.config.ignoreClassMethod).filter(a => a),
|
137 | produceSourceMap: this.config.produceSourceMap,
|
138 | compact: this.config.compact,
|
139 | preserveComments: this.config.preserveComments,
|
140 | esModules: this.config.esModules,
|
141 | plugins: this.config.parserPlugins
|
142 | })
|
143 | }
|
144 |
|
145 | addFile (filename) {
|
146 | const source = this._readTranspiledSource(filename)
|
147 | this._maybeInstrumentSource(source, filename)
|
148 | }
|
149 |
|
150 | _readTranspiledSource (filePath) {
|
151 | var source = null
|
152 | var ext = path.extname(filePath)
|
153 | if (typeof Module._extensions[ext] === 'undefined') {
|
154 | ext = '.js'
|
155 | }
|
156 | Module._extensions[ext]({
|
157 | _compile: function (content, filename) {
|
158 | source = content
|
159 | }
|
160 | }, filePath)
|
161 | return source
|
162 | }
|
163 |
|
164 | addAllFiles () {
|
165 | this._loadAdditionalModules()
|
166 |
|
167 | this.fakeRequire = true
|
168 | this.exclude.globSync(this.cwd).forEach(relFile => {
|
169 | const filename = path.resolve(this.cwd, relFile)
|
170 | this.addFile(filename)
|
171 | const coverage = coverageFinder()
|
172 | const lastCoverage = this.instrumenter().lastFileCoverage()
|
173 | if (lastCoverage) {
|
174 | coverage[lastCoverage.path] = lastCoverage
|
175 | }
|
176 | })
|
177 | this.fakeRequire = false
|
178 |
|
179 | this.writeCoverageFile()
|
180 | }
|
181 |
|
182 | instrumentAllFiles (input, output, cb) {
|
183 | let inputDir = '.' + path.sep
|
184 | const visitor = relFile => {
|
185 | const inFile = path.resolve(inputDir, relFile)
|
186 | const inCode = fs.readFileSync(inFile, 'utf-8')
|
187 | const outCode = this._transform(inCode, inFile) || inCode
|
188 |
|
189 | if (output) {
|
190 | const mode = fs.statSync(inFile).mode
|
191 | const outFile = path.resolve(output, relFile)
|
192 | mkdirp.sync(path.dirname(outFile))
|
193 | fs.writeFileSync(outFile, outCode)
|
194 | fs.chmodSync(outFile, mode)
|
195 | } else {
|
196 | console.log(outCode)
|
197 | }
|
198 | }
|
199 |
|
200 | this._loadAdditionalModules()
|
201 |
|
202 | try {
|
203 | const stats = fs.lstatSync(input)
|
204 | if (stats.isDirectory()) {
|
205 | inputDir = input
|
206 |
|
207 | const filesToInstrument = this.exclude.globSync(input)
|
208 |
|
209 | if (this.config.completeCopy && output) {
|
210 | const globOptions = { dot: true, nodir: true, ignore: ['**/.git', '**/.git/**', path.join(output, '**')] }
|
211 | glob.sync(path.resolve(input, '**'), globOptions)
|
212 | .forEach(src => cpFile.sync(src, path.join(output, path.relative(input, src))))
|
213 | }
|
214 | filesToInstrument.forEach(visitor)
|
215 | } else {
|
216 | visitor(input)
|
217 | }
|
218 | } catch (err) {
|
219 | return cb(err)
|
220 | }
|
221 | cb()
|
222 | }
|
223 |
|
224 | _transform (code, filename) {
|
225 | const extname = path.extname(filename).toLowerCase()
|
226 | const transform = this.transforms[extname] || (() => null)
|
227 |
|
228 | return transform(code, { filename })
|
229 | }
|
230 |
|
231 | _maybeInstrumentSource (code, filename) {
|
232 | if (!this.exclude.shouldInstrument(filename)) {
|
233 | return null
|
234 | }
|
235 |
|
236 | return this._transform(code, filename)
|
237 | }
|
238 |
|
239 | maybePurgeSourceMapCache () {
|
240 | if (!this.cache) {
|
241 | this.sourceMaps.purgeCache()
|
242 | }
|
243 | }
|
244 |
|
245 | _transformFactory (cacheDir) {
|
246 | const instrumenter = this.instrumenter()
|
247 | let instrumented
|
248 |
|
249 | return (code, metadata, hash) => {
|
250 | const filename = metadata.filename
|
251 | let sourceMap = null
|
252 |
|
253 | if (this._sourceMap) sourceMap = this.sourceMaps.extractAndRegister(code, filename, hash)
|
254 |
|
255 | try {
|
256 | instrumented = instrumenter.instrumentSync(code, filename, sourceMap)
|
257 | } catch (e) {
|
258 | debugLog('failed to instrument ' + filename + ' with error: ' + e.stack)
|
259 | if (this.config.exitOnError) {
|
260 | console.error('Failed to instrument ' + filename)
|
261 | process.exit(1)
|
262 | } else {
|
263 | instrumented = code
|
264 | }
|
265 | }
|
266 |
|
267 | if (this.fakeRequire) {
|
268 | return 'function x () {}'
|
269 | } else {
|
270 | return instrumented
|
271 | }
|
272 | }
|
273 | }
|
274 |
|
275 | _handleJs (code, options) {
|
276 |
|
277 | const filename = path.resolve(this.cwd, options.filename)
|
278 | return this._maybeInstrumentSource(code, filename) || code
|
279 | }
|
280 |
|
281 | _addHook (type) {
|
282 | const handleJs = this._handleJs.bind(this)
|
283 | const dummyMatcher = () => true
|
284 | libHook['hook' + type](dummyMatcher, handleJs, { extensions: this.extensions })
|
285 | }
|
286 |
|
287 | _addRequireHooks () {
|
288 | if (this.hookRequire) {
|
289 | this._addHook('Require')
|
290 | }
|
291 | if (this.hookRunInContext) {
|
292 | this._addHook('RunInContext')
|
293 | }
|
294 | if (this.hookRunInThisContext) {
|
295 | this._addHook('RunInThisContext')
|
296 | }
|
297 | }
|
298 |
|
299 | cleanup () {
|
300 | if (!process.env.NYC_CWD) rimraf.sync(this.tempDirectory())
|
301 | }
|
302 |
|
303 | clearCache () {
|
304 | if (this.cache) {
|
305 | rimraf.sync(this.cacheDirectory)
|
306 | }
|
307 | }
|
308 |
|
309 | createTempDirectory () {
|
310 | mkdirp.sync(this.tempDirectory())
|
311 | if (this.cache) mkdirp.sync(this.cacheDirectory)
|
312 |
|
313 | mkdirp.sync(this.processInfoDirectory())
|
314 | }
|
315 |
|
316 | reset () {
|
317 | this.cleanup()
|
318 | this.createTempDirectory()
|
319 | }
|
320 |
|
321 | _wrapExit () {
|
322 |
|
323 |
|
324 | onExit(() => {
|
325 | this.writeCoverageFile()
|
326 | }, { alwaysLast: true })
|
327 | }
|
328 |
|
329 | wrap (bin) {
|
330 | process.env.NYC_PROCESS_ID = this.processInfo.uuid
|
331 | this._addRequireHooks()
|
332 | this._wrapExit()
|
333 | this._loadAdditionalModules()
|
334 | return this
|
335 | }
|
336 |
|
337 | writeCoverageFile () {
|
338 | var coverage = coverageFinder()
|
339 | if (!coverage) return
|
340 |
|
341 |
|
342 | Object.keys(coverage).forEach(function (absFile) {
|
343 | if (!this.exclude.shouldInstrument(absFile)) {
|
344 | delete coverage[absFile]
|
345 | }
|
346 | }, this)
|
347 |
|
348 | if (this.cache) {
|
349 | Object.keys(coverage).forEach(function (absFile) {
|
350 | if (this.hashCache[absFile] && coverage[absFile]) {
|
351 | coverage[absFile].contentHash = this.hashCache[absFile]
|
352 | }
|
353 | }, this)
|
354 | } else {
|
355 | coverage = this.sourceMaps.remapCoverage(coverage)
|
356 | }
|
357 |
|
358 | var id = this.processInfo.uuid
|
359 | var coverageFilename = path.resolve(this.tempDirectory(), id + '.json')
|
360 |
|
361 | fs.writeFileSync(
|
362 | coverageFilename,
|
363 | JSON.stringify(coverage),
|
364 | 'utf-8'
|
365 | )
|
366 |
|
367 | this.processInfo.coverageFilename = coverageFilename
|
368 | this.processInfo.files = Object.keys(coverage)
|
369 |
|
370 | fs.writeFileSync(
|
371 | path.resolve(this.processInfoDirectory(), id + '.json'),
|
372 | JSON.stringify(this.processInfo),
|
373 | 'utf-8'
|
374 | )
|
375 | }
|
376 |
|
377 | getCoverageMapFromAllCoverageFiles (baseDirectory) {
|
378 | const map = libCoverage.createCoverageMap({})
|
379 |
|
380 | this.eachReport(undefined, (report) => {
|
381 | map.merge(report)
|
382 | }, baseDirectory)
|
383 |
|
384 | map.data = this.sourceMaps.remapCoverage(map.data)
|
385 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 | if (this.config.excludeAfterRemap) {
|
391 | map.filter(filename => this.exclude.shouldInstrument(filename))
|
392 | }
|
393 |
|
394 | return map
|
395 | }
|
396 |
|
397 | report () {
|
398 | var tree
|
399 | var map = this.getCoverageMapFromAllCoverageFiles()
|
400 | var context = libReport.createContext({
|
401 | dir: this.reportDirectory(),
|
402 | watermarks: this.config.watermarks
|
403 | })
|
404 |
|
405 | tree = libReport.summarizers.pkg(map)
|
406 |
|
407 | this.reporter.forEach((_reporter) => {
|
408 | tree.visit(reports.create(_reporter, {
|
409 | skipEmpty: this.config.skipEmpty,
|
410 | skipFull: this.config.skipFull
|
411 | }), context)
|
412 | })
|
413 |
|
414 | if (this._showProcessTree) {
|
415 | this.showProcessTree()
|
416 | }
|
417 | }
|
418 |
|
419 |
|
420 | writeProcessIndex () {
|
421 | const dir = this.processInfoDirectory()
|
422 | const pidToUid = new Map()
|
423 | const infoByUid = new Map()
|
424 | const eidToUid = new Map()
|
425 | const infos = fs.readdirSync(dir).filter(f => f !== 'index.json').map(f => {
|
426 | try {
|
427 | const info = JSON.parse(fs.readFileSync(path.resolve(dir, f), 'utf-8'))
|
428 | info.children = []
|
429 | pidToUid.set(info.uuid, info.pid)
|
430 | pidToUid.set(info.pid, info.uuid)
|
431 | infoByUid.set(info.uuid, info)
|
432 | if (info.externalId) {
|
433 | eidToUid.set(info.externalId, info.uuid)
|
434 | }
|
435 | return info
|
436 | } catch (er) {
|
437 | return null
|
438 | }
|
439 | }).filter(Boolean)
|
440 |
|
441 |
|
442 | infos.forEach(info => {
|
443 | if (info.parent) {
|
444 | const parentInfo = infoByUid.get(info.parent)
|
445 | if (parentInfo && !parentInfo.children.includes(info.uuid)) {
|
446 | parentInfo.children.push(info.uuid)
|
447 | }
|
448 | }
|
449 | })
|
450 |
|
451 |
|
452 | const files = infos.reduce((files, info) => {
|
453 | info.files.forEach(f => {
|
454 | files[f] = files[f] || []
|
455 | files[f].push(info.uuid)
|
456 | })
|
457 | return files
|
458 | }, {})
|
459 |
|
460 |
|
461 | const index = infos.reduce((index, info) => {
|
462 | index.processes[info.uuid] = {}
|
463 | index.processes[info.uuid].parent = info.parent
|
464 | if (info.externalId) {
|
465 | if (index.externalIds[info.externalId]) {
|
466 | throw new Error(`External ID ${info.externalId} used by multiple processes`)
|
467 | }
|
468 | index.processes[info.uuid].externalId = info.externalId
|
469 | index.externalIds[info.externalId] = {
|
470 | root: info.uuid,
|
471 | children: info.children
|
472 | }
|
473 | }
|
474 | index.processes[info.uuid].children = Array.from(info.children)
|
475 | return index
|
476 | }, { processes: {}, files: files, externalIds: {} })
|
477 |
|
478 |
|
479 | Object.keys(index.externalIds).forEach(eid => {
|
480 | const { children } = index.externalIds[eid]
|
481 |
|
482 | for (let i = 0; i < children.length; i++) {
|
483 | const nextGen = index.processes[children[i]].children
|
484 | if (nextGen && nextGen.length) {
|
485 | children.push(...nextGen.filter(uuid => children.indexOf(uuid) === -1))
|
486 | }
|
487 | }
|
488 | })
|
489 |
|
490 | fs.writeFileSync(path.resolve(dir, 'index.json'), JSON.stringify(index))
|
491 | }
|
492 |
|
493 | showProcessTree () {
|
494 | var processTree = ProcessInfo.buildProcessTree(this._loadProcessInfos())
|
495 |
|
496 | console.log(processTree.render(this))
|
497 | }
|
498 |
|
499 | checkCoverage (thresholds, perFile) {
|
500 | var map = this.getCoverageMapFromAllCoverageFiles()
|
501 | var nyc = this
|
502 |
|
503 | if (perFile) {
|
504 | map.files().forEach(function (file) {
|
505 |
|
506 | nyc._checkCoverage(map.fileCoverageFor(file).toSummary(), thresholds, file)
|
507 | })
|
508 | } else {
|
509 |
|
510 | nyc._checkCoverage(map.getCoverageSummary(), thresholds)
|
511 | }
|
512 | }
|
513 |
|
514 | _checkCoverage (summary, thresholds, file) {
|
515 | Object.keys(thresholds).forEach(function (key) {
|
516 | var coverage = summary[key].pct
|
517 | if (coverage < thresholds[key]) {
|
518 | process.exitCode = 1
|
519 | if (file) {
|
520 | console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet threshold (' + thresholds[key] + '%) for ' + file)
|
521 | } else {
|
522 | console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet global threshold (' + thresholds[key] + '%)')
|
523 | }
|
524 | }
|
525 | })
|
526 | }
|
527 |
|
528 | _loadProcessInfos () {
|
529 | return fs.readdirSync(this.processInfoDirectory()).map(f => {
|
530 | let data
|
531 | try {
|
532 | data = JSON.parse(fs.readFileSync(
|
533 | path.resolve(this.processInfoDirectory(), f),
|
534 | 'utf-8'
|
535 | ))
|
536 | } catch (e) {
|
537 | return null
|
538 | }
|
539 | if (f !== 'index.json') {
|
540 | data.nodes = []
|
541 | data = new ProcessInfo(data)
|
542 | }
|
543 | return { file: path.basename(f, '.json'), data: data }
|
544 | }).filter(Boolean).reduce((infos, info) => {
|
545 | infos[info.file] = info.data
|
546 | return infos
|
547 | }, {})
|
548 | }
|
549 |
|
550 | eachReport (filenames, iterator, baseDirectory) {
|
551 | baseDirectory = baseDirectory || this.tempDirectory()
|
552 |
|
553 | if (typeof filenames === 'function') {
|
554 | iterator = filenames
|
555 | filenames = undefined
|
556 | }
|
557 |
|
558 | var _this = this
|
559 | var files = filenames || fs.readdirSync(baseDirectory)
|
560 |
|
561 | files.forEach(function (f) {
|
562 | var report
|
563 | try {
|
564 | report = JSON.parse(fs.readFileSync(
|
565 | path.resolve(baseDirectory, f),
|
566 | 'utf-8'
|
567 | ))
|
568 |
|
569 | _this.sourceMaps.reloadCachedSourceMaps(report)
|
570 | } catch (e) {
|
571 | report = {}
|
572 | }
|
573 |
|
574 | iterator(report)
|
575 | })
|
576 | }
|
577 |
|
578 | loadReports (filenames) {
|
579 | var reports = []
|
580 |
|
581 | this.eachReport(filenames, (report) => {
|
582 | reports.push(report)
|
583 | })
|
584 |
|
585 | return reports
|
586 | }
|
587 |
|
588 | tempDirectory () {
|
589 | return path.resolve(this.cwd, this._tempDirectory)
|
590 | }
|
591 |
|
592 | reportDirectory () {
|
593 | return path.resolve(this.cwd, this._reportDir)
|
594 | }
|
595 |
|
596 | processInfoDirectory () {
|
597 | return path.resolve(this.tempDirectory(), 'processinfo')
|
598 | }
|
599 | }
|
600 |
|
601 | module.exports = NYC
|