Skip to content

Commit f3603cd

Browse files
authored
Merge pull request #5 from rezib/pr/run-yoann-comments
Some improvements in run module
2 parents b0cdeec + 83e0c74 commit f3603cd

File tree

3 files changed

+75
-58
lines changed

3 files changed

+75
-58
lines changed

lib/rift/Mock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def _exec(self, cmd):
172172
cmd,
173173
live_output=logging.getLogger().isEnabledFor(logging.INFO),
174174
capture_output=True,
175-
merged_capture=True,
175+
merge_out_err=True,
176176
cwd='/'
177177
)
178178
if proc.returncode != 0:

lib/rift/run.py

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,30 @@
4545
'RunResult', ['returncode', 'out', 'err']
4646
)
4747

48-
def _handle_process_output(process, selector):
49-
"""Handle process output events with registered selectors."""
48+
def _handle_process_output(process, live_output, buf_out, buf_err):
49+
"""Handle process output until it is terminated."""
50+
51+
# Process output lines handlers
52+
def handle_stdout_line(line):
53+
buf_out.write(line)
54+
if live_output:
55+
sys.stdout.write(line)
56+
def handle_stderr_line(line):
57+
buf_err.write(line)
58+
if live_output:
59+
sys.stderr.write(line)
60+
61+
# Process output event handlers
62+
def handle_stdout_event(stream):
63+
handle_stdout_line(stream.readline())
64+
def handle_stderr_event(stream):
65+
handle_stderr_line(stream.readline())
66+
67+
# Register callback for read events from subprocess stdout/stderr streams
68+
selector = selectors.DefaultSelector()
69+
selector.register(process.stdout, selectors.EVENT_READ, handle_stdout_event)
70+
selector.register(process.stderr, selectors.EVENT_READ, handle_stderr_event)
71+
5072
# Loop until subprocess is terminated
5173
while process.poll() is None:
5274
# Wait for events and handle them with their registered callbacks
@@ -55,18 +77,31 @@ def _handle_process_output(process, selector):
5577
callback = key.data
5678
callback(key.fileobj)
5779

80+
# Close selector
81+
selector.close()
82+
83+
# The loop above stops processing output as soon as the process is
84+
# terminated. However, there may still be buffered output to flush.
85+
for line in process.stdout:
86+
handle_stdout_line(line)
87+
for line in process.stderr:
88+
handle_stderr_line(line)
89+
90+
# Ensure process is terminated
91+
process.wait()
92+
5893
def run_command(
5994
cmd,
6095
live_output=True,
6196
capture_output=False,
62-
merged_capture=False,
97+
merge_out_err=False,
6398
**kwargs
6499
):
65100
"""
66-
Run a command and return RunResult named tuple. When live_output is True,
67-
command stdout/stderr are streamed in live in current process stdout/stderr.
68-
When capture_output is True, command stdout/stderr are available in out/err
69-
attributes of RunResult namedtuple. When merged_capture is True as well,
101+
Run a command and return a RunResult named tuple. When live_output is True,
102+
command stdout/stderr are redirected to current process stdout/stderr. When
103+
capture_output is True, command stdout/stderr are available in out/err
104+
attributes of RunResult namedtuple. When merge_out_err is True as well,
70105
command stderr is merged with stdout in out attribute of RunResult named
71106
tuple. In this case, err attribute is None.
72107
@@ -100,55 +135,18 @@ def run_command(
100135
# Initialize string buffers to store process output in memory
101136
buf_out = io.StringIO()
102137
buf_err = None
103-
if not merged_capture:
138+
if merge_out_err:
139+
buf_err = buf_out
140+
else:
104141
buf_err = io.StringIO()
105142

106-
# Process output lines handlers
107-
def handle_stdout(stream):
108-
line = stream.readline()
109-
buf_out.write(line)
110-
if live_output:
111-
sys.stdout.write(line)
112-
def handle_stderr(stream):
113-
line = stream.readline()
114-
if merged_capture:
115-
buf_out.write(line)
116-
else:
117-
buf_err.write(line)
118-
if live_output:
119-
sys.stderr.write(line)
120-
121-
# Register callback for read events from subprocess stdout/stderr streams
122-
selector = selectors.DefaultSelector()
123-
selector.register(process.stdout, selectors.EVENT_READ, handle_stdout)
124-
selector.register(process.stderr, selectors.EVENT_READ, handle_stderr)
125-
126-
# Handle process output with registered selectors
127-
_handle_process_output(process, selector)
128-
129-
# _handle_process_output stops processing output as soon as the process
130-
# is terminated. However, there may still be buffered output to flush.
131-
for line in process.stdout:
132-
buf_out.write(line)
133-
if live_output:
134-
sys.stdout.write(line)
135-
for line in process.stderr:
136-
if merged_capture:
137-
buf_out.write(line)
138-
else:
139-
buf_err.write(line)
140-
if live_output:
141-
sys.stdout.write(line)
142-
143-
# Ensure process is terminated
144-
process.wait()
145-
146-
# Store buffered output
147-
selector.close()
143+
# Handle process output
144+
_handle_process_output(process, live_output, buf_out, buf_err)
148145

146+
# Get values for out/err buffers and close them
149147
out = buf_out.getvalue()
150148
buf_out.close()
151-
if merged_capture:
149+
if merge_out_err:
152150
err = None
153151
else:
154152
err = buf_err.getvalue()

tests/run.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ def test_run_command_failed(self):
2525
""" Test run_command() with basic failed command. """
2626
proc = run_command(["/bin/false"])
2727
self.assertEqual(proc.returncode, 1)
28+
self.assertIsNone(proc.out)
29+
self.assertIsNone(proc.err)
2830

2931
@patch('sys.stdout', new_callable=StringIO)
3032
def test_run_command_capture_stdout(self, mock_stdout):
3133
""" Test run_command() with captured standard output. """
3234
proc = run_command(["/bin/echo", "output_data"], capture_output=True)
3335
# Standard output must be available in out attribute of RunResult named
3436
# tuple.
37+
self.assertEqual(proc.returncode, 0)
3538
self.assertEqual(proc.out, "output_data\n")
3639
self.assertEqual(proc.err, "")
3740
# Standard output must also be streamed into current process stdout.
@@ -43,26 +46,42 @@ def test_run_command_capture_stderr(self, mock_stderr):
4346
proc = run_command("/bin/echo error_data 1>&2", capture_output=True, shell=True)
4447
# Standard err must be available in err attribute of RunResult named
4548
# tuple.
49+
self.assertEqual(proc.returncode, 0)
4650
self.assertEqual(proc.out, "")
4751
self.assertEqual(proc.err, "error_data\n")
4852
# Standard error must also be streamed into current process stderr.
4953
self.assertEqual(mock_stderr.getvalue(), "error_data\n")
5054

5155
@patch('sys.stderr', new_callable=StringIO)
52-
def test_run_command_capture_merged(self, mock_stderr):
53-
""" Test run_command() with merged output capture. """
54-
proc = run_command("/bin/echo error_data 1>&2", capture_output=True, merged_capture=True, shell=True)
56+
def test_run_command_capture_stderr_merged(self, mock_stderr):
57+
""" Test run_command() with merged error output capture. """
58+
proc = run_command("/bin/echo error_data 1>&2", capture_output=True,
59+
merge_out_err=True, shell=True)
5560
# With merged_capture, standard err must be available in out attribute
5661
# of RunResult named tuple, and err attribute must be None.
5762
self.assertEqual(proc.out, "error_data\n")
5863
self.assertIsNone(proc.err)
5964
# Standard error must also be streamed into current process stderr.
6065
self.assertEqual(mock_stderr.getvalue(), "error_data\n")
6166

67+
@patch('sys.stderr', new_callable=StringIO)
68+
def test_run_command_capture_both_merged(self, mock_stderr):
69+
""" Test run_command() with merged error and standard output capture. """
70+
proc = run_command("/bin/echo error_data 1>&2 && /bin/echo output_data",
71+
capture_output=True, merge_out_err=True, shell=True)
72+
# With merge_out_err, standard err must be available in out attribute
73+
# of RunResult named tuple, and err attribute must be None.
74+
self.assertEqual(proc.out, "error_data\noutput_data\n")
75+
self.assertIsNone(proc.err)
76+
# Standard error must also be streamed into current process stderr.
77+
self.assertEqual(mock_stderr.getvalue(), "error_data\n")
78+
79+
@patch('sys.stderr', new_callable=StringIO)
6280
@patch('sys.stdout', new_callable=StringIO)
63-
def test_run_command_no_output(self, mock_stdout):
81+
def test_run_command_no_output(self, mock_stdout, mock_stderr):
6482
""" Test run_command() without live output. """
65-
# With live_output disabled, standard output must not be streamed into
66-
# current process stdout.
83+
# With live_output disabled, standard output and standard error must not
84+
# be redirected in current process stdout.
6785
proc = run_command(["/bin/echo", "output_data"], live_output=False)
6886
self.assertEqual(mock_stdout.getvalue(), "")
87+
self.assertEqual(mock_stderr.getvalue(), "")

0 commit comments

Comments
 (0)