Skip to content
This repository has been archived by the owner on Feb 7, 2018. It is now read-only.

Commit

Permalink
Save both STDOUT and STDERR when possible
Browse files Browse the repository at this point in the history
Create an Output object that stores the returned output and error
streams from the executed commands. Because only Process.spawn and
Posix.spawn use pipes like this, they are the only two Runners that can
handle capturing both STDOUT and STDERR. The PopenRunner can only handle
one pipe, and backticks can only report on STDOUT.
  • Loading branch information
Jon Yurek committed Sep 25, 2015
1 parent 2a89b71 commit ed4b3ab
Show file tree
Hide file tree
Showing 18 changed files with 186 additions and 69 deletions.
2 changes: 2 additions & 0 deletions lib/cocaine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'rbconfig'
require 'cocaine/os_detector'
require 'cocaine/command_line'
require 'cocaine/command_line/output'
require 'cocaine/command_line/multi_pipe'
require 'cocaine/command_line/runners'
require 'cocaine/exceptions'

Expand Down
21 changes: 16 additions & 5 deletions lib/cocaine/command_line.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,11 @@ def command(interpolations = {})
end

def run(interpolations = {})
output = ''
@exit_status = nil
begin
full_command = command(interpolations)
log("#{colored("Command")} :: #{full_command}")
output = execute(full_command)
@output = execute(full_command)
rescue Errno::ENOENT => e
raise Cocaine::CommandNotFoundError, e.message
ensure
Expand All @@ -86,12 +85,24 @@ def run(interpolations = {})
unless @expected_outcodes.include?(@exit_status)
message = [
"Command '#{full_command}' returned #{@exit_status}. Expected #{@expected_outcodes.join(", ")}",
"Here is the command output:\n",
output
"Here is the command output: STDOUT:\n", command_output,
"\nSTDERR:\n", command_error_output
].join("\n")
raise Cocaine::ExitStatusError, message
end
output
command_output
end

def command_output
output.output
end

def command_error_output
output.error_output
end

def output
@output || Output.new
end

private
Expand Down
50 changes: 50 additions & 0 deletions lib/cocaine/command_line/multi_pipe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Cocaine
class CommandLine
class MultiPipe
def initialize
@stdout_in, @stdout_out = IO.pipe
@stderr_in, @stderr_out = IO.pipe
end

def pipe_options
{ out: @stdout_out, err: @stderr_out }
end

def output
Output.new(@stdout_output, @stderr_output)
end

def read_and_then(&block)
close_write
read
block.call
close_read
end

private

def close_write
@stdout_out.close
@stderr_out.close
end

def read
@stdout_output = read_stream(@stdout_in)
@stderr_output = read_stream(@stderr_in)
end

def close_read
@stdout_in.close
@stderr_in.close
end

def read_stream(io)
result = ""
while partial_result = io.read(8192)
result << partial_result
end
result
end
end
end
end
12 changes: 12 additions & 0 deletions lib/cocaine/command_line/output.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Cocaine::CommandLine::Output
def initialize(output = nil, error_output = nil)
@output = output
@error_output = error_output
end

attr_reader :output, :error_output

def to_s
output.to_s
end
end
2 changes: 1 addition & 1 deletion lib/cocaine/command_line/runners/backticks_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def supported?

def call(command, env = {}, options = {})
with_modified_environment(env) do
`#{command}`
Output.new(`#{command}`)
end
end

Expand Down
5 changes: 2 additions & 3 deletions lib/cocaine/command_line/runners/fake_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ def initialize

def call(command, env = {}, options = {})
commands << [command, env]
""
Output.new("")
end

def ran?(predicate_command)
@commands.any?{|(command, env)| command =~ Regexp.new(predicate_command) }
@commands.any?{|(command, _)| command =~ Regexp.new(predicate_command) }
end

end
end
end
2 changes: 1 addition & 1 deletion lib/cocaine/command_line/runners/popen_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def supported?
def call(command, env = {}, options = {})
with_modified_environment(env) do
IO.popen(command, "r", options) do |pipe|
pipe.read
Output.new(pipe.read)
end
end
end
Expand Down
15 changes: 5 additions & 10 deletions lib/cocaine/command_line/runners/posix_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@ def supported?
end

def call(command, env = {}, options = {})
input, output = IO.pipe
options[:out] = output
pid = spawn(env, command, options)
output.close
result = ""
while partial_result = input.read(8192)
result << partial_result
pipe = MultiPipe.new
pid = spawn(env, command, options.merge(pipe.pipe_options))
pipe.read_and_then do
waitpid(pid)
end
waitpid(pid)
input.close
result
pipe.output
end

private
Expand Down
14 changes: 6 additions & 8 deletions lib/cocaine/command_line/runners/process_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ def supported?
end

def call(command, env = {}, options = {})
input, output = IO.pipe
options[:out] = output
pid = spawn(env, command, options)
output.close
result = input.read
waitpid(pid)
input.close
result
pipe = MultiPipe.new
pid = spawn(env, command, options.merge(pipe.pipe_options))
pipe.read_and_then do
waitpid(pid)
end
pipe.output
end

private
Expand Down
14 changes: 14 additions & 0 deletions spec/cocaine/command_line/output_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require 'spec_helper'

describe Cocaine::CommandLine::Output do
it 'holds an input and error stream' do
output = Cocaine::CommandLine::Output.new(:a, :b)
expect(output.output).to eq :a
expect(output.error_output).to eq :b
end

it 'calls #to_s on the output when you call #to_s on it' do
output = Cocaine::CommandLine::Output.new(:a, :b)
expect(output.to_s).to eq 'a'
end
end
8 changes: 5 additions & 3 deletions spec/cocaine/command_line/runners/backticks_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
if Cocaine::CommandLine::BackticksRunner.supported?
it_behaves_like 'a command that does not block'

it 'runs the command given' do
subject.call("echo hello").should == "hello\n"
it 'runs the command given and captures the output in an Output' do
output = subject.call("echo hello")
expect(output).to have_output "hello\n"
end

it 'modifies the environment and runs the command given' do
subject.call("echo $yes", {"yes" => "no"}).should == "no\n"
output = subject.call("echo $yes", {"yes" => "no"})
expect(output).to have_output "no\n"
end

it 'sets the exitstatus when a command completes' do
Expand Down
8 changes: 5 additions & 3 deletions spec/cocaine/command_line/runners/popen_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
if Cocaine::CommandLine::PopenRunner.supported?
it_behaves_like 'a command that does not block'

it 'runs the command given' do
subject.call("echo hello").should == "hello\n"
it 'runs the command given and captures the output in an Output' do
output = subject.call("echo hello")
expect(output).to have_output "hello\n"
end

it 'modifies the environment and runs the command given' do
subject.call("echo $yes", {"yes" => "no"}).should == "no\n"
output = subject.call("echo $yes", {"yes" => "no"})
expect(output).to have_output "no\n"
end

it 'sets the exitstatus when a command completes' do
Expand Down
13 changes: 10 additions & 3 deletions spec/cocaine/command_line/runners/posix_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
if Cocaine::CommandLine::PosixRunner.supported?
it_behaves_like 'a command that does not block'

it 'runs the command given' do
subject.call("echo hello").should == "hello\n"
it 'runs the command given and captures the output' do
output = subject.call("echo hello")
expect(output).to have_output "hello\n"
end

it 'runs the command given and captures the error output' do
output = subject.call("echo hello 1>&2")
expect(output).to have_error_output "hello\n"
end

it 'modifies the environment and runs the command given' do
subject.call("echo $yes", {"yes" => "no"}).should == "no\n"
output = subject.call("echo $yes", {"yes" => "no"})
expect(output).to have_output "no\n"
end

it 'sets the exitstatus when a command completes' do
Expand Down
13 changes: 10 additions & 3 deletions spec/cocaine/command_line/runners/process_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
if Cocaine::CommandLine::ProcessRunner.supported?
it_behaves_like "a command that does not block"

it 'runs the command given' do
subject.call("echo hello").should == "hello\n"
it 'runs the command given and captures the output' do
output = subject.call("echo hello")
expect(output).to have_output "hello\n"
end

it 'runs the command given and captures the error output' do
output = subject.call("echo hello 1>&2")
expect(output).to have_error_output "hello\n"
end

it 'modifies the environment and runs the command given' do
subject.call("echo $yes", {"yes" => "no"}).should == "no\n"
output = subject.call("echo $yes", {"yes" => "no"})
expect(output).to have_output "no\n"
end

it 'sets the exitstatus when a command completes' do
Expand Down
26 changes: 20 additions & 6 deletions spec/cocaine/command_line_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,26 @@
cmd.command.should == "convert a.jpg b.png 2>NUL"
end

it "runs the command it's given and return the output" do
cmd = Cocaine::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.stubs(:execute).with("convert a.jpg b.png").returns(:correct_value)
with_exitstatus_returning(0) do
cmd.run.should == :correct_value
end
it "runs the command it's given and returns the output" do
cmd = Cocaine::CommandLine.new("echo", "hello", :swallow_stderr => false)
expect(cmd.run).to eq "hello\n"
end

it "runs the command it's given and allows access to stdout afterwards" do
cmd = Cocaine::CommandLine.new("echo", "hello", :swallow_stderr => false)
cmd.run
expect(cmd.command_output).to eq "hello\n"
end

it "runs the command it's given and allows access to stderr afterwards" do
cmd = Cocaine::CommandLine.new(
"ruby",
"-e '$stdout.puts %{hello}; $stderr.puts %{goodbye}'",
:swallow_stderr => false
)
cmd.run
expect(cmd.command_output).to eq "hello\n"
expect(cmd.command_error_output).to eq "goodbye\n"
end

it "colorizes the output to a tty" do
Expand Down
37 changes: 15 additions & 22 deletions spec/cocaine/errors_spec.rb
Original file line number Diff line number Diff line change
@@ -1,47 +1,40 @@
require 'spec_helper'

describe "When an error happens" do
it "raises a CommandLineError if the result code from the command isn't expected" do
cmd = Cocaine::CommandLine.new("convert", "a.jpg b.png", :swallow_stderr => false)
cmd.stubs(:execute).with("convert a.jpg b.png").returns(:correct_value)
it "raises a CommandLineError if the result code command isn't expected" do
cmd = Cocaine::CommandLine.new("echo", "hello")
cmd.stubs(:execute)
with_exitstatus_returning(1) do
lambda do
cmd.run
end.should raise_error(Cocaine::CommandLineError)
lambda { cmd.run }.should raise_error(Cocaine::CommandLineError)
end
end

it "does not raise if the result code is expected, even if nonzero" do
cmd = Cocaine::CommandLine.new("convert",
"a.jpg b.png",
:expected_outcodes => [0, 1],
:swallow_stderr => false)
cmd.stubs(:execute).with("convert a.jpg b.png").returns(:correct_value)
cmd = Cocaine::CommandLine.new("echo", "hello", expected_outcodes: [0, 1])
cmd.stubs(:execute)
with_exitstatus_returning(1) do
lambda do
cmd.run
end.should_not raise_error
lambda { cmd.run }.should_not raise_error
end
end

it "adds command output to exception message if the result code is nonzero" do
cmd = Cocaine::CommandLine.new("convert",
"a.jpg b.png",
:swallow_stderr => false)
cmd = Cocaine::CommandLine.new("echo", "hello")
error_output = "Error 315"
cmd.stubs(:execute).with("convert a.jpg b.png").returns(error_output)
cmd.
stubs(:execute).
returns(Cocaine::CommandLine::Output.new("", error_output))
with_exitstatus_returning(1) do
begin
cmd.run
rescue Cocaine::ExitStatusError => e
e.message.should =~ /#{error_output}/
e.message.should =~ /STDERR:\s+#{error_output}/
end
end
end

it 'passes error message to the exception when command fails with Errno::ENOENT' do
it 'passes the error message to the exception when command is not found' do
cmd = Cocaine::CommandLine.new('test', '')
cmd.stubs(:execute).with('test').raises(Errno::ENOENT.new("not found"))
cmd.stubs(:execute).raises(Errno::ENOENT.new("not found"))
begin
cmd.run
rescue Cocaine::CommandNotFoundError => e
Expand All @@ -58,7 +51,7 @@
cmd.exit_status.should == 1
end

it "does not blow up if running the command errored before the actual execution" do
it "does not blow up if running the command errored before execution" do
assuming_no_processes_have_been_run
command = Cocaine::CommandLine.new("echo", ":hello_world")
command.stubs(:command).raises("An Error")
Expand Down
Loading

0 comments on commit ed4b3ab

Please sign in to comment.