diff --git a/features/actions/sh.feature b/features/actions/sh.feature index 8c8c35e..c796e27 100644 --- a/features/actions/sh.feature +++ b/features/actions/sh.feature @@ -37,3 +37,15 @@ Feature: `sh' task action """ When I execute the recipe Then the output must not contain "after_fail" + + Scenario: prints command when execution fail + Given a recipe with: + """ + target 'some_host.test' + + task :some_task do + sh '\false' + end + """ + When I execute the recipe + Then the output must match /\A\w+Error:\s+\\false/ diff --git a/lib/producer/core/cli.rb b/lib/producer/core/cli.rb index b097de9..947a722 100644 --- a/lib/producer/core/cli.rb +++ b/lib/producer/core/cli.rb @@ -5,18 +5,22 @@ module Producer USAGE = "Usage: #{File.basename $0} [-v] [-n] recipe_file".freeze - EX_USAGE = 64 + EX_USAGE = 64 + EX_SOFTWARE = 70 class << self def run!(arguments, output: $stderr) + cli = new(arguments) begin - cli = new(arguments) cli.parse_arguments! + cli.run rescue ArgumentError output.puts USAGE exit EX_USAGE + rescue RuntimeError => e + output.puts "#{e.class.name.split('::').last}: #{e.message}" + exit EX_SOFTWARE end - cli.run end end diff --git a/lib/producer/core/errors.rb b/lib/producer/core/errors.rb index 6710882..e3eb8b3 100644 --- a/lib/producer/core/errors.rb +++ b/lib/producer/core/errors.rb @@ -1,7 +1,8 @@ module Producer module Core Error = Class.new(StandardError) + RuntimeError = Class.new(RuntimeError) ConditionNotMetError = Class.new(Error) - RemoteCommandExecutionError = Class.new(Error) + RemoteCommandExecutionError = Class.new(RuntimeError) end end diff --git a/lib/producer/core/remote.rb b/lib/producer/core/remote.rb index c654e72..d875e91 100644 --- a/lib/producer/core/remote.rb +++ b/lib/producer/core/remote.rb @@ -32,7 +32,7 @@ module Producer ch.on_request 'exit-status' do |c, data| exit_status = data.read_long - raise RemoteCommandExecutionError if exit_status != 0 + raise RemoteCommandExecutionError, command if exit_status != 0 end end end diff --git a/spec/producer/core/cli_spec.rb b/spec/producer/core/cli_spec.rb index e5a324a..e837102 100644 --- a/spec/producer/core/cli_spec.rb +++ b/spec/producer/core/cli_spec.rb @@ -17,35 +17,33 @@ module Producer::Core let(:output) { StringIO.new } subject(:run) { described_class.run! arguments, output: output } + before { allow(described_class).to receive(:new) { cli } } + it 'builds a new CLI with given arguments' do - expect(described_class) - .to receive(:new).with(arguments).and_call_original + expect(described_class).to receive(:new).with(arguments) run end it 'runs the CLI' do - allow(described_class).to receive(:new) { cli } expect(cli).to receive :run run end it 'parses CLI arguments' do - allow(described_class).to receive(:new) { cli } expect(cli).to receive :parse_arguments! run end context 'when an argument error is raised' do before do - allow(described_class).to receive(:new) { cli } allow(cli).to receive(:parse_arguments!) .and_raise described_class::ArgumentError end it 'exits with a return status of 64' do - expect { run }.to raise_error(SystemExit) { |e| + expect { run }.to raise_error(SystemExit) do |e| expect(e.status).to eq 64 - } + end end it 'prints the usage' do @@ -53,6 +51,24 @@ module Producer::Core expect(output.string).to match /\AUsage: .+/ end end + + context 'when a runtime error is raised' do + before do + allow(cli).to receive(:run) + .and_raise RuntimeError, 'some message' + end + + it 'exits with a return status of 70' do + expect { run }.to raise_error(SystemExit) do |e| + expect(e.status).to eq 70 + end + end + + it 'prints exception name and message' do + trap_exit { run } + expect(output.string).to match /\ARuntimeError: some message/ + end + end end describe '#initialize' do diff --git a/spec/producer/core/remote_spec.rb b/spec/producer/core/remote_spec.rb index d0ceef7..97ad825 100644 --- a/spec/producer/core/remote_spec.rb +++ b/spec/producer/core/remote_spec.rb @@ -106,14 +106,23 @@ module Producer::Core expect(output.string).to eq arguments end - it 'raises an exception when the exit status code is not 0' do - story_with_new_channel do |ch| - ch.sends_exec command - ch.gets_data arguments - ch.gets_exit_status 1 + context 'when command execution fails' do + before do + story_with_new_channel do |ch| + ch.sends_exec command + ch.gets_data arguments + ch.gets_exit_status 1 + end + end + + it 'raises an exception' do + expect { remote.execute command } + .to raise_error(RemoteCommandExecutionError) + end + + it 'includes the command in the exception message' do + expect { remote.execute command }.to raise_error /#{command}/ end - expect { remote.execute command } - .to raise_error(RemoteCommandExecutionError) end end