Skip to content

Commit 61ad1ab

Browse files
vstinnersorcio
andcommitted
pythongh-108973: Fix asyncio test_subprocess_consistent_callbacks()
Subprocess events can be delivered in any order: tolerate that in the test. Revert commit 282edd7. _child_watcher_callback() calls immediately _process_exited(): don't add an additional delay with call_soon(). The reverted change didn't make _process_exited() more determistic: it can still be called before pipe_connection_lost() for example. Co-authored-by: Davide Rizzo <[email protected]>
1 parent 1ece084 commit 61ad1ab

File tree

4 files changed

+47
-14
lines changed

4 files changed

+47
-14
lines changed

Doc/library/asyncio-llapi-index.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,9 @@ Protocol classes can implement the following **callback methods**:
496496

497497
* - ``callback`` :meth:`process_exited()
498498
<SubprocessProtocol.process_exited>`
499-
- Called when the child process has exited.
499+
- Called when the child process has exited. This method is not always
500+
the last one to be called: protocol methods are not called in a
501+
determistic order.
500502

501503

502504
Event Loop Policies

Doc/library/asyncio-protocol.rst

+19-1
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,11 @@ Subprocess Protocol instances should be constructed by protocol
688688
factories passed to the :meth:`loop.subprocess_exec` and
689689
:meth:`loop.subprocess_shell` methods.
690690

691+
The protocol methods are not called in a determistic order. The order depends
692+
on the event loop implementation and the operating system. The protocol should
693+
implement its own logic depending on the use case to decide when it's done with
694+
the subprocess.
695+
691696
.. method:: SubprocessProtocol.pipe_data_received(fd, data)
692697

693698
Called when the child process writes data into its stdout or stderr
@@ -1003,12 +1008,25 @@ The subprocess is created by the :meth:`loop.subprocess_exec` method::
10031008
def __init__(self, exit_future):
10041009
self.exit_future = exit_future
10051010
self.output = bytearray()
1011+
self.pipe_closed = False
1012+
self.exited = False
1013+
1014+
def pipe_connection_lost(self, fd, exc):
1015+
self.pipe_closed = True
1016+
self.exit_maybe()
10061017

10071018
def pipe_data_received(self, fd, data):
10081019
self.output.extend(data)
10091020

10101021
def process_exited(self):
1011-
self.exit_future.set_result(True)
1022+
self.exited = True
1023+
# process_exited() can be called before pipe_connection_lost():
1024+
# wait until both methods are called.
1025+
self.exit_maybe()
1026+
1027+
def exit_maybe(self):
1028+
if self.pipe_closed and self.exited:
1029+
self.exit_future.set_result(True)
10121030

10131031
async def get_date():
10141032
# Get a reference to the event loop as we plan to use

Lib/asyncio/unix_events.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,7 @@ async def _make_subprocess_transport(self, protocol, args, shell,
226226
return transp
227227

228228
def _child_watcher_callback(self, pid, returncode, transp):
229-
# Skip one iteration for callbacks to be executed
230-
self.call_soon_threadsafe(self.call_soon, transp._process_exited, returncode)
229+
self.call_soon_threadsafe(transp._process_exited, returncode)
231230

232231
async def create_unix_connection(
233232
self, protocol_factory, path=None, *,

Lib/test/test_asyncio/test_subprocess.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -753,21 +753,39 @@ async def main() -> None:
753753

754754
self.loop.run_until_complete(main())
755755

756-
def test_subprocess_consistent_callbacks(self):
756+
def test_subprocess_protocol_events(self):
757+
# gh-108973: Test that all subprocess protocol methods are called.
758+
# The protocol methods are not called in a determistic order.
759+
# The order depends on the event loop and the operating system.
757760
events = []
761+
expected = [
762+
('pipe_data_received', 1, b'stdout'),
763+
('pipe_data_received', 2, b'stderr'),
764+
('pipe_connection_lost', 1),
765+
('pipe_connection_lost', 2),
766+
'process_exited',
767+
]
768+
758769
class MyProtocol(asyncio.SubprocessProtocol):
759770
def __init__(self, exit_future: asyncio.Future) -> None:
760771
self.exit_future = exit_future
761772

762773
def pipe_data_received(self, fd, data) -> None:
763774
events.append(('pipe_data_received', fd, data))
775+
self.exit_maybe()
764776

765777
def pipe_connection_lost(self, fd, exc) -> None:
766-
events.append('pipe_connection_lost')
778+
events.append(('pipe_connection_lost', fd))
779+
self.exit_maybe()
767780

768781
def process_exited(self) -> None:
769782
events.append('process_exited')
770-
self.exit_future.set_result(True)
783+
self.exit_maybe()
784+
785+
def exit_maybe(self):
786+
# Only exit when we got all expected events
787+
if len(events) >= len(expected):
788+
self.exit_future.set_result(True)
771789

772790
async def main() -> None:
773791
loop = asyncio.get_running_loop()
@@ -777,13 +795,9 @@ async def main() -> None:
777795
sys.executable, '-c', code, stdin=None)
778796
await exit_future
779797
transport.close()
780-
self.assertEqual(events, [
781-
('pipe_data_received', 1, b'stdout'),
782-
('pipe_data_received', 2, b'stderr'),
783-
'pipe_connection_lost',
784-
'pipe_connection_lost',
785-
'process_exited',
786-
])
798+
799+
# Protocol methods can be called in any order
800+
self.assertSetEqual(set(events), set(expected))
787801

788802
self.loop.run_until_complete(main())
789803

0 commit comments

Comments
 (0)