Skip to content

Commit 8dcf180

Browse files
isaacscoreyfarrell
authored andcommitted
feat: add processinfo index, add externalId (#1055)
If a NYC_PROCESSINFO_EXTERNAL_ID environment variable is set, then it is saved in the processinfo as `externalId`. Furthermore, when this file is generated, some additional helpful metadata is memoized to the processinfo json files, to minimize the cost of repeated generation. (This isn't necessarily a breaking change, but it is an update to the de facto schema for those files.) As soon as possible, index generation and process tree display should be migrated out to a new 'istanbul-lib-processinfo' library. This opens the door to add features in the v14 release family to improve support for partial/resumed test runs and file watching. - When a process is run with --clean=false and a previously seen externalId, clear away all the coverage files in the set for that externalId. - When a file is changed, a test runner can use the index to determine which tests (by externalId) ought to be re-run. - Adds a NYC_PROCESS_ID to environment - Adds `parent` to processInfo object, a uuid referring to parent. - Rebase onto processinfo-numeric-pids branch - Avoid re-writing the processinfo/{uuid}.json files - Update process tree output to rely on process index instead of duplicating effort. BREAKING CHANGE: This adds a file named 'index.json' to the .nyc_output/processinfo directory, which has a different format from the other files in this dir.
1 parent 32f75b0 commit 8dcf180

File tree

5 files changed

+214
-34
lines changed

5 files changed

+214
-34
lines changed

bin/nyc.js

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ if ([
6565
), function (done) {
6666
var mainChildExitCode = process.exitCode
6767

68+
if (argv.showProcessTree || argv.buildProcessTree) {
69+
nyc.writeProcessIndex()
70+
}
71+
6872
if (argv.checkCoverage) {
6973
nyc.checkCoverage({
7074
lines: argv.lines,

bin/wrap.js

+5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ config.isChildProcess = true
88
config._processInfo = {
99
pid: process.pid,
1010
ppid: process.ppid,
11+
parent: process.env.NYC_PROCESS_ID || null,
1112
root: process.env.NYC_ROOT_ID
1213
}
14+
if (process.env.NYC_PROCESSINFO_EXTERNAL_ID) {
15+
config._processInfo.externalId = process.env.NYC_PROCESSINFO_EXTERNAL_ID
16+
delete process.env.NYC_PROCESSINFO_EXTERNAL_ID
17+
}
1318

1419
;(new NYC(config)).wrap()
1520

index.js

+92-10
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ NYC.prototype._wrapExit = function () {
312312
}
313313

314314
NYC.prototype.wrap = function (bin) {
315+
process.env.NYC_PROCESS_ID = this.processInfo.uuid
315316
this._addRequireHooks()
316317
this._wrapExit()
317318
this._loadAdditionalModules()
@@ -341,7 +342,7 @@ NYC.prototype.writeCoverageFile = function () {
341342
coverage = this.sourceMaps.remapCoverage(coverage)
342343
}
343344

344-
var id = this.generateUniqueID()
345+
var id = this.processInfo.uuid
345346
var coverageFilename = path.resolve(this.tempDirectory(), id + '.json')
346347

347348
fs.writeFileSync(
@@ -355,6 +356,7 @@ NYC.prototype.writeCoverageFile = function () {
355356
}
356357

357358
this.processInfo.coverageFilename = coverageFilename
359+
this.processInfo.files = Object.keys(coverage)
358360

359361
fs.writeFileSync(
360362
path.resolve(this.processInfoDirectory(), id + '.json'),
@@ -412,6 +414,80 @@ NYC.prototype.report = function () {
412414
}
413415
}
414416

417+
// XXX(@isaacs) Index generation should move to istanbul-lib-processinfo
418+
NYC.prototype.writeProcessIndex = function () {
419+
const dir = this.processInfoDirectory()
420+
const pidToUid = new Map()
421+
const infoByUid = new Map()
422+
const eidToUid = new Map()
423+
const infos = fs.readdirSync(dir).filter(f => f !== 'index.json').map(f => {
424+
try {
425+
const info = JSON.parse(fs.readFileSync(path.resolve(dir, f), 'utf-8'))
426+
info.children = []
427+
pidToUid.set(info.uuid, info.pid)
428+
pidToUid.set(info.pid, info.uuid)
429+
infoByUid.set(info.uuid, info)
430+
if (info.externalId) {
431+
eidToUid.set(info.externalId, info.uuid)
432+
}
433+
return info
434+
} catch (er) {
435+
return null
436+
}
437+
}).filter(Boolean)
438+
439+
// create all the parent-child links and write back the updated info
440+
infos.forEach(info => {
441+
if (info.parent) {
442+
const parentInfo = infoByUid.get(info.parent)
443+
if (parentInfo.children.indexOf(info.uuid) === -1) {
444+
parentInfo.children.push(info.uuid)
445+
}
446+
}
447+
})
448+
449+
// figure out which files were touched by each process.
450+
const files = infos.reduce((files, info) => {
451+
info.files.forEach(f => {
452+
files[f] = files[f] || []
453+
files[f].push(info.uuid)
454+
})
455+
return files
456+
}, {})
457+
458+
// build the actual index!
459+
const index = infos.reduce((index, info) => {
460+
index.processes[info.uuid] = {}
461+
index.processes[info.uuid].parent = info.parent
462+
if (info.externalId) {
463+
if (index.externalIds[info.externalId]) {
464+
throw new Error(`External ID ${info.externalId} used by multiple processes`)
465+
}
466+
index.processes[info.uuid].externalId = info.externalId
467+
index.externalIds[info.externalId] = {
468+
root: info.uuid,
469+
children: info.children
470+
}
471+
}
472+
index.processes[info.uuid].children = Array.from(info.children)
473+
return index
474+
}, { processes: {}, files: files, externalIds: {} })
475+
476+
// flatten the descendant sets of all the externalId procs
477+
Object.keys(index.externalIds).forEach(eid => {
478+
const { children } = index.externalIds[eid]
479+
// push the next generation onto the list so we accumulate them all
480+
for (let i = 0; i < children.length; i++) {
481+
const nextGen = index.processes[children[i]].children
482+
if (nextGen && nextGen.length) {
483+
children.push(...nextGen.filter(uuid => children.indexOf(uuid) === -1))
484+
}
485+
}
486+
})
487+
488+
fs.writeFileSync(path.resolve(dir, 'index.json'), JSON.stringify(index))
489+
}
490+
415491
NYC.prototype.showProcessTree = function () {
416492
var processTree = ProcessInfo.buildProcessTree(this._loadProcessInfos())
417493

@@ -448,19 +524,25 @@ NYC.prototype._checkCoverage = function (summary, thresholds, file) {
448524
}
449525

450526
NYC.prototype._loadProcessInfos = function () {
451-
var _this = this
452-
var files = fs.readdirSync(this.processInfoDirectory())
453-
454-
return files.map(function (f) {
527+
return fs.readdirSync(this.processInfoDirectory()).map(f => {
528+
let data
455529
try {
456-
return new ProcessInfo(JSON.parse(fs.readFileSync(
457-
path.resolve(_this.processInfoDirectory(), f),
530+
data = JSON.parse(fs.readFileSync(
531+
path.resolve(this.processInfoDirectory(), f),
458532
'utf-8'
459-
)))
533+
))
460534
} catch (e) { // handle corrupt JSON output.
461-
return {}
535+
return null
462536
}
463-
})
537+
if (f !== 'index.json') {
538+
data.nodes = []
539+
data = new ProcessInfo(data)
540+
}
541+
return { file: path.basename(f, '.json'), data: data }
542+
}).filter(Boolean).reduce((infos, info) => {
543+
infos[info.file] = info.data
544+
return infos
545+
}, {})
464546
}
465547

466548
NYC.prototype.eachReport = function (filenames, iterator, baseDirectory) {

lib/process.js

+18-24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
const archy = require('archy')
22
const libCoverage = require('istanbul-lib-coverage')
3+
const uuid = require('uuid/v4')
34

45
function ProcessInfo (defaults) {
56
defaults = defaults || {}
67

8+
this.uuid = null
9+
this.parent = null
710
this.pid = String(process.pid)
811
this.argv = process.argv
912
this.execArgv = process.execArgv
@@ -12,13 +15,14 @@ function ProcessInfo (defaults) {
1215
this.ppid = null
1316
this.root = null
1417
this.coverageFilename = null
15-
this.nodes = [] // list of children, filled by buildProcessTree()
16-
17-
this._coverageMap = null
1818

1919
for (var key in defaults) {
2020
this[key] = defaults[key]
2121
}
22+
23+
if (!this.uuid) {
24+
this.uuid = uuid()
25+
}
2226
}
2327

2428
Object.defineProperty(ProcessInfo.prototype, 'label', {
@@ -36,29 +40,19 @@ Object.defineProperty(ProcessInfo.prototype, 'label', {
3640
})
3741

3842
ProcessInfo.buildProcessTree = function (infos) {
39-
var treeRoot = new ProcessInfo({ _label: 'nyc' })
40-
var nodes = { }
41-
42-
infos = infos.sort(function (a, b) {
43-
return a.time - b.time
44-
})
45-
46-
infos.forEach(function (p) {
47-
nodes[p.root + ':' + p.pid] = p
48-
})
49-
50-
infos.forEach(function (p) {
51-
if (!p.ppid) {
52-
return
43+
const treeRoot = new ProcessInfo({ _label: 'nyc', nodes: [] })
44+
const index = infos.index
45+
for (const id in index.processes) {
46+
const node = infos[id]
47+
if (!node) {
48+
throw new Error(`Invalid entry in processinfo index: ${id}`)
5349
}
54-
55-
var parent = nodes[p.root + ':' + p.ppid]
56-
if (!parent) {
57-
parent = treeRoot
50+
const idx = index.processes[id]
51+
node.nodes = idx.children.map(id => infos[id]).sort((a, b) => a.time - b.time)
52+
if (!node.parent) {
53+
treeRoot.nodes.push(node)
5854
}
59-
60-
parent.nodes.push(p)
61-
})
55+
}
6256

6357
return treeRoot
6458
}

test/processinfo.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const { resolve } = require('path')
2+
const bin = resolve(__dirname, '../self-coverage/bin/nyc')
3+
const { spawn } = require('child_process')
4+
const t = require('tap')
5+
const rimraf = require('rimraf')
6+
const node = process.execPath
7+
const fixturesCLI = resolve(__dirname, './fixtures/cli')
8+
const tmp = 'processinfo-test'
9+
const fs = require('fs')
10+
const resolvedJS = resolve(fixturesCLI, 'selfspawn-fibonacci.js')
11+
12+
rimraf.sync(resolve(fixturesCLI, tmp))
13+
t.teardown(() => rimraf.sync(resolve(fixturesCLI, tmp)))
14+
15+
t.test('build some processinfo', t => {
16+
var args = [
17+
bin, '-t', tmp, '--build-process-tree',
18+
node, 'selfspawn-fibonacci.js', '5'
19+
]
20+
var proc = spawn(process.execPath, args, {
21+
cwd: fixturesCLI,
22+
env: {
23+
PATH: process.env.PATH,
24+
NYC_PROCESSINFO_EXTERNAL_ID: 'blorp'
25+
}
26+
})
27+
// don't actually care about the output for this test, just the data
28+
proc.stderr.resume()
29+
proc.stdout.resume()
30+
proc.on('close', (code, signal) => {
31+
t.equal(code, 0)
32+
t.equal(signal, null)
33+
t.end()
34+
})
35+
})
36+
37+
t.test('validate the created processinfo data', t => {
38+
const covs = fs.readdirSync(resolve(fixturesCLI, tmp))
39+
.filter(f => f !== 'processinfo')
40+
t.plan(covs.length * 2)
41+
42+
covs.forEach(f => {
43+
fs.readFile(resolve(fixturesCLI, tmp, f), 'utf8', (er, covjson) => {
44+
if (er) {
45+
throw er
46+
}
47+
const covdata = JSON.parse(covjson)
48+
t.same(Object.keys(covdata), [resolvedJS])
49+
// should have matching processinfo for each cov json
50+
const procInfoFile = resolve(fixturesCLI, tmp, 'processinfo', f)
51+
fs.readFile(procInfoFile, 'utf8', (er, procInfoJson) => {
52+
if (er) {
53+
throw er
54+
}
55+
const procInfoData = JSON.parse(procInfoJson)
56+
t.match(procInfoData, {
57+
pid: Number,
58+
ppid: Number,
59+
uuid: f.replace(/\.json$/, ''),
60+
argv: [
61+
node,
62+
resolvedJS,
63+
/[1-5]/
64+
],
65+
execArgv: [],
66+
cwd: fixturesCLI,
67+
time: Number,
68+
root: /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/,
69+
coverageFilename: resolve(fixturesCLI, tmp, f),
70+
files: [ resolvedJS ]
71+
})
72+
})
73+
})
74+
})
75+
})
76+
77+
t.test('check out the index', t => {
78+
const indexFile = resolve(fixturesCLI, tmp, 'processinfo', 'index.json')
79+
const indexJson = fs.readFileSync(indexFile, 'utf-8')
80+
const index = JSON.parse(indexJson)
81+
const u = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/
82+
t.match(index, {
83+
processes: {},
84+
files: {
85+
[resolvedJS]: [ u, u, u, u, u, u, u, u, u ]
86+
},
87+
externalIds: {
88+
blorp: {
89+
root: u,
90+
children: [ u, u, u, u, u, u, u, u ]
91+
}
92+
}
93+
})
94+
t.end()
95+
})

0 commit comments

Comments
 (0)