Forward standard error stream from remote execution

This commit is contained in:
Thibault Jouan 2014-07-21 12:45:57 +00:00
parent a033e19583
commit db91eb06cd
14 changed files with 93 additions and 17 deletions

View File

@ -11,6 +11,16 @@ Feature: `sh' task action
When I successfully execute the recipe on remote target When I successfully execute the recipe on remote target
Then the output must contain exactly "hello from remote\n" Then the output must contain exactly "hello from remote\n"
Scenario: forwards error ouput
Given a recipe with:
"""
task :sh_action do
sh '\echo error from remote >&2'
end
"""
When I successfully execute the recipe on remote target
Then the error output must contain exactly "error from remote\n"
Scenario: aborts on failed command execution Scenario: aborts on failed command execution
Given a recipe with: Given a recipe with:
""" """

View File

@ -21,3 +21,7 @@ end
Then /^the output must contain exactly:$/ do |content| Then /^the output must contain exactly:$/ do |content|
assert_exact_output content, all_output assert_exact_output content, all_output
end end
Then /^the error output must contain exactly "([^"]+)"$/ do |content|
assert_exact_output content, all_stderr
end

View File

@ -2,7 +2,7 @@ module Producer
module Core module Core
class Action class Action
extend Forwardable extend Forwardable
def_delegators :@env, :input, :output, :remote def_delegators :@env, :input, :output, :error_output, :remote
def_delegators :remote, :fs def_delegators :remote, :fs
attr_reader :env, :arguments attr_reader :env, :arguments

View File

@ -7,7 +7,7 @@ module Producer
end end
def apply def apply
remote.execute(arguments.first, output) remote.execute(arguments.first, output, error_output)
end end
end end
end end

View File

@ -31,7 +31,7 @@ module Producer
@arguments = args @arguments = args
@stdin = stdin @stdin = stdin
@stdout = stdout @stdout = stdout
@env = Env.new(input: stdin, output: stdout) @env = Env.new(input: stdin, output: stdout, error_output: stderr)
end end
def parse_arguments! def parse_arguments!

View File

@ -1,15 +1,16 @@
module Producer module Producer
module Core module Core
class Env class Env
attr_reader :input, :output, :registry, :logger attr_reader :input, :output, :error_output, :registry, :logger
attr_accessor :target, :verbose, :dry_run attr_accessor :target, :verbose, :dry_run
def initialize(input: $stdin, output: $stdout, remote: nil, registry: {}) def initialize(input: $stdin, output: $stdout, error_output: $stderr, remote: nil, registry: {})
@verbose = @dry_run = false @verbose = @dry_run = false
@input = input @input = input
@output = output @output = output
@remote = remote @error_output = error_output
@registry = registry @remote = remote
@registry = registry
end end
def remote def remote

View File

@ -24,13 +24,17 @@ module Producer
@fs ||= Remote::FS.new(session.sftp.connect) @fs ||= Remote::FS.new(session.sftp.connect)
end end
def execute(command, output = '') def execute(command, output = '', error_output = '')
channel = session.open_channel do |channel| channel = session.open_channel do |channel|
channel.exec command do |ch, success| channel.exec command do |ch, success|
ch.on_data do |c, data| ch.on_data do |c, data|
output << data output << data
end end
ch.on_extended_data do |c, type, data|
error_output << data
end
ch.on_request 'exit-status' do |c, data| ch.on_request 'exit-status' do |c, data|
exit_status = data.read_long exit_status = data.read_long
fail RemoteCommandExecutionError, command if exit_status != 0 fail RemoteCommandExecutionError, command if exit_status != 0

View File

@ -6,13 +6,18 @@ module Producer
fail 'no session for mock remote!' fail 'no session for mock remote!'
end end
def execute(command, output = '') def execute(command, output = '', error_output = '')
tokens = command.split tokens = command.gsub(/\d?>.*/, '').split
program = tokens.shift program = tokens.shift
case program case program
when 'echo' when 'echo'
output << tokens.join(' ') << "\n" out = tokens.join(' ') << "\n"
if command =~ />&2\z/
error_output << out
else
output << out
end
when 'true' when 'true'
output << '' output << ''
when 'false' when 'false'

View File

@ -19,6 +19,15 @@ module Producer::Core
sh.apply sh.apply
expect(output).to eq "#{command_args}\n" expect(output).to eq "#{command_args}\n"
end end
context 'when content is written to standard error' do
let(:command) { "echo #{command_args} >&2" }
it 'writes errors to given error stream' do
sh.apply
expect(error_output).to eq "#{command_args}\n"
end
end
end end
end end
end end

View File

@ -12,7 +12,9 @@ module Producer::Core
let(:stdout) { StringIO.new } let(:stdout) { StringIO.new }
let(:stderr) { StringIO.new } let(:stderr) { StringIO.new }
subject(:cli) { CLI.new(arguments, stdin: stdin, stdout: stdout) } subject(:cli) { described_class.new(
arguments,
stdin: stdin, stdout: stdout, stderr: stderr) }
describe '.run!' do describe '.run!' do
let(:cli) { double('cli').as_null_object } let(:cli) { double('cli').as_null_object }
@ -118,6 +120,10 @@ module Producer::Core
it 'assigns CLI stdout as the env output' do it 'assigns CLI stdout as the env output' do
expect(cli.env.output).to be stdout expect(cli.env.output).to be stdout
end end
it 'assigns CLI stderr as the env error output' do
expect(cli.env.error_output).to be stderr
end
end end
describe '#parse_arguments!' do describe '#parse_arguments!' do

View File

@ -10,6 +10,10 @@ module Producer::Core
expect(env.input).to be $stdin expect(env.input).to be $stdin
end end
it 'assigns $stderr as the default error output' do
expect(env.error_output).to be $stderr
end
it 'assigns no default target' do it 'assigns no default target' do
expect(env.target).not_to be expect(env.target).not_to be
end end
@ -51,6 +55,15 @@ module Producer::Core
end end
end end
context 'when error output is given as argument' do
let(:error_output) { StringIO.new }
subject(:env) { described_class.new(error_output: error_output) }
it 'assigns the given error output' do
expect(env.error_output).to be error_output
end
end
context 'when remote is given as argument' do context 'when remote is given as argument' do
let(:remote) { double 'remote' } let(:remote) { double 'remote' }
subject(:env) { described_class.new(remote: remote) } subject(:env) { described_class.new(remote: remote) }

View File

@ -106,6 +106,16 @@ module Producer::Core
expect(output.string).to eq arguments expect(output.string).to eq arguments
end end
it 'writes command error output to provided error output' do
error_output = StringIO.new
story_with_new_channel do |ch|
ch.sends_exec command
ch.gets_extended_data arguments
end
remote.execute command, output, error_output
expect(error_output.string).to eq arguments
end
context 'when command execution fails' do context 'when command execution fails' do
before do before do
story_with_new_channel do |ch| story_with_new_channel do |ch|

View File

@ -29,6 +29,12 @@ module Producer::Core
end end
end end
describe '#error_output' do
it 'returns env error output' do
expect(action.error_output).to be env.error_output
end
end
describe '#remote' do describe '#remote' do
it 'returns env remote' do it 'returns env remote' do
expect(action.remote).to be env.remote expect(action.remote).to be env.remote

View File

@ -9,6 +9,10 @@ module TestEnvHelpers
env.output.string env.output.string
end end
def error_output
env.error_output.string
end
def remote_fs def remote_fs
env.remote.fs env.remote.fs
end end
@ -17,14 +21,18 @@ module TestEnvHelpers
opts = { expected_from: caller.first } opts = { expected_from: caller.first }
RSpec::Mocks RSpec::Mocks
.expect_message(env.remote, :execute, opts) .expect_message(env.remote, :execute, opts)
.with(command, env.output) .with(command, env.output, env.error_output)
end end
private private
def build_env def build_env
Producer::Core::Env.new(output: StringIO.new, remote: build_remote) Producer::Core::Env.new(
output: StringIO.new,
error_output: StringIO.new,
remote: build_remote
)
end end
def build_remote def build_remote