commandProcess.test.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. // @vitest-environment node
  2. import {afterEach,expect,it,vi} from 'vitest'
  3. import {execFile} from 'node:child_process'
  4. import {randomUUID} from 'node:crypto'
  5. import {mkdtemp,rm,access,writeFile,mkdir} from 'node:fs/promises'
  6. import {join} from 'node:path'
  7. import {promisify} from 'node:util'
  8. import {GUEST_COMMAND} from '../../src/runner/computers/commandProcess'
  9. const homes:string[]=[]
  10. afterEach(async()=>{await Promise.all(homes.splice(0).map(path=>rm(path,{recursive:true,force:true})))})
  11. const python=promisify(execFile)
  12. const launch=(root:string,key:string,script:string,timeout=3)=>python('python3',['-c',GUEST_COMMAND,'run',root,key,'root',String(timeout),'python3','-c',script],{timeout:10000})
  13. const cancel=(root:string,key:string)=>python('python3',['-c',GUEST_COMMAND,'cancel',root,key],{timeout:10000})
  14. const exists=(path:string)=>access(path).then(()=>true,()=>false)
  15. async function fixture(){const root=await mkdtemp('/tmp/yaoyao-cmd-');homes.push(root);return root}
  16. it.skipIf(process.platform==='win32')('cancels a guest process group while a peer command finishes and preserves command IO',async()=>{
  17. const root=await fixture(),key=randomUUID(),ready=join(root,'ready'),late=join(root,'late')
  18. const child=`import pathlib,time;time.sleep(.6);pathlib.Path(${JSON.stringify(late)}).touch()`
  19. const execution=launch(root,key,`import subprocess,pathlib,time;subprocess.Popen(['python3','-c',${JSON.stringify(child)}]);pathlib.Path(${JSON.stringify(ready)}).touch();time.sleep(30)`).catch(error=>error)
  20. const peer=launch(root,randomUUID(),"import time,sys;time.sleep(.3);print('peer');print('stderr',file=sys.stderr)")
  21. try{
  22. await vi.waitFor(async()=>expect(await exists(ready)).toBe(true))
  23. await expect(launch(root,key,"print('duplicate')")).rejects.toMatchObject({code:130})
  24. expect(JSON.parse((await cancel(root,key)).stdout)).toEqual({stopped:true})
  25. expect(await execution).toMatchObject({code:130})
  26. expect(await peer).toMatchObject({stdout:'peer\n',stderr:'stderr\n'})
  27. await new Promise(resolve=>setTimeout(resolve,600))
  28. expect(await exists(late)).toBe(false)
  29. }finally{await cancel(root,key).catch(()=>{});await Promise.allSettled([execution,peer])}
  30. },10000)
  31. it.skipIf(process.platform==='win32')('prevents launch when cancellation arrives first and acknowledges completed commands',async()=>{
  32. const root=await fixture(),key=randomUUID(),marker=join(root,'never')
  33. expect(JSON.parse((await cancel(root,key)).stdout)).toEqual({stopped:true})
  34. await expect(launch(root,key,`import pathlib;pathlib.Path(${JSON.stringify(marker)}).touch()`)).rejects.toMatchObject({code:130})
  35. expect(await exists(marker)).toBe(false)
  36. const completed=randomUUID()
  37. expect((await launch(root,completed,"print('done')")).stdout).toBe('done\n')
  38. expect(JSON.parse((await cancel(root,completed)).stdout)).toEqual({stopped:true})
  39. })
  40. it.skipIf(process.platform==='win32')('times out a process group and refuses to confirm a crashed supervisor',async()=>{
  41. const root=await fixture(),key=randomUUID(),late=join(root,'late')
  42. const child=`import pathlib,time;time.sleep(.6);pathlib.Path(${JSON.stringify(late)}).touch()`
  43. await expect(launch(root,key,`import subprocess,time;subprocess.Popen(['python3','-c',${JSON.stringify(child)}]);time.sleep(30)`,.15)).rejects.toMatchObject({code:124})
  44. await new Promise(resolve=>setTimeout(resolve,600))
  45. expect(await exists(late)).toBe(false)
  46. const crashed=randomUUID();await mkdir(join(root,crashed));await writeFile(join(root,crashed,'started'),'')
  47. await expect(cancel(root,crashed)).rejects.toMatchObject({code:1})
  48. expect(await exists(join(root,crashed,'done'))).toBe(false)
  49. })