Capturing stdout one line at a time?
Closed this issue ยท 8 comments
Hi,
How can I capture stdout one line at a time with Async?
Without async I can capture stdout with:
def with_captured_stdout
original_stdout = $stdout # capture previous value of $stdout
$stdout = StringIO.new # assign a string buffer to $stdout
yield # perform the body of the user code
$stdout.string # return the contents of the string buffer
ensure
$stdout = original_stdout # restore $stdout to its previous value
end
captured = with_captured_stdout { puts "guava"}
puts "captured: #{captured}"
But I would like to process each line as it's written to stdout. It's for testing a CLI, where I would like to stop a long-running Async task as a soon as a particular output to stdout is seen.
Have you considered using a pipe?
You mean running it as a separate process and using IO.pipe?
I have a solution based on RSpec and Aruba, where the executable is run (presumably in a separate process). The drawback is that it's quite slow, but I guess that's because the output from the executable is not evaluated line-by-line. Instead it's stopped after a certain time, and the entire output is then checked.
Something like this should work:
#!/usr/bin/env ruby
require 'async'
input, output = IO.pipe
$stdout.reopen(output)
Async do
Async do
10.times do
puts "Hello"
sleep 1
end
end
while line = input.gets
# Log this to stderr otherwise it will go to our pipe:
$stderr.puts "Got: #{line}"
end
end
Maybe I can use a thin wrapper around stdout, and use Async just for a timeout:
require 'async'
class Capture
class Match < StandardError
end
attr_reader :original_stdout
def initialize regex
@regex = regex
@original_stdout = $stdout
end
def puts s
@original_stdout.puts s
raise Match if @regex.match s
end
def write s
@original_stdout.write s
end
end
def expect_stdout_within regex, timeout:
Async do |task|
$stdout = Capture.new regex
task.with_timeout(timeout) { yield }
rescue Capture::Match
end
ensure
$stdout = $stdout.original_stdout
end
expect_stdout_within /Ready/, timeout: 3.5 do
puts 'Step 1'; sleep 1
puts 'Step 2'; sleep 1
puts 'Ready'; sleep 1
puts 'Step 3'; sleep 1
puts 'Step 4'; sleep 1
puts 'Step 5'; sleep 1
end
Seems to work... As soon as "Ready" is output using puts(), the code stops (ie. the test has passed). If the timeout is reached, an exception can be raised (ie. test has failed).
Didn't see your reply before I posted the above - thank you, will try it!
Yeah your solution seems simpler and more robust. Going to close this issue, it seems there are not really any Async classes needed for capturing stdout. Thank you for your help!
In your example, how would you restore $stdout to it's normal state after calling reopen()?
#!/usr/bin/env ruby
require 'async'
def captured_stdout
input, output = IO.pipe
old_stdout = $stdout.dup
$stdout.reopen(output)
yield input, output
ensure
if old_stdout
$stdout.reopen(old_stdout)
end
end
Async do
captured_stdout do |input, output|
reader = Async do
while line = input.gets
# Log this to stderr otherwise it will go to our pipe:
$stderr.puts "Got: #{line}"
end
end
10.times do
puts "Hello"
sleep 0.1
end
reader.stop
end
puts "Done"
end