Skip to content

Commit fabe5f3

Browse files
addaleaxbcoe
authored andcommitted
feat: gather process tree information (#364)
* 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
1 parent 64c68b7 commit fabe5f3

9 files changed

+267
-12
lines changed

README.md

+20
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,26 @@ __instrument the entire ./lib folder:__
269269

270270
`nyc instrument ./lib ./output`
271271

272+
## Process tree information
273+
274+
nyc is able to show you all Node processes that are spawned when running a
275+
test script under it:
276+
277+
```
278+
$ nyc --show-process-tree npm test
279+
3 passed
280+
----------|----------|----------|----------|----------|----------------|
281+
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
282+
----------|----------|----------|----------|----------|----------------|
283+
All files | 100 | 100 | 100 | 100 | |
284+
index.js | 100 | 100 | 100 | 100 | |
285+
----------|----------|----------|----------|----------|----------------|
286+
nyc
287+
└─┬ /usr/local/bin/node /usr/local/bin/npm test
288+
└─┬ /usr/local/bin/node /path/to/your/project/node_modules/.bin/ava
289+
└── /usr/local/bin/node /path/to/your/project/node_modules/ava/lib/test-worker.js …
290+
```
291+
272292
## Integrating with coveralls
273293

274294
[coveralls.io](https://coveralls.io) is a great tool for adding

bin/nyc.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ if (argv._[0] === 'report') {
4444
exclude: argv.exclude,
4545
sourceMap: !!argv.sourceMap,
4646
instrumenter: argv.instrumenter,
47-
hookRunInContext: argv.hookRunInContext
47+
hookRunInContext: argv.hookRunInContext,
48+
showProcessTree: argv.showProcessTree
4849
}))
4950
nyc.reset()
5051

@@ -56,6 +57,8 @@ if (argv._[0] === 'report') {
5657
NYC_SOURCE_MAP: argv.sourceMap ? 'enable' : 'disable',
5758
NYC_INSTRUMENTER: argv.instrumenter,
5859
NYC_HOOK_RUN_IN_CONTEXT: argv.hookRunInContext ? 'enable' : 'disable',
60+
NYC_SHOW_PROCESS_TREE: argv.showProcessTree ? 'enable' : 'disable',
61+
NYC_ROOT_ID: nyc.rootId,
5962
BABEL_DISABLE_CACHE: 1
6063
}
6164
if (argv.require.length) {
@@ -101,11 +104,13 @@ if (argv._[0] === 'report') {
101104
function report (argv) {
102105
process.env.NYC_CWD = process.cwd()
103106

104-
;(new NYC({
107+
var nyc = new NYC({
105108
reporter: argv.reporter,
106109
reportDir: argv.reportDir,
107-
tempDirectory: argv.tempDirectory
108-
})).report()
110+
tempDirectory: argv.tempDirectory,
111+
showProcessTree: argv.showProcessTree
112+
})
113+
nyc.report()
109114
}
110115

111116
function checkCoverage (argv, cb) {
@@ -138,6 +143,11 @@ function buildYargs () {
138143
describe: 'directory from which coverage JSON files are read',
139144
default: './.nyc_output'
140145
})
146+
.option('show-process-tree', {
147+
describe: 'display the tree of spawned processes',
148+
default: false,
149+
type: 'boolean'
150+
})
141151
.example('$0 report --reporter=lcov', 'output an HTML lcov report to ./coverage')
142152
})
143153
.command('check-coverage', 'check whether coverage is within thresholds provided', function (yargs) {
@@ -244,6 +254,11 @@ function buildYargs () {
244254
type: 'boolean',
245255
description: 'should nyc wrap vm.runInThisContext?'
246256
})
257+
.option('show-process-tree', {
258+
describe: 'display the tree of spawned processes',
259+
default: false,
260+
type: 'boolean'
261+
})
247262
.pkgConf('nyc', process.cwd())
248263
.example('$0 npm test', 'instrument your tests with coverage')
249264
.example('$0 --require babel-core/register npm test', 'instrument your tests with coverage and babel')

bin/wrap.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ try {
66
NYC = require('../index.js')
77
}
88

9+
var parentPid = process.env.NYC_PARENT_PID || '0'
10+
process.env.NYC_PARENT_PID = process.pid
11+
912
;(new NYC({
1013
require: process.env.NYC_REQUIRE ? process.env.NYC_REQUIRE.split(',') : [],
1114
extension: process.env.NYC_EXTENSION ? process.env.NYC_EXTENSION.split(',') : [],
@@ -14,7 +17,12 @@ try {
1417
enableCache: process.env.NYC_CACHE === 'enable',
1518
sourceMap: process.env.NYC_SOURCE_MAP === 'enable',
1619
instrumenter: process.env.NYC_INSTRUMENTER,
17-
hookRunInContext: process.env.NYC_HOOK_RUN_IN_CONTEXT === 'enable'
20+
hookRunInContext: process.env.NYC_HOOK_RUN_IN_CONTEXT === 'enable',
21+
showProcessTree: process.env.NYC_SHOW_PROCESS_TREE === 'enable',
22+
_processInfo: {
23+
ppid: parentPid,
24+
root: process.env.NYC_ROOT_ID
25+
}
1826
})).wrap()
1927

2028
sw.runMain()

build-self-coverage.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ var fs = require('fs')
33
var path = require('path')
44

55
;[
6-
'index.js'
6+
'index.js',
7+
'lib/process.js'
78
].forEach(function (name) {
89
var indexPath = path.join(__dirname, name)
910
var source = fs.readFileSync(indexPath, 'utf8')

index.js

+70-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ var pkgUp = require('pkg-up')
2222
var testExclude = require('test-exclude')
2323
var yargs = require('yargs')
2424

25+
var ProcessInfo
26+
try {
27+
ProcessInfo = require('./lib/process.covered.js')
28+
} catch (e) {
29+
ProcessInfo = require('./lib/process.js')
30+
}
31+
2532
/* istanbul ignore next */
2633
if (/index\.covered\.js$/.test(__filename)) {
2734
require('./lib/self-coverage-helper')
@@ -36,6 +43,7 @@ function NYC (opts) {
3643
this._instrumenterLib = require(config.instrumenter || './lib/instrumenters/istanbul')
3744
this._reportDir = config.reportDir
3845
this._sourceMap = config.sourceMap
46+
this._showProcessTree = config.showProcessTree
3947
this.cwd = config.cwd
4048

4149
this.reporter = arrify(config.reporter || 'text')
@@ -71,6 +79,9 @@ function NYC (opts) {
7179
this.hashCache = {}
7280
this.loadedMaps = null
7381
this.fakeRequire = null
82+
83+
this.processInfo = new ProcessInfo(opts && opts._processInfo)
84+
this.rootId = this.processInfo.root || this.generateUniqueID()
7485
}
7586

7687
NYC.prototype._loadConfig = function (opts) {
@@ -327,6 +338,10 @@ NYC.prototype.clearCache = function () {
327338

328339
NYC.prototype.createTempDirectory = function () {
329340
mkdirp.sync(this.tempDirectory())
341+
342+
if (this._showProcessTree) {
343+
mkdirp.sync(this.processInfoDirectory())
344+
}
330345
}
331346

332347
NYC.prototype.reset = function () {
@@ -352,6 +367,12 @@ NYC.prototype.wrap = function (bin) {
352367
return this
353368
}
354369

370+
NYC.prototype.generateUniqueID = function () {
371+
return md5hex(
372+
process.hrtime().concat(process.pid).map(String)
373+
)
374+
}
375+
355376
NYC.prototype.writeCoverageFile = function () {
356377
var coverage = coverageFinder()
357378
if (!coverage) return
@@ -366,15 +387,26 @@ NYC.prototype.writeCoverageFile = function () {
366387
coverage = this.sourceMapTransform(coverage)
367388
}
368389

369-
var id = md5hex(
370-
process.hrtime().concat(process.pid).map(String)
371-
)
390+
var id = this.generateUniqueID()
391+
var coverageFilename = path.resolve(this.tempDirectory(), id + '.json')
372392

373393
fs.writeFileSync(
374-
path.resolve(this.tempDirectory(), './', id + '.json'),
394+
coverageFilename,
375395
JSON.stringify(coverage),
376396
'utf-8'
377397
)
398+
399+
if (!this._showProcessTree) {
400+
return
401+
}
402+
403+
this.processInfo.coverageFilename = coverageFilename
404+
405+
fs.writeFileSync(
406+
path.resolve(this.processInfoDirectory(), id + '.json'),
407+
JSON.stringify(this.processInfo),
408+
'utf-8'
409+
)
378410
}
379411

380412
NYC.prototype.sourceMapTransform = function (obj) {
@@ -413,6 +445,14 @@ NYC.prototype.report = function () {
413445
this.reporter.forEach(function (_reporter) {
414446
tree.visit(reports.create(_reporter), context)
415447
})
448+
449+
if (this._showProcessTree) {
450+
this.showProcessTree()
451+
}
452+
}
453+
454+
NYC.prototype.showProcessTree = function () {
455+
console.log(this._loadProcessInfoTree().render())
416456
}
417457

418458
NYC.prototype.checkCoverage = function (thresholds) {
@@ -429,6 +469,26 @@ NYC.prototype.checkCoverage = function (thresholds) {
429469
})
430470
}
431471

472+
NYC.prototype._loadProcessInfoTree = function () {
473+
return ProcessInfo.buildProcessTree(this._loadProcessInfos())
474+
}
475+
476+
NYC.prototype._loadProcessInfos = function () {
477+
var _this = this
478+
var files = fs.readdirSync(this.processInfoDirectory())
479+
480+
return files.map(function (f) {
481+
try {
482+
return new ProcessInfo(JSON.parse(fs.readFileSync(
483+
path.resolve(_this.processInfoDirectory(), f),
484+
'utf-8'
485+
)))
486+
} catch (e) { // handle corrupt JSON output.
487+
return {}
488+
}
489+
})
490+
}
491+
432492
NYC.prototype._loadReports = function () {
433493
var _this = this
434494
var files = fs.readdirSync(this.tempDirectory())
@@ -441,7 +501,7 @@ NYC.prototype._loadReports = function () {
441501
var report
442502
try {
443503
report = JSON.parse(fs.readFileSync(
444-
path.resolve(_this.tempDirectory(), './', f),
504+
path.resolve(_this.tempDirectory(), f),
445505
'utf-8'
446506
))
447507
} catch (e) { // handle corrupt JSON output.
@@ -472,7 +532,11 @@ NYC.prototype._loadReports = function () {
472532
}
473533

474534
NYC.prototype.tempDirectory = function () {
475-
return path.resolve(this.cwd, './', this._tempDirectory)
535+
return path.resolve(this.cwd, this._tempDirectory)
536+
}
537+
538+
NYC.prototype.processInfoDirectory = function () {
539+
return path.resolve(this.tempDirectory(), 'processinfo')
476540
}
477541

478542
module.exports = NYC

lib/process.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict'
2+
var archy = require('archy')
3+
4+
function ProcessInfo (defaults) {
5+
defaults = defaults || {}
6+
7+
this.pid = String(process.pid)
8+
this.argv = process.argv
9+
this.execArgv = process.execArgv
10+
this.cwd = process.cwd()
11+
this.time = Date.now()
12+
this.ppid = null
13+
this.root = null
14+
this.coverageFilename = null
15+
this.nodes = [] // list of children, filled by buildProcessTree()
16+
17+
for (var key in defaults) {
18+
this[key] = defaults[key]
19+
}
20+
}
21+
22+
Object.defineProperty(ProcessInfo.prototype, 'label', {
23+
get: function () {
24+
if (this._label) {
25+
return this._label
26+
}
27+
28+
return this.argv.join(' ')
29+
}
30+
})
31+
32+
ProcessInfo.buildProcessTree = function (infos) {
33+
var treeRoot = new ProcessInfo({ _label: 'nyc' })
34+
var nodes = { }
35+
36+
infos = infos.sort(function (a, b) {
37+
return a.time - b.time
38+
})
39+
40+
infos.forEach(function (p) {
41+
nodes[p.root + ':' + p.pid] = p
42+
})
43+
44+
infos.forEach(function (p) {
45+
if (!p.ppid) {
46+
return
47+
}
48+
49+
var parent = nodes[p.root + ':' + p.ppid]
50+
if (!parent) {
51+
parent = treeRoot
52+
}
53+
54+
parent.nodes.push(p)
55+
})
56+
57+
return treeRoot
58+
}
59+
60+
ProcessInfo.prototype.render = function () {
61+
return archy(this)
62+
}
63+
64+
module.exports = ProcessInfo

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"author": "Ben Coe <[email protected]>",
7373
"license": "ISC",
7474
"dependencies": {
75+
"archy": "^1.0.0",
7576
"arrify": "^1.0.1",
7677
"caching-transform": "^1.0.0",
7778
"convert-source-map": "^1.3.0",
@@ -124,6 +125,7 @@
124125
"url": "[email protected]:istanbuljs/nyc.git"
125126
},
126127
"bundledDependencies": [
128+
"archy",
127129
"arrify",
128130
"caching-transform",
129131
"convert-source-map",
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
var cp = require('child_process');
3+
4+
var index = +process.argv[2] || 0
5+
if (index <= 1) {
6+
console.log(0)
7+
return
8+
}
9+
if (index == 2) {
10+
console.log(1)
11+
return
12+
}
13+
14+
function getFromChild(n, cb) {
15+
var proc = cp.spawn(process.execPath, [__filename, n])
16+
var stdout = ''
17+
proc.stdout.on('data', function (data) { stdout += data })
18+
proc.on('close', function () {
19+
cb(null, +stdout)
20+
})
21+
proc.on('error', cb)
22+
}
23+
24+
getFromChild(index - 1, function(err, result1) {
25+
if (err) throw err
26+
getFromChild(index - 2, function(err, result2) {
27+
if (err) throw err
28+
console.log(result1 + result2)
29+
})
30+
})

0 commit comments

Comments
 (0)