diff --git a/invoke/runners.py b/invoke/runners.py index f1c888f44..04699ca4f 100644 --- a/invoke/runners.py +++ b/invoke/runners.py @@ -1,5 +1,6 @@ import errno import locale +import io import os import struct import sys @@ -71,7 +72,7 @@ class Runner: opts: Dict[str, Any] using_pty: bool - read_chunk_size = 1000 + read_chunk_size = io.DEFAULT_BUFFER_SIZE input_sleep = 0.01 def __init__(self, context: "Context") -> None: @@ -894,8 +895,10 @@ def handle_stdin( # race conditions re: unread stdin.) if self.program_finished.is_set() and not data: break + # When data is None, we're waiting for input on stdin. # Take a nap so we're not chewing CPU. - time.sleep(self.input_sleep) + if data is None: + time.sleep(self.input_sleep) def should_echo_stdin(self, input_: IO, output: IO) -> bool: """ diff --git a/tests/runners.py b/tests/runners.py index 94c63d8b3..e55218aeb 100644 --- a/tests/runners.py +++ b/tests/runners.py @@ -8,6 +8,7 @@ import types from io import StringIO from io import BytesIO +from io import TextIOBase from itertools import chain, repeat from pytest import raises, skip @@ -1098,16 +1099,52 @@ def subclasses_can_override_input_sleep(self): class MyRunner(_Dummy): input_sleep = 0.007 + def fake_stdin_stream(): + # The value "foo" is eventually returned. + yield "f" + # None values simulate waiting for input on stdin. + yield None + yield "o" + yield None + yield "o" + yield None + # Once the stream is closed, stdin returns empty strings. + while True: + yield "" + + class FakeStdin(TextIOBase): + def __init__(self, stdin): + self.stream = stdin + + def read(self, size): + return next(self.stream) + with patch("invoke.runners.time") as mock_time: MyRunner(Context()).run( _, - in_stream=StringIO("foo"), + in_stream=FakeStdin(fake_stdin_stream()), out_stream=StringIO(), # null output to not pollute tests ) - # Just make sure the first few sleeps all look good. Can't know - # exact length of list due to stdin worker hanging out til end of - # process. Still worth testing more than the first tho. - assert mock_time.sleep.call_args_list[:3] == [call(0.007)] * 3 + # Just make sure the sleeps all look good. + # There are three calls because of the Nones in fake_stdin_stream. + assert mock_time.sleep.call_args_list == [call(0.007)] * 3 + + @mock_subprocess() + def populated_streams_do_not_sleep(self): + class MyRunner(_Dummy): + read_chunk_size = 1 + + runner = MyRunner(Context()) + with patch("invoke.runners.time") as mock_time: + with patch.object(runner, "wait"): + runner.run( + _, + in_stream=StringIO("lots of bytes to read"), + # null output to not pollute tests + out_stream=StringIO(), + ) + # Sleep should not be called before we break. + assert len(mock_time.sleep.call_args_list) == 0 class stdin_mirroring: def _test_mirroring(self, expect_mirroring, **kwargs):