Skip to content

Commit 2034b82

Browse files
committed
Only sleep when input stream is waiting
This change means that we don't delay reading the input stream when there is still data available to read from it. This provides a significant speed improvement to scripts which are passing a populated stream of data, rather than awaiting user input from stdin. See #774
1 parent 6a71e68 commit 2034b82

2 files changed

Lines changed: 45 additions & 7 deletions

File tree

invoke/runners.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,10 @@ def handle_stdin(
912912
# race conditions re: unread stdin.)
913913
if self.program_finished.is_set() and not data:
914914
break
915+
# When data is None, we're waiting for input on stdin.
915916
# Take a nap so we're not chewing CPU.
916-
time.sleep(self.input_sleep)
917+
if data is None:
918+
time.sleep(self.input_sleep)
917919

918920
def should_echo_stdin(self, input_: IO, output: IO) -> bool:
919921
"""

tests/runners.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import threading
88
import types
99
from contextlib import AbstractContextManager
10-
from io import BytesIO, StringIO
10+
from io import BytesIO, StringIO, TextIOBase
1111
from itertools import chain, repeat
1212
from unittest.mock import Mock, call, patch
1313

@@ -1100,16 +1100,52 @@ def subclasses_can_override_input_sleep(self):
11001100
class MyRunner(_Dummy):
11011101
input_sleep = 0.007
11021102

1103+
def fake_stdin_stream():
1104+
# The value "foo" is eventually returned.
1105+
yield "f"
1106+
# None values simulate waiting for input on stdin.
1107+
yield None
1108+
yield "o"
1109+
yield None
1110+
yield "o"
1111+
yield None
1112+
# Once the stream is closed, stdin returns empty strings.
1113+
while True:
1114+
yield ""
1115+
1116+
class FakeStdin(TextIOBase):
1117+
def __init__(self, stdin):
1118+
self.stream = stdin
1119+
1120+
def read(self, size):
1121+
return next(self.stream)
1122+
11031123
with patch("invoke.runners.time") as mock_time:
11041124
MyRunner(Context()).run(
11051125
_,
1106-
in_stream=StringIO("foo"),
1126+
in_stream=FakeStdin(fake_stdin_stream()),
11071127
out_stream=StringIO(), # null output to not pollute tests
11081128
)
1109-
# Just make sure the first few sleeps all look good. Can't know
1110-
# exact length of list due to stdin worker hanging out til end of
1111-
# process. Still worth testing more than the first tho.
1112-
assert mock_time.sleep.call_args_list[:3] == [call(0.007)] * 3
1129+
# Just make sure the sleeps all look good.
1130+
# There are three calls because of the Nones in fake_stdin_stream.
1131+
assert mock_time.sleep.call_args_list == [call(0.007)] * 3
1132+
1133+
@mock_subprocess()
1134+
def populated_streams_do_not_sleep(self):
1135+
class MyRunner(_Dummy):
1136+
read_chunk_size = 1
1137+
1138+
runner = MyRunner(Context())
1139+
with patch("invoke.runners.time") as mock_time:
1140+
with patch.object(runner, "wait"):
1141+
runner.run(
1142+
_,
1143+
in_stream=StringIO("lots of bytes to read"),
1144+
# null output to not pollute tests
1145+
out_stream=StringIO(),
1146+
)
1147+
# Sleep should not be called before we break.
1148+
assert len(mock_time.sleep.call_args_list) == 0
11131149

11141150
class stdin_mirroring:
11151151
def _test_mirroring(self, expect_mirroring, **kwargs):

0 commit comments

Comments
 (0)