Skip to content

Commit

Permalink
feat: gather process tree information (#364)
Browse files Browse the repository at this point in the history
* feat: gather process tree information

Add a `--show-process-tree` that shows a pretty tree of all spawned
processes after `nyc` has run.

The data files for that are stored in `processinfo` are stored in
`(temp directory)/processinfo` so that they don’t interfere with
the fixed format of the coverage files.

Fixes: #158

* [squash] cleanup: make ProcessTree instances nodes of the tree themselves

This is in preparation for per-subtree coverage at some point.

* [squash] add short section about --show-process-tree to readme
  • Loading branch information
addaleax authored and bcoe committed Sep 2, 2016
1 parent 64c68b7 commit fabe5f3
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 12 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions bin/nyc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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')
Expand Down
10 changes: 9 additions & 1 deletion bin/wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(',') : [],
Expand All @@ -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()
3 changes: 2 additions & 1 deletion build-self-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
76 changes: 70 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 () {
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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())
Expand All @@ -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.
Expand Down Expand Up @@ -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
64 changes: 64 additions & 0 deletions lib/process.js
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"author": "Ben Coe <[email protected]>",
"license": "ISC",
"dependencies": {
"archy": "^1.0.0",
"arrify": "^1.0.1",
"caching-transform": "^1.0.0",
"convert-source-map": "^1.3.0",
Expand Down Expand Up @@ -124,6 +125,7 @@
"url": "[email protected]:istanbuljs/nyc.git"
},
"bundledDependencies": [
"archy",
"arrify",
"caching-transform",
"convert-source-map",
Expand Down
30 changes: 30 additions & 0 deletions test/fixtures/cli/selfspawn-fibonacci.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading

0 comments on commit fabe5f3

Please sign in to comment.