59 lines
2.2 KiB
JavaScript
59 lines
2.2 KiB
JavaScript
import {spawn} from 'node:child_process';
|
|
|
|
const pools=new Set();let terminating;
|
|
const terminate=signal=>{
|
|
if(terminating)return;
|
|
// Every close starts synchronously by killing only its captured child groups;
|
|
// wait for all close events before forwarding the conventional exit status.
|
|
terminating=Promise.allSettled([...pools].map(close=>close())).then(results=>{
|
|
process.exit(results.some(result=>result.status==='rejected')?1:(signal==='SIGTERM'?143:130));
|
|
});
|
|
};
|
|
const term=()=>terminate('SIGTERM'),interrupt=()=>terminate('SIGINT');
|
|
function register(close){
|
|
if(pools.size===0){process.on('SIGTERM',term);process.on('SIGINT',interrupt)}
|
|
pools.add(close);
|
|
}
|
|
function unregister(close){
|
|
pools.delete(close);
|
|
if(pools.size===0){process.removeListener('SIGTERM',term);process.removeListener('SIGINT',interrupt)}
|
|
}
|
|
|
|
// Fixture children have their own groups so abort/teardown can stop descendants
|
|
// without signalling the test runner or another fixture's work.
|
|
export function createOwnedRunner(t,env){
|
|
const children=new Map();let closed=false;
|
|
const stop=child=>{
|
|
if(!child.pid)return;
|
|
try{process.kill(-child.pid,'SIGKILL')}catch(e){if(e.code!=='ESRCH')throw e}
|
|
};
|
|
const close=async()=>{
|
|
closed=true;
|
|
for(const child of children.keys())stop(child);
|
|
await Promise.all(children.values());
|
|
unregister(close);
|
|
};
|
|
register(close);
|
|
t.after(close);
|
|
const run=(command,args=[],options={})=>{
|
|
if(closed)throw Error('fixture runner closed');
|
|
const child=spawn(command,args,{env:env(),...options,detached:true,stdio:['ignore','pipe','pipe']});
|
|
let stdout='',stderr='',error;
|
|
child.stdout.setEncoding('utf8');child.stderr.setEncoding('utf8');
|
|
child.stdout.on('data',chunk=>{stdout+=chunk});child.stderr.on('data',chunk=>{stderr+=chunk});
|
|
const abort=()=>stop(child);
|
|
const done=new Promise(resolve=>{
|
|
child.on('error',e=>{error=e});
|
|
child.once('close',(status,signal)=>{
|
|
children.delete(child);t.signal.removeEventListener('abort',abort);
|
|
resolve({status,signal,stdout,stderr,...(error?{error}: {})});
|
|
});
|
|
});
|
|
children.set(child,done);
|
|
t.signal.addEventListener('abort',abort,{once:true});
|
|
if(t.signal.aborted)abort();
|
|
return done;
|
|
};
|
|
return {run,close};
|
|
}
|