11import { arch } from 'node:os' ;
22import process from 'node:process' ;
3+ import { finished } from 'node:stream/promises' ;
34import test from 'ava' ;
45import { execa , execaSync , $ } from '../../index.js' ;
56import { setFixtureDirectory } from '../helpers/fixtures-directory.js' ;
7+ import { foobarString , foobarUint8Array } from '../helpers/input.js' ;
68import { fullStdio } from '../helpers/stdio.js' ;
79import {
810 earlyErrorOptions ,
@@ -17,6 +19,14 @@ setFixtureDirectory();
1719const isWindows = process . platform === 'win32' ;
1820const ENOENT_REGEXP = isWindows ? / f a i l e d w i t h e x i t c o d e 1 / : / s p a w n .* E N O E N T / ;
1921
22+ // A nonexistent command fails asynchronously, so it does not go through the early error path, but the subprocess still has no process to signal
23+ test ( 'kill() does not signal the current process when the command does not exist' , async t => {
24+ const subprocess = execa ( 'nonexistent-command-for-execa' ) ;
25+ t . is ( subprocess . pid , undefined ) ;
26+ t . is ( subprocess . kill ( ) , false ) ;
27+ t . is ( ( await t . throwsAsync ( subprocess ) ) . code , 'ENOENT' ) ;
28+ } ) ;
29+
2030test ( 'execaSync() throws error if ENOENT' , t => {
2131 t . throws ( ( ) => {
2232 execaSync ( 'foo' ) ;
@@ -35,6 +45,26 @@ const testEarlyErrorShape = async (t, reject) => {
3545test ( 'child_process.spawn() early errors have correct shape' , testEarlyErrorShape , true ) ;
3646test ( 'child_process.spawn() early errors have correct shape - reject false' , testEarlyErrorShape , false ) ;
3747
48+ // The subprocess never spawned, so there is no process to signal.
49+ // `ChildProcess.prototype.kill()` would signal the current process group instead, since its process id is `0`.
50+ // The handle is replaced by a stub, so that a regression is caught by an assertion instead of killing this test process.
51+ test ( 'kill() does not signal the current process on early errors' , async t => {
52+ const subprocess = getEarlyErrorSubprocess ( { reject : false } ) ;
53+ let isSignaled = false ;
54+ subprocess . nodeChildProcess . _handle = {
55+ kill ( ) {
56+ isSignaled = true ;
57+ return 0 ;
58+ } ,
59+ } ;
60+
61+ t . is ( subprocess . kill ( ) , false ) ;
62+ t . false ( isSignaled ) ;
63+ const { failed, isCanceled} = await subprocess ;
64+ t . true ( failed ) ;
65+ t . false ( isCanceled ) ;
66+ } ) ;
67+
3868test ( 'child_process.spawn() early errors are propagated' , async t => {
3969 await t . throwsAsync ( getEarlyErrorSubprocess ( ) , expectedEarlyError ) ;
4070} ) ;
@@ -113,6 +143,54 @@ test('child_process.spawn() early errors can use .readableStream()', testEarlyEr
113143test ( 'child_process.spawn() early errors can use .writableStream()' , testEarlyErrorWebConvertor , 'writableStream' ) ;
114144test ( 'child_process.spawn() early errors can use .transformStream()' , testEarlyErrorWebConvertor , 'transformStream' ) ;
115145
146+ // The subprocess never started, so those streams have no contents.
147+ // They must end right away, like `subprocess.stdout` and `.iterable()` do, instead of hanging forever.
148+ const testEarlyErrorReadableEnd = async ( t , streamMethod ) => {
149+ const subprocess = getEarlyErrorSubprocess ( ) ;
150+ const chunks = await Array . fromAsync ( subprocess [ streamMethod ] ( ) ) ;
151+
152+ t . deepEqual ( chunks , [ ] ) ;
153+ await t . throwsAsync ( subprocess ) ;
154+ } ;
155+
156+ test ( 'child_process.spawn() early errors end .readable()' , testEarlyErrorReadableEnd , 'readable' ) ;
157+ test ( 'child_process.spawn() early errors end .duplex()' , testEarlyErrorReadableEnd , 'duplex' ) ;
158+ test ( 'child_process.spawn() early errors end .readableStream()' , testEarlyErrorReadableEnd , 'readableStream' ) ;
159+
160+ const testEarlyErrorWritableEnd = async ( t , streamMethod ) => {
161+ const subprocess = getEarlyErrorSubprocess ( ) ;
162+ const stream = subprocess [ streamMethod ] ( ) ;
163+ stream . end ( foobarString ) ;
164+ await finished ( stream , { readable : false } ) ;
165+
166+ t . false ( stream . writable ) ;
167+ await t . throwsAsync ( subprocess ) ;
168+ } ;
169+
170+ test ( 'child_process.spawn() early errors end .writable()' , testEarlyErrorWritableEnd , 'writable' ) ;
171+ test ( 'child_process.spawn() early errors end .duplex() writable side' , testEarlyErrorWritableEnd , 'duplex' ) ;
172+
173+ test ( 'child_process.spawn() early errors end .writableStream()' , async t => {
174+ const subprocess = getEarlyErrorSubprocess ( ) ;
175+ const writer = subprocess . writableStream ( ) . getWriter ( ) ;
176+ await writer . write ( foobarUint8Array ) ;
177+ await writer . close ( ) ;
178+
179+ await t . throwsAsync ( subprocess ) ;
180+ } ) ;
181+
182+ test ( 'child_process.spawn() early errors end .transformStream()' , async t => {
183+ const subprocess = getEarlyErrorSubprocess ( ) ;
184+ const { readable, writable} = subprocess . transformStream ( ) ;
185+ const writer = writable . getWriter ( ) ;
186+ await writer . write ( foobarUint8Array ) ;
187+ await writer . close ( ) ;
188+ const chunks = await Array . fromAsync ( readable ) ;
189+
190+ t . deepEqual ( chunks , [ ] ) ;
191+ await t . throwsAsync ( subprocess ) ;
192+ } ) ;
193+
116194const testEarlyErrorIpc = async ( t , runIpcMethod ) => {
117195 const subprocess = getEarlyErrorSubprocess ( { ipc : true } ) ;
118196 t . throws ( ( ) => {
0 commit comments