1- import { x } from '../main.js' ;
21import { describe , test , expect } from 'vitest' ;
32import os from 'node:os' ;
43import fs from 'node:fs' ;
@@ -7,124 +6,127 @@ import {spawnSync} from 'node:child_process';
76
87const isWindows = os . platform ( ) === 'win32' ;
98
9+ function runInSubprocess (
10+ childScript : string ,
11+ runnerBody : string
12+ ) : { status : number | null ; signal : string | null ; stdout : string } {
13+ const dir = path . dirname ( childScript ) ;
14+ const runnerScript = path . join ( dir , 'runner.mjs' ) ;
15+ const distPath = JSON . stringify (
16+ path . join ( process . cwd ( ) , 'dist' , 'main.mjs' )
17+ ) ;
18+ const childPath = JSON . stringify ( childScript ) ;
19+
20+ fs . writeFileSync (
21+ runnerScript ,
22+ `import { x } from ${ distPath } \n${ runnerBody . replace ( / C H I L D _ S C R I P T / g, childPath ) } `
23+ ) ;
24+
25+ try {
26+ const proc = spawnSync ( 'node' , [ runnerScript ] , {
27+ timeout : 10000 ,
28+ encoding : 'utf8' ,
29+ killSignal : 'SIGKILL'
30+ } ) ;
31+
32+ return {
33+ status : proc . status ,
34+ signal : proc . signal ,
35+ stdout : proc . stdout ?? ''
36+ } ;
37+ } finally {
38+ try {
39+ spawnSync ( 'pkill' , [ '-f' , dir ] ) ;
40+ } catch { }
41+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
42+ }
43+ }
44+
1045describe . skipIf ( isWindows ) ( 'exec (grandchild pipe inheritance)' , ( ) => {
1146 test ( 'await completes when grandchild holds piped stdout open' , async ( ) => {
12- const dir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'tinyexec-grandchild-' ) ) ;
47+ const dir = fs . mkdtempSync (
48+ path . join ( os . tmpdir ( ) , 'tinyexec-grandchild-' )
49+ ) ;
1350 const childScript = path . join ( dir , 'child.mjs' ) ;
1451
1552 fs . writeFileSync (
1653 childScript ,
1754 `import { spawn } from 'node:child_process'
55+ console.log('output')
1856spawn('node', ['-e', 'setTimeout(() => void 0, 30000)'], {
1957 stdio: ['ignore', 1, 'ignore'],
2058})
21- process.stdout.write('output\\n')
2259process.exit(0)
2360`
2461 ) ;
2562
26- // Run in a subprocess so the orphaned grandchild doesn't block vitest.
27- // The runner uses the built dist/main.mjs directly.
28- const runnerScript = path . join ( dir , 'runner.mjs' ) ;
29- const distPath = path . join ( process . cwd ( ) , 'dist' , 'main.mjs' ) ;
30- fs . writeFileSync (
31- runnerScript ,
32- `import { x } from '${ distPath } '
33-
63+ const result = runInSubprocess (
64+ childScript ,
65+ `
3466const result = await Promise.race([
35- x('node', [' ${ childScript } ' ]).then(() => 'completed'),
67+ x('node', [CHILD_SCRIPT ]).then(() => 'completed'),
3668 new Promise((resolve) => setTimeout(() => resolve('hung'), 5000)),
3769])
38-
39- try { process.kill(-process.pid) } catch {}
4070process.stdout.write(result)
4171process.exit(result === 'completed' ? 0 : 1)
4272`
4373 ) ;
4474
45- try {
46- const proc = spawnSync ( 'node' , [ runnerScript ] , {
47- timeout : 10000 ,
48- encoding : 'utf8' ,
49- killSignal : 'SIGKILL' ,
50- } ) ;
51-
52- if ( proc . signal === 'SIGKILL' ) {
53- expect . unreachable (
54- 'exec hung for 10s (grandchild held pipe open)'
55- ) ;
56- }
57-
58- expect ( proc . status ) . toBe ( 0 ) ;
59- expect ( proc . stdout . trim ( ) ) . toBe ( 'completed' ) ;
60- } finally {
61- try {
62- spawnSync ( 'pkill' , [ '-f' , dir ] ) ;
63- } catch { }
64- fs . rmSync ( dir , { recursive : true , force : true } ) ;
75+ if ( result . signal === 'SIGKILL' ) {
76+ expect . unreachable (
77+ 'exec hung for 10s (grandchild held pipe open)'
78+ ) ;
6579 }
80+
81+ expect ( result . status ) . toBe ( 0 ) ;
82+ expect ( result . stdout . trim ( ) ) . toBe ( 'completed' ) ;
6683 } ) ;
6784
6885 test ( 'async iterator completes when grandchild holds piped stdout open' , async ( ) => {
69- const dir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'tinyexec-grandchild-' ) ) ;
86+ const dir = fs . mkdtempSync (
87+ path . join ( os . tmpdir ( ) , 'tinyexec-grandchild-' )
88+ ) ;
7089 const childScript = path . join ( dir , 'child.mjs' ) ;
7190
7291 fs . writeFileSync (
7392 childScript ,
7493 `import { spawn } from 'node:child_process'
94+ console.log('line1')
95+ console.log('line2')
7596spawn('node', ['-e', 'setTimeout(() => void 0, 30000)'], {
7697 stdio: ['ignore', 1, 'ignore'],
7798})
78- process.stdout.write('line1\\nline2\\n')
7999process.exit(0)
80100`
81101 ) ;
82102
83- const runnerScript = path . join ( dir , 'runner.mjs' ) ;
84- const distPath = path . join ( process . cwd ( ) , 'dist' , 'main.mjs' ) ;
85- fs . writeFileSync (
86- runnerScript ,
87- `import { x } from '${ distPath } '
88-
103+ const result = runInSubprocess (
104+ childScript ,
105+ `
89106const lines = []
90107const result = await Promise.race([
91108 (async () => {
92- for await (const line of x('node', [' ${ childScript } ' ])) {
109+ for await (const line of x('node', [CHILD_SCRIPT ])) {
93110 lines.push(line)
94111 }
95112 return 'completed'
96113 })(),
97114 new Promise((resolve) => setTimeout(() => resolve('hung'), 5000)),
98115])
99-
100- try { process.kill(-process.pid) } catch {}
101116process.stdout.write(JSON.stringify({ result, lines }))
102117process.exit(result === 'completed' ? 0 : 1)
103118`
104119 ) ;
105120
106- try {
107- const proc = spawnSync ( 'node' , [ runnerScript ] , {
108- timeout : 10000 ,
109- encoding : 'utf8' ,
110- killSignal : 'SIGKILL' ,
111- } ) ;
112-
113- if ( proc . signal === 'SIGKILL' ) {
114- expect . unreachable (
115- 'async iterator hung for 10s (grandchild held pipe open)'
116- ) ;
117- }
118-
119- expect ( proc . status ) . toBe ( 0 ) ;
120- const parsed = JSON . parse ( proc . stdout . trim ( ) ) ;
121- expect ( parsed . result ) . toBe ( 'completed' ) ;
122- expect ( parsed . lines ) . toEqual ( [ 'line1' , 'line2' ] ) ;
123- } finally {
124- try {
125- spawnSync ( 'pkill' , [ '-f' , dir ] ) ;
126- } catch { }
127- fs . rmSync ( dir , { recursive : true , force : true } ) ;
121+ if ( result . signal === 'SIGKILL' ) {
122+ expect . unreachable (
123+ 'async iterator hung for 10s (grandchild held pipe open)'
124+ ) ;
128125 }
126+
127+ expect ( result . status ) . toBe ( 0 ) ;
128+ const parsed = JSON . parse ( result . stdout . trim ( ) ) ;
129+ expect ( parsed . result ) . toBe ( 'completed' ) ;
130+ expect ( parsed . lines ) . toEqual ( [ 'line1' , 'line2' ] ) ;
129131 } ) ;
130132} ) ;
0 commit comments