Repository: byllyfish/shellous (language: python)

Issue:
Case where process exits before we can write to stdin...
```
tests/test_shellous.py::test_broken_pipe 
-------------------------------- live log setup --------------------------------
1630453746.249 DEBUG Using selector: KqueueSelector
1630453746.249 DEBUG Using selector: KqueueSelector
1630453746.250 DEBUG Close <_UnixSelectorEventLoop running=False closed=False debug=True>
-------------------------------- live log call ---------------------------------
1630453746.255 INFO Runner.__aenter__ stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=None> ('macOS-10.15.7 CPython 3.10.0rc1 asyncio.unix_events._UnixSelectorEventLoop ThreadedChildWatcher')
1630453746.255 INFO Runner._start stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=None>
1630453746.256 DEBUG execute program '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.10/bin/python' stdin=<pipe> stdout=<pipe> stderr=<Redirect.DEVNULL: -3>
1630453746.262 DEBUG process '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.10/bin/python' created: pid 8727
1630453746.294 DEBUG process 8727 exited with returncode 0
1630453746.296 INFO <_UnixSubprocessTransport pid=8727 running> exited with return code 0
1630453746.296 DEBUG Write pipe 12 connected: (<_UnixWritePipeTransport fd=12 idle bufsize=0>, <WriteSubprocessPipeProto fd=0 pipe=<_UnixWritePipeTransport fd=12 idle bufsize=0>>)
1630453746.297 INFO <_UnixWritePipeTransport fd=12 idle bufsize=0> was closed by peer
1630453746.298 DEBUG Read pipe 14 connected: (<_UnixReadPipeTransport fd=14 polling>, <ReadSubprocessPipeProto fd=1 pipe=<_UnixReadPipeTransport fd=14 polling>>)
1630453746.299 INFO <_UnixReadPipeTransport fd=14 polling> was closed by peer
1630453746.300 INFO execute program '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.10/bin/python': <_UnixSubprocessTransport pid=8727 returncode=0 stdin=<_UnixWritePipeTransport closed fd=12 closed> stdout=<_UnixReadPipeTransport closing fd=14 idle>>
1630453746.301 INFO Runner._start stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.301 INFO Runner.__aenter__ stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.302 INFO write_stream stepin
1630453746.303 INFO write_stream stepout ex=None
1630453746.303 INFO Runner.__aexit__ stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> exc_value=None
1630453746.303 INFO Runner._finish stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0>
1630453746.303 INFO Runner._wait stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0>
1630453746.304 INFO Runner._wait stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.305 INFO Runner._close stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0>
1630453746.307 INFO Runner._close stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.307 INFO Runner._finish stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.307 INFO Runner.__aexit__ stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None exc_value=None
FAILED
```

We might want to proactively raise an exception similar to BrokenPipeError.
Case where process exits before we can write to stdin...
```
tests/test_shellous.py::test_broken_pipe 
-------------------------------- live log setup --------------------------------
1630453746.249 DEBUG Using selector: KqueueSelector
1630453746.249 DEBUG Using selector: KqueueSelector
1630453746.250 DEBUG Close <_UnixSelectorEventLoop running=False closed=False debug=True>
-------------------------------- live log call ---------------------------------
1630453746.255 INFO Runner.__aenter__ stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=None> ('macOS-10.15.7 CPython 3.10.0rc1 asyncio.unix_events._UnixSelectorEventLoop ThreadedChildWatcher')
1630453746.255 INFO Runner._start stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=None>
1630453746.256 DEBUG execute program '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.10/bin/python' stdin=<pipe> stdout=<pipe> stderr=<Redirect.DEVNULL: -3>
1630453746.262 DEBUG process '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.10/bin/python' created: pid 8727
1630453746.294 DEBUG process 8727 exited with returncode 0
1630453746.296 INFO <_UnixSubprocessTransport pid=8727 running> exited with return code 0
1630453746.296 DEBUG Write pipe 12 connected: (<_UnixWritePipeTransport fd=12 idle bufsize=0>, <WriteSubprocessPipeProto fd=0 pipe=<_UnixWritePipeTransport fd=12 idle bufsize=0>>)
1630453746.297 INFO <_UnixWritePipeTransport fd=12 idle bufsize=0> was closed by peer
1630453746.298 DEBUG Read pipe 14 connected: (<_UnixReadPipeTransport fd=14 polling>, <ReadSubprocessPipeProto fd=1 pipe=<_UnixReadPipeTransport fd=14 polling>>)
1630453746.299 INFO <_UnixReadPipeTransport fd=14 polling> was closed by peer
1630453746.300 INFO execute program '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.10/bin/python': <_UnixSubprocessTransport pid=8727 returncode=0 stdin=<_UnixWritePipeTransport closed fd=12 closed> stdout=<_UnixReadPipeTransport closing fd=14 idle>>
1630453746.301 INFO Runner._start stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.301 INFO Runner.__aenter__ stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.302 INFO write_stream stepin
1630453746.303 INFO write_stream stepout ex=None
1630453746.303 INFO Runner.__aexit__ stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> exc_value=None
1630453746.303 INFO Runner._finish stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0>
1630453746.303 INFO Runner._wait stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0>
1630453746.304 INFO Runner._wait stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.305 INFO Runner._close stepin <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0>
1630453746.307 INFO Runner._close stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.307 INFO Runner._finish stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None
1630453746.307 INFO Runner.__aexit__ stepout <Runner '...lous-fzoxJzh3-py3.10/bin/python' pid=8727 exit_code=0> ex=None exc_value=None
FAILED
```

We might want to proactively raise an exception similar to BrokenPipeError.


Discussion / hints:
Latest version of the log.  May need to synthesize a BrokenPipeError in this case...

```
------------------------------ Captured log call -------------------------------
INFO     shellous:log.py:47 Runner.__aenter__ stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=None> (macOS-10.15.7 CPython 3.9.6 asyncio.unix_events._UnixSelectorEventLoop MainThread ThreadedChildWatcher)
INFO     shellous:log.py:55 Runner._start stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=None>
INFO     asyncio:base_subprocess.py:211 <_UnixSubprocessTransport pid=5288 running stdin=<_UnixWritePipeTransport fd=10 idle bufsize=0> stdout=<_UnixReadPipeTransport fd=11 polling>> exited with return code 0
INFO     asyncio:unix_events.py:642 <_UnixWritePipeTransport fd=10 idle bufsize=0> was closed by peer
INFO     asyncio:unix_events.py:503 <_UnixReadPipeTransport fd=11 polling> was closed by peer
INFO     asyncio:base_events.py:1665 execute program '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.9/bin/python': <_UnixSubprocessTransport pid=5288 returncode=0 stdin=<_UnixWritePipeTransport closing fd=10 idle bufsize=0> stdout=<_UnixReadPipeTransport closing fd=11 idle>>
INFO     shellous:log.py:60 Runner._start stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:60 Runner.__aenter__ stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:73 copy_bytearray stepin
INFO     shellous:log.py:73 write_stream stepin
INFO     shellous:log.py:55 Runner._waiter stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:60 Runner._waiter stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:77 write_stream stepout ex=None
INFO     shellous:log.py:77 copy_bytearray stepout ex=None
INFO     shellous:log.py:55 Runner.__aexit__ stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> exc_value=None
INFO     shellous:log.py:55 Runner._finish stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:55 Runner._wait stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:60 Runner._wait stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:55 Runner._close stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:60 Runner._close stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:60 Runner._finish stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:60 Runner.__aexit__ stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None exc_value=None

```
Latest version of the log.  May need to synthesize a BrokenPipeError in this case...

```
------------------------------ Captured log call -------------------------------
INFO     shellous:log.py:47 Runner.__aenter__ stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=None> (macOS-10.15.7 CPython 3.9.6 asyncio.unix_events._UnixSelectorEventLoop MainThread ThreadedChildWatcher)
INFO     shellous:log.py:55 Runner._start stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=None>
INFO     asyncio:base_subprocess.py:211 <_UnixSubprocessTransport pid=5288 running stdin=<_UnixWritePipeTransport fd=10 idle bufsize=0> stdout=<_UnixReadPipeTransport fd=11 polling>> exited with return code 0
INFO     asyncio:unix_events.py:642 <_UnixWritePipeTransport fd=10 idle bufsize=0> was closed by peer
INFO     asyncio:unix_events.py:503 <_UnixReadPipeTransport fd=11 polling> was closed by peer
INFO     asyncio:base_events.py:1665 execute program '/Users/runner/Library/Caches/pypoetry/virtualenvs/shellous-fzoxJzh3-py3.9/bin/python': <_UnixSubprocessTransport pid=5288 returncode=0 stdin=<_UnixWritePipeTransport closing fd=10 idle bufsize=0> stdout=<_UnixReadPipeTransport closing fd=11 idle>>
INFO     shellous:log.py:60 Runner._start stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:60 Runner.__aenter__ stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:73 copy_bytearray stepin
INFO     shellous:log.py:73 write_stream stepin
INFO     shellous:log.py:55 Runner._waiter stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:60 Runner._waiter stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:77 write_stream stepout ex=None
INFO     shellous:log.py:77 copy_bytearray stepout ex=None
INFO     shellous:log.py:55 Runner.__aexit__ stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> exc_value=None
INFO     shellous:log.py:55 Runner._finish stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:55 Runner._wait stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:60 Runner._wait stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:55 Runner._close stepin <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0>
INFO     shellous:log.py:60 Runner._close stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:60 Runner._finish stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None
INFO     shellous:log.py:60 Runner.__aexit__ stepout <Runner '...llous-fzoxJzh3-py3.9/bin/python' pid=5288 exit_code=0> ex=None exc_value=None

```

Fix patch:
diff --git a/README.md b/README.md
index b487d3e0..e27dbfa5 100644
--- a/README.md
+++ b/README.md
@@ -256,8 +256,8 @@ shellous.result.ResultError: Result(output_bytes=b'', exit_code=-15, cancelled=T
 When you use `incomplete_result`, your code should respect the `cancelled` attribute in the Result object. 
 Otherwise, your code may swallow the CancelledError.
 
-Pseudo-Terminal Support
------------------------
+Pseudo-Terminal Support (Unix Only)
+-----------------------------------
 
 To run a command through a pseudo-terminal, set the `pty` option to True. Alternatively, you can pass
 a function to configure the tty mode and size.
diff --git a/shellous/redirect.py b/shellous/redirect.py
index 700cb149..25b8134d 100644
--- a/shellous/redirect.py
+++ b/shellous/redirect.py
@@ -4,6 +4,7 @@
 import enum
 import io
 import os
+from typing import Optional
 
 from shellous.log import LOGGER, log_method
 from shellous.util import decode
@@ -31,7 +32,7 @@ def is_custom(self):
 STDOUT_APPEND_TYPES = (str, bytes, os.PathLike)
 
 
-async def _drain(stream):
+async def _drain(stream: asyncio.StreamWriter):
     "Safe drain method."
     try:
         await stream.drain()
@@ -42,9 +43,18 @@ async def _drain(stream):
 
 
 @log_method(_DETAILED_LOGGING)
-async def write_stream(input_bytes, stream, eof=None):
+async def write_stream(
+    input_bytes: bytes,
+    stream: asyncio.StreamWriter,
+    eof: Optional[bytes] = None,
+):
     "Write input_bytes to stream."
     if input_bytes:
+        # Check for stream that is already closing before we've written
+        # anything. Proactively raise a BrokenPipeError here. (issue #45)
+        if stream.is_closing():
+            raise BrokenPipeError()
+
         stream.write(input_bytes)
         await _drain(stream)
 
@@ -68,7 +78,11 @@ async def write_stream(input_bytes, stream, eof=None):
 
 
 @log_method(_DETAILED_LOGGING)
-async def copy_stringio(source, dest, encoding):
+async def copy_stringio(
+    source: asyncio.StreamReader,
+    dest: io.StringIO,
+    encoding: str,
+):
     "Copy bytes from source stream to dest StringIO."
     # Collect partial reads into a BytesIO.
     buf = io.BytesIO()
@@ -85,7 +99,7 @@ async def copy_stringio(source, dest, encoding):
 
 
 @log_method(_DETAILED_LOGGING)
-async def copy_bytesio(source, dest):
+async def copy_bytesio(source: asyncio.StreamReader, dest: io.BytesIO):
     "Copy bytes from source stream to dest BytesIO."
     # Collect partial reads into a BytesIO.
     while True:
@@ -96,7 +110,7 @@ async def copy_bytesio(source, dest):
 
 
 @log_method(_DETAILED_LOGGING)
-async def copy_bytearray(source, dest):
+async def copy_bytearray(source: asyncio.StreamReader, dest: bytearray):
     "Copy bytes from source stream to dest bytearray."
     # Collect partial reads into a bytearray.
     while True:
@@ -106,7 +120,12 @@ async def copy_bytearray(source, dest):
         dest.extend(data)
 
 
-async def read_lines(source, encoding):
+async def read_lines(source: asyncio.StreamReader, encoding: Optional[str]):
     "Async iterator over lines in stream."
-    async for line in source:
-        yield decode(line, encoding)
+    if encoding is None:
+        async for line in source:
+            yield line
+
+    else:
+        async for line in source:
+            yield decode(line, encoding)
diff --git a/shellous/runner.py b/shellous/runner.py
index 8a1f036c..13f691f1 100644
--- a/shellous/runner.py
+++ b/shellous/runner.py
@@ -12,7 +12,7 @@
 from shellous.log import LOGGER, log_method
 from shellous.redirect import Redirect
 from shellous.result import Result, make_result
-from shellous.util import close_fds, decode
+from shellous.util import close_fds
 
 _NORMAL_LOGGING = True
 _DETAILED_LOGGING = True
@@ -477,6 +477,8 @@ async def _waiter(self):
     def _setup_output_sink(self, stream, sink, encoding, tag):
         "Set up a task to write to custom output sink."
         if isinstance(sink, io.StringIO):
+            if encoding is None:
+                raise TypeError("StringIO not supported when encoding=None")
             self.add_task(redir.copy_stringio(stream, sink, encoding), tag)
             stream = None
         elif isinstance(sink, io.BytesIO):
diff --git a/shellous/util.py b/shellous/util.py
index 22308be4..a4e4884c 100644
--- a/shellous/util.py
+++ b/shellous/util.py
@@ -6,12 +6,8 @@
 from .log import LOGGER
 
 
-def decode(data: Optional[bytes], encoding: Optional[str]) -> Union[str, bytes]:
+def decode(data: Optional[bytes], encoding: str) -> str:
     "Utility function to decode optional byte strings."
-    if encoding is None:
-        if data is None:
-            return b""
-        return data
     if not data:
         return ""
     return data.decode(*encoding.split(maxsplit=1))


Test patch:
diff --git a/tests/test_shellous.py b/tests/test_shellous.py
index 57be7fe3..b9b1f287 100644
--- a/tests/test_shellous.py
+++ b/tests/test_shellous.py
@@ -9,8 +9,7 @@
 from pathlib import Path
 
 import pytest
-from shellous import (CAPTURE, DEVNULL, INHERIT, PipeResult, Result,
-                      ResultError, context)
+from shellous import CAPTURE, DEVNULL, INHERIT, PipeResult, Result, ResultError, context
 from shellous.harvest import harvest_results
 
 pytestmark = pytest.mark.asyncio
@@ -341,9 +340,24 @@ async def test_broken_pipe_in_failed_pipeline(cat_cmd, echo_cmd):
     data = b"c" * PIPE_MAX_SIZE
     echo = echo_cmd.env(SHELLOUS_EXIT_CODE=7)
 
-    with pytest.raises(BrokenPipeError):
+    # This test has a race condition. Sometimes we get a BrokenPipeError, and
+    # sometimes both commands fail before shellous detects the broken pipe.
+
+    with pytest.raises((BrokenPipeError, ResultError)) as exc_info:
         await (data | cat_cmd | echo("abc"))
 
+    if exc_info.type == ResultError:
+        assert exc_info.value.result == Result(
+            output_bytes=b"abc",
+            exit_code=7,
+            cancelled=False,
+            encoding="utf-8",
+            extra=(
+                PipeResult(exit_code=120, cancelled=True),
+                PipeResult(exit_code=7, cancelled=False),
+            ),
+        )
+
 
 async def test_broken_pipe_in_async_with_failed_pipeline(cat_cmd, echo_cmd):
     "Test broken pipe error within a pipeline; last command fails."
@@ -533,3 +547,24 @@ async def test_process_substitution(echo_cmd, cat_cmd):
     else:
         result = await cmd
         assert result == "abc"
+
+
+async def test_async_iter_with_bytes_encoding(cat_cmd):
+    "Test async iteration with encoding=None."
+
+    cmd = b"a\nb\nc\nd" | cat_cmd.set(encoding=None)
+
+    async with cmd.run() as run:
+        lines = [line async for line in run]
+
+    assert lines == [b"a\n", b"b\n", b"c\n", b"d"]
+
+
+async def test_stringio_redirect_with_bytes_encoding(echo_cmd):
+    "Can't use StringIO redirect output buffer with encoding=None."
+
+    buf = io.StringIO()
+    cmd = echo_cmd | buf
+
+    with pytest.raises(TypeError, match="StringIO"):
+        await cmd("abc").set(encoding=None)
diff --git a/tests/test_util.py b/tests/test_util.py
index 33a6ce4c..f6314a1c 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -8,9 +8,6 @@
 
 def test_decode():
     "Test the util.decode function."
-    assert decode(None, None) == b""
-    assert decode(b"", None) == b""
-    assert decode(b"abc", None) == b"abc"
 
     assert decode(b"", "utf-8") == ""
     assert decode(b"abc", "utf-8") == "abc"
@@ -22,6 +19,16 @@ def test_decode():
     assert decode(b"\x81abc", "utf-8 replace") == "\ufffdabc"
 
 
+def test_decode_encoding_none():
+    "Test the util.decode function encoding=None (invalid)."
+
+    # Invalid but allowed.
+    assert decode(None, None) == ""
+
+    with pytest.raises(AttributeError):
+        decode(b"abc", None)
+
+
 def test_coerce_env():
     "Test the util.coerce_env function."
     result = coerce_env(dict(a="a", b=1))
diff --git a/tests/unix/test_unix.py b/tests/unix/test_unix.py
index 973da299..399e26e6 100644
--- a/tests/unix/test_unix.py
+++ b/tests/unix/test_unix.py
@@ -881,6 +881,7 @@ async def test_pty(sh):
     async with cmd.run() as run:
         run.stdin.write(b"abc\n")
         await run.stdin.drain()
+        await asyncio.sleep(0.1)
         result = await run.stdout.read(1024)
         run.stdin.close()
 
@@ -966,6 +967,7 @@ async def test_pty_tr_eot(sh):
     async with cmd.run() as run:
         run.stdin.write(b"abc\n\x04")
         await run.stdin.drain()
+        await asyncio.sleep(0.1)
         result = await run.stdout.read(1024)
 
     if sys.platform == "linux":
@@ -982,6 +984,7 @@ async def test_pty_cat_eot(sh):
     async with cmd.run() as run:
         run.stdin.write(b"abc\x04\x04")
         await run.stdin.drain()
+        await asyncio.sleep(0.1)
         result = await run.stdout.read(1024)
 
     if sys.platform == "linux":