@@ -294,6 +294,85 @@ async def test_pidfd_ENOSYS(self, monkeypatch: pytest.MonkeyPatch) -> None:
294294 protocol , _transport = self .subprocess (['true' ])
295295 await protocol .eof_and_exited_with_code (0 )
296296
297+ @pytest .mark .asyncio
298+ async def test_pidfd_ENOSYS_nonzero_exit (self , monkeypatch : pytest .MonkeyPatch ) -> None :
299+ # test that non-zero exit codes are correctly reported via the threaded fallback path
300+ monkeypatch .setattr (os , 'pidfd_open' , unittest .mock .Mock (side_effect = OSError ), raising = False )
301+ protocol , _transport = self .subprocess (['false' ])
302+ await protocol .eof_and_exited_with_code (1 )
303+
304+ @pytest .mark .asyncio
305+ async def test_pidfd_ENOSYS_exit_code (self , monkeypatch : pytest .MonkeyPatch ) -> None :
306+ # test that specific exit codes are correctly reported via the threaded fallback path
307+ monkeypatch .setattr (os , 'pidfd_open' , unittest .mock .Mock (side_effect = OSError ), raising = False )
308+ protocol , _transport = self .subprocess (['sh' , '-c' , 'exit 42' ])
309+ await protocol .eof_and_exited_with_code (42 )
310+
311+ @pytest .mark .asyncio
312+ async def test_pidfd_ENOSYS_signal (self , monkeypatch : pytest .MonkeyPatch ) -> None :
313+ # test that signal termination is correctly reported via the threaded fallback path
314+ monkeypatch .setattr (os , 'pidfd_open' , unittest .mock .Mock (side_effect = OSError ), raising = False )
315+ protocol , transport = self .subprocess (['cat' ])
316+ transport .send_signal (signal .SIGTERM )
317+ await protocol .eof_and_exited_with_code (- signal .SIGTERM )
318+
319+ @pytest .mark .asyncio
320+ async def test_pidfd_ENOSYS_kill (self , monkeypatch : pytest .MonkeyPatch ) -> None :
321+ # test that SIGKILL is correctly reported via the threaded fallback path
322+ monkeypatch .setattr (os , 'pidfd_open' , unittest .mock .Mock (side_effect = OSError ), raising = False )
323+ protocol , transport = self .subprocess (['cat' ])
324+ transport .kill ()
325+ await protocol .eof_and_exited_with_code (- signal .SIGKILL )
326+
327+ @pytest .mark .asyncio
328+ async def test_pidfd_ENOSYS_concurrent (self , monkeypatch : pytest .MonkeyPatch ) -> None :
329+ # test multiple concurrent subprocesses with different exit scenarios
330+ # using the threaded fallback path, to ensure exit statuses don't get mixed up
331+ monkeypatch .setattr (os , 'pidfd_open' , unittest .mock .Mock (side_effect = OSError ), raising = False )
332+
333+ # start processes that block on stdin - we control when they exit
334+ proto_0 , transport_0 = self .subprocess (['sh' , '-c' , 'read a; exit 0' ])
335+ proto_1 , transport_1 = self .subprocess (['sh' , '-c' , 'read a; exit 1' ])
336+ proto_42 , transport_42 = self .subprocess (['sh' , '-c' , 'read a; exit 42' ])
337+ proto_term , transport_term = self .subprocess (['sh' , '-c' , 'read a; exit 99' ])
338+ proto_kill , transport_kill = self .subprocess (['sh' , '-c' , 'read a; exit 99' ])
339+
340+ # create tasks for each process
341+ task_0 = asyncio .create_task (proto_0 .eof_and_exited_with_code (0 ))
342+ task_1 = asyncio .create_task (proto_1 .eof_and_exited_with_code (1 ))
343+ task_42 = asyncio .create_task (proto_42 .eof_and_exited_with_code (42 ))
344+ task_term = asyncio .create_task (proto_term .eof_and_exited_with_code (- signal .SIGTERM ))
345+ task_kill = asyncio .create_task (proto_kill .eof_and_exited_with_code (- signal .SIGKILL ))
346+ pending = {task_0 , task_1 , task_42 , task_term , task_kill }
347+
348+ # exit them one by one in a specific order and verify each time
349+ transport_kill .kill ()
350+ done , pending = await asyncio .wait (pending , return_when = asyncio .FIRST_COMPLETED )
351+ assert done == {task_kill }
352+ task_kill .result ()
353+
354+ transport_term .send_signal (signal .SIGTERM )
355+ done , pending = await asyncio .wait (pending , return_when = asyncio .FIRST_COMPLETED )
356+ assert done == {task_term }
357+ task_term .result ()
358+
359+ transport_42 .write_eof ()
360+ done , pending = await asyncio .wait (pending , return_when = asyncio .FIRST_COMPLETED )
361+ assert done == {task_42 }
362+ task_42 .result ()
363+
364+ transport_1 .write_eof ()
365+ done , pending = await asyncio .wait (pending , return_when = asyncio .FIRST_COMPLETED )
366+ assert done == {task_1 }
367+ task_1 .result ()
368+
369+ transport_0 .write_eof ()
370+ done , pending = await asyncio .wait (pending , return_when = asyncio .FIRST_COMPLETED )
371+ assert done == {task_0 }
372+ task_0 .result ()
373+
374+ assert not pending
375+
297376 @pytest .mark .asyncio
298377 async def test_true_pty (self ) -> None :
299378 loop = asyncio .get_running_loop ()
0 commit comments