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

5 files changed

Lines changed: 214 additions & 34 deletions

File tree

‎bin/nyc.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ if ([
6565
),function(done){
6666
varmainChildExitCode=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‎

Lines changed: 5 additions & 0 deletions
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+
deleteprocess.env.NYC_PROCESSINFO_EXTERNAL_ID
17+
}
1318

1419
;(newNYC(config)).wrap()
1520

‎index.js‎

Lines changed: 92 additions & 10 deletions
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-
varid=this.generateUniqueID()
345+
varid=this.processInfo.uuid
345346
varcoverageFilename=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+
constdir=this.processInfoDirectory()
420+
constpidToUid=newMap()
421+
constinfoByUid=newMap()
422+
consteidToUid=newMap()
423+
constinfos=fs.readdirSync(dir).filter(f=>f!=='index.json').map(f=>{
424+
try{
425+
constinfo=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+
returninfo
434+
}catch(er){
435+
returnnull
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+
constparentInfo=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+
constfiles=infos.reduce((files,info)=>{
451+
info.files.forEach(f=>{
452+
files[f]=files[f]||[]
453+
files[f].push(info.uuid)
454+
})
455+
returnfiles
456+
},{})
457+
458+
// build the actual index!
459+
constindex=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+
thrownewError(`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+
returnindex
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(leti=0;i<children.length;i++){
481+
constnextGen=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
varprocessTree=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-
varfiles=fs.readdirSync(this.processInfoDirectory())
453-
454-
returnfiles.map(function(f){
527+
returnfs.readdirSync(this.processInfoDirectory()).map(f=>{
528+
letdata
455529
try{
456-
returnnewProcessInfo(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+
returnnull
462536
}
463-
})
537+
if(f!=='index.json'){
538+
data.nodes=[]
539+
data=newProcessInfo(data)
540+
}
541+
return{file: path.basename(f,'.json'),data: data}
542+
}).filter(Boolean).reduce((infos,info)=>{
543+
infos[info.file]=info.data
544+
returninfos
545+
},{})
464546
}
465547

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

‎lib/process.js‎

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
constarchy=require('archy')
22
constlibCoverage=require('istanbul-lib-coverage')
3+
constuuid=require('uuid/v4')
34

45
functionProcessInfo(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(varkeyindefaults){
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-
vartreeRoot=newProcessInfo({_label: 'nyc'})
40-
varnodes={}
41-
42-
infos=infos.sort(function(a,b){
43-
returna.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+
consttreeRoot=newProcessInfo({_label: 'nyc',nodes: []})
44+
constindex=infos.index
45+
for(constidinindex.processes){
46+
constnode=infos[id]
47+
if(!node){
48+
thrownewError(`Invalid entry in processinfo index: ${id}`)
5349
}
54-
55-
varparent=nodes[p.root+':'+p.ppid]
56-
if(!parent){
57-
parent=treeRoot
50+
constidx=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
returntreeRoot
6458
}

‎test/processinfo.js‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const{ resolve }=require('path')
2+
constbin=resolve(__dirname,'../self-coverage/bin/nyc')
3+
const{ spawn }=require('child_process')
4+
constt=require('tap')
5+
constrimraf=require('rimraf')
6+
constnode=process.execPath
7+
constfixturesCLI=resolve(__dirname,'./fixtures/cli')
8+
consttmp='processinfo-test'
9+
constfs=require('fs')
10+
constresolvedJS=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+
varargs=[
17+
bin,'-t',tmp,'--build-process-tree',
18+
node,'selfspawn-fibonacci.js','5'
19+
]
20+
varproc=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+
constcovs=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+
thrower
46+
}
47+
constcovdata=JSON.parse(covjson)
48+
t.same(Object.keys(covdata),[resolvedJS])
49+
// should have matching processinfo for each cov json
50+
constprocInfoFile=resolve(fixturesCLI,tmp,'processinfo',f)
51+
fs.readFile(procInfoFile,'utf8',(er,procInfoJson)=>{
52+
if(er){
53+
thrower
54+
}
55+
constprocInfoData=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+
constindexFile=resolve(fixturesCLI,tmp,'processinfo','index.json')
79+
constindexJson=fs.readFileSync(indexFile,'utf-8')
80+
constindex=JSON.parse(indexJson)
81+
constu=/^[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)