diff --git a/README.md b/README.md index 77c401c94..2873e44e8 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,26 @@ __instrument the entire ./lib folder:__ `nyc instrument ./lib ./output` +## Process tree information + +nyc is able to show you all Node processes that are spawned when running a +test script under it: + +``` +$ nyc --show-process-tree npm test + 3 passed +----------|----------|----------|----------|----------|----------------| +File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines | +----------|----------|----------|----------|----------|----------------| +All files | 100 | 100 | 100 | 100 | | + index.js | 100 | 100 | 100 | 100 | | +----------|----------|----------|----------|----------|----------------| +nyc +└─┬ /usr/local/bin/node /usr/local/bin/npm test + └─┬ /usr/local/bin/node /path/to/your/project/node_modules/.bin/ava + └── /usr/local/bin/node /path/to/your/project/node_modules/ava/lib/test-worker.js … +``` + ## Integrating with coveralls [coveralls.io](https://coveralls.io) is a great tool for adding diff --git a/bin/nyc.js b/bin/nyc.js index a16de4fea..1f74661fe 100755 --- a/bin/nyc.js +++ b/bin/nyc.js @@ -44,7 +44,8 @@ if (argv._[0] === 'report') { exclude: argv.exclude, sourceMap: !!argv.sourceMap, instrumenter: argv.instrumenter, - hookRunInContext: argv.hookRunInContext + hookRunInContext: argv.hookRunInContext, + showProcessTree: argv.showProcessTree })) nyc.reset() @@ -56,6 +57,8 @@ if (argv._[0] === 'report') { NYC_SOURCE_MAP: argv.sourceMap ? 'enable' : 'disable', NYC_INSTRUMENTER: argv.instrumenter, NYC_HOOK_RUN_IN_CONTEXT: argv.hookRunInContext ? 'enable' : 'disable', + NYC_SHOW_PROCESS_TREE: argv.showProcessTree ? 'enable' : 'disable', + NYC_ROOT_ID: nyc.rootId, BABEL_DISABLE_CACHE: 1 } if (argv.require.length) { @@ -101,11 +104,13 @@ if (argv._[0] === 'report') { function report (argv) { process.env.NYC_CWD = process.cwd() - ;(new NYC({ + var nyc = new NYC({ reporter: argv.reporter, reportDir: argv.reportDir, - tempDirectory: argv.tempDirectory - })).report() + tempDirectory: argv.tempDirectory, + showProcessTree: argv.showProcessTree + }) + nyc.report() } function checkCoverage (argv, cb) { @@ -138,6 +143,11 @@ function buildYargs () { describe: 'directory from which coverage JSON files are read', default: './.nyc_output' }) + .option('show-process-tree', { + describe: 'display the tree of spawned processes', + default: false, + type: 'boolean' + }) .example('$0 report --reporter=lcov', 'output an HTML lcov report to ./coverage') }) .command('check-coverage', 'check whether coverage is within thresholds provided', function (yargs) { @@ -244,6 +254,11 @@ function buildYargs () { type: 'boolean', description: 'should nyc wrap vm.runInThisContext?' }) + .option('show-process-tree', { + describe: 'display the tree of spawned processes', + default: false, + type: 'boolean' + }) .pkgConf('nyc', process.cwd()) .example('$0 npm test', 'instrument your tests with coverage') .example('$0 --require babel-core/register npm test', 'instrument your tests with coverage and babel') diff --git a/bin/wrap.js b/bin/wrap.js index b0bca588b..c53925343 100644 --- a/bin/wrap.js +++ b/bin/wrap.js @@ -6,6 +6,9 @@ try { NYC = require('../index.js') } +var parentPid = process.env.NYC_PARENT_PID || '0' +process.env.NYC_PARENT_PID = process.pid + ;(new NYC({ require: process.env.NYC_REQUIRE ? process.env.NYC_REQUIRE.split(',') : [], extension: process.env.NYC_EXTENSION ? process.env.NYC_EXTENSION.split(',') : [], @@ -14,7 +17,12 @@ try { enableCache: process.env.NYC_CACHE === 'enable', sourceMap: process.env.NYC_SOURCE_MAP === 'enable', instrumenter: process.env.NYC_INSTRUMENTER, - hookRunInContext: process.env.NYC_HOOK_RUN_IN_CONTEXT === 'enable' + hookRunInContext: process.env.NYC_HOOK_RUN_IN_CONTEXT === 'enable', + showProcessTree: process.env.NYC_SHOW_PROCESS_TREE === 'enable', + _processInfo: { + ppid: parentPid, + root: process.env.NYC_ROOT_ID + } })).wrap() sw.runMain() diff --git a/build-self-coverage.js b/build-self-coverage.js index 710a9f92d..bc8c2b63c 100644 --- a/build-self-coverage.js +++ b/build-self-coverage.js @@ -3,7 +3,8 @@ var fs = require('fs') var path = require('path') ;[ - 'index.js' + 'index.js', + 'lib/process.js' ].forEach(function (name) { var indexPath = path.join(__dirname, name) var source = fs.readFileSync(indexPath, 'utf8') diff --git a/index.js b/index.js index 73aaae9da..993a9744f 100755 --- a/index.js +++ b/index.js @@ -22,6 +22,13 @@ var pkgUp = require('pkg-up') var testExclude = require('test-exclude') var yargs = require('yargs') +var ProcessInfo +try { + ProcessInfo = require('./lib/process.covered.js') +} catch (e) { + ProcessInfo = require('./lib/process.js') +} + /* istanbul ignore next */ if (/index\.covered\.js$/.test(__filename)) { require('./lib/self-coverage-helper') @@ -36,6 +43,7 @@ function NYC (opts) { this._instrumenterLib = require(config.instrumenter || './lib/instrumenters/istanbul') this._reportDir = config.reportDir this._sourceMap = config.sourceMap + this._showProcessTree = config.showProcessTree this.cwd = config.cwd this.reporter = arrify(config.reporter || 'text') @@ -71,6 +79,9 @@ function NYC (opts) { this.hashCache = {} this.loadedMaps = null this.fakeRequire = null + + this.processInfo = new ProcessInfo(opts && opts._processInfo) + this.rootId = this.processInfo.root || this.generateUniqueID() } NYC.prototype._loadConfig = function (opts) { @@ -327,6 +338,10 @@ NYC.prototype.clearCache = function () { NYC.prototype.createTempDirectory = function () { mkdirp.sync(this.tempDirectory()) + + if (this._showProcessTree) { + mkdirp.sync(this.processInfoDirectory()) + } } NYC.prototype.reset = function () { @@ -352,6 +367,12 @@ NYC.prototype.wrap = function (bin) { return this } +NYC.prototype.generateUniqueID = function () { + return md5hex( + process.hrtime().concat(process.pid).map(String) + ) +} + NYC.prototype.writeCoverageFile = function () { var coverage = coverageFinder() if (!coverage) return @@ -366,15 +387,26 @@ NYC.prototype.writeCoverageFile = function () { coverage = this.sourceMapTransform(coverage) } - var id = md5hex( - process.hrtime().concat(process.pid).map(String) - ) + var id = this.generateUniqueID() + var coverageFilename = path.resolve(this.tempDirectory(), id + '.json') fs.writeFileSync( - path.resolve(this.tempDirectory(), './', id + '.json'), + coverageFilename, JSON.stringify(coverage), 'utf-8' ) + + if (!this._showProcessTree) { + return + } + + this.processInfo.coverageFilename = coverageFilename + + fs.writeFileSync( + path.resolve(this.processInfoDirectory(), id + '.json'), + JSON.stringify(this.processInfo), + 'utf-8' + ) } NYC.prototype.sourceMapTransform = function (obj) { @@ -413,6 +445,14 @@ NYC.prototype.report = function () { this.reporter.forEach(function (_reporter) { tree.visit(reports.create(_reporter), context) }) + + if (this._showProcessTree) { + this.showProcessTree() + } +} + +NYC.prototype.showProcessTree = function () { + console.log(this._loadProcessInfoTree().render()) } NYC.prototype.checkCoverage = function (thresholds) { @@ -429,6 +469,26 @@ NYC.prototype.checkCoverage = function (thresholds) { }) } +NYC.prototype._loadProcessInfoTree = function () { + return ProcessInfo.buildProcessTree(this._loadProcessInfos()) +} + +NYC.prototype._loadProcessInfos = function () { + var _this = this + var files = fs.readdirSync(this.processInfoDirectory()) + + return files.map(function (f) { + try { + return new ProcessInfo(JSON.parse(fs.readFileSync( + path.resolve(_this.processInfoDirectory(), f), + 'utf-8' + ))) + } catch (e) { // handle corrupt JSON output. + return {} + } + }) +} + NYC.prototype._loadReports = function () { var _this = this var files = fs.readdirSync(this.tempDirectory()) @@ -441,7 +501,7 @@ NYC.prototype._loadReports = function () { var report try { report = JSON.parse(fs.readFileSync( - path.resolve(_this.tempDirectory(), './', f), + path.resolve(_this.tempDirectory(), f), 'utf-8' )) } catch (e) { // handle corrupt JSON output. @@ -472,7 +532,11 @@ NYC.prototype._loadReports = function () { } NYC.prototype.tempDirectory = function () { - return path.resolve(this.cwd, './', this._tempDirectory) + return path.resolve(this.cwd, this._tempDirectory) +} + +NYC.prototype.processInfoDirectory = function () { + return path.resolve(this.tempDirectory(), 'processinfo') } module.exports = NYC diff --git a/lib/process.js b/lib/process.js new file mode 100644 index 000000000..cc5d7865a --- /dev/null +++ b/lib/process.js @@ -0,0 +1,64 @@ +'use strict' +var archy = require('archy') + +function ProcessInfo (defaults) { + defaults = defaults || {} + + this.pid = String(process.pid) + this.argv = process.argv + this.execArgv = process.execArgv + this.cwd = process.cwd() + this.time = Date.now() + this.ppid = null + this.root = null + this.coverageFilename = null + this.nodes = [] // list of children, filled by buildProcessTree() + + for (var key in defaults) { + this[key] = defaults[key] + } +} + +Object.defineProperty(ProcessInfo.prototype, 'label', { + get: function () { + if (this._label) { + return this._label + } + + return this.argv.join(' ') + } +}) + +ProcessInfo.buildProcessTree = function (infos) { + var treeRoot = new ProcessInfo({ _label: 'nyc' }) + var nodes = { } + + infos = infos.sort(function (a, b) { + return a.time - b.time + }) + + infos.forEach(function (p) { + nodes[p.root + ':' + p.pid] = p + }) + + infos.forEach(function (p) { + if (!p.ppid) { + return + } + + var parent = nodes[p.root + ':' + p.ppid] + if (!parent) { + parent = treeRoot + } + + parent.nodes.push(p) + }) + + return treeRoot +} + +ProcessInfo.prototype.render = function () { + return archy(this) +} + +module.exports = ProcessInfo diff --git a/package.json b/package.json index 4cbc11236..f44114ed6 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "author": "Ben Coe ", "license": "ISC", "dependencies": { + "archy": "^1.0.0", "arrify": "^1.0.1", "caching-transform": "^1.0.0", "convert-source-map": "^1.3.0", @@ -124,6 +125,7 @@ "url": "git@github.com:istanbuljs/nyc.git" }, "bundledDependencies": [ + "archy", "arrify", "caching-transform", "convert-source-map", diff --git a/test/fixtures/cli/selfspawn-fibonacci.js b/test/fixtures/cli/selfspawn-fibonacci.js new file mode 100644 index 000000000..d176415d7 --- /dev/null +++ b/test/fixtures/cli/selfspawn-fibonacci.js @@ -0,0 +1,30 @@ +'use strict'; +var cp = require('child_process'); + +var index = +process.argv[2] || 0 +if (index <= 1) { + console.log(0) + return +} +if (index == 2) { + console.log(1) + return +} + +function getFromChild(n, cb) { + var proc = cp.spawn(process.execPath, [__filename, n]) + var stdout = '' + proc.stdout.on('data', function (data) { stdout += data }) + proc.on('close', function () { + cb(null, +stdout) + }) + proc.on('error', cb) +} + +getFromChild(index - 1, function(err, result1) { + if (err) throw err + getFromChild(index - 2, function(err, result2) { + if (err) throw err + console.log(result1 + result2) + }) +}) diff --git a/test/src/nyc-bin.js b/test/src/nyc-bin.js index 952e0586e..faa2ff4d1 100644 --- a/test/src/nyc-bin.js +++ b/test/src/nyc-bin.js @@ -465,4 +465,55 @@ describe('the nyc cli', function () { }) }) }) + + describe('--show-process-tree', function () { + it('displays a tree of spawned processes', function (done) { + var args = [bin, '--show-process-tree', process.execPath, 'selfspawn-fibonacci.js', '5'] + + var proc = spawn(process.execPath, args, { + cwd: fixturesCLI, + env: env + }) + + var stdout = '' + proc.stdout.setEncoding('utf8') + proc.stdout.on('data', function (chunk) { + stdout += chunk + }) + + proc.on('close', function (code) { + code.should.equal(0) + stdout.should.match(new RegExp( + 'nyc\n' + + '└─┬.*selfspawn-fibonacci.js 5\n' + + ' ├─┬.*selfspawn-fibonacci.js 4\n' + + ' │ ├─┬.*selfspawn-fibonacci.js 3\n' + + ' │ │ ├──.*selfspawn-fibonacci.js 2\n' + + ' │ │ └──.*selfspawn-fibonacci.js 1\n' + + ' │ └──.*selfspawn-fibonacci.js 2\n' + + ' └─┬.*selfspawn-fibonacci.js 3\n' + + ' ├──.*selfspawn-fibonacci.js 2\n' + + ' └──.*selfspawn-fibonacci.js 1\n' + )) + done() + }) + }) + + it('doesn’t create the temp directory for process info files when not present', function (done) { + var args = [bin, process.execPath, 'selfspawn-fibonacci.js', '5'] + + var proc = spawn(process.execPath, args, { + cwd: fixturesCLI, + env: env + }) + + proc.on('exit', function (code) { + code.should.equal(0) + fs.stat(path.resolve(fixturesCLI, '.nyc_output', 'processinfo'), function (err, stat) { + err.code.should.equal('ENOENT') + done() + }) + }) + }) + }) })