diff --git a/lib/rubyshell/debugger.rb b/lib/rubyshell/debugger.rb index 6313f74..b208499 100644 --- a/lib/rubyshell/debugger.rb +++ b/lib/rubyshell/debugger.rb @@ -5,27 +5,41 @@ module Debugger class << self def run_wrapper(command, debug: nil) if debug || RubyShell.debug? - time_one = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = yield + begin + result = yield + rescue RubyShell::CommandError => e + time_two = Process.clock_gettime(Process::CLOCK_MONOTONIC) + log_command(command, time_two - time_one, e.status, e.stdout, e.stderr) + raise + end time_two = Process.clock_gettime(Process::CLOCK_MONOTONIC) - RubyShell.log(<<~TEXT - \nExecuted: #{command.to_shell.chomp} - Duration: #{format("%.6f", time_two - time_one)}s - Pid: #{result.metadata[:exit_status].pid} - Exit code: #{result.metadata[:exit_status].to_i} - Stdout: #{result.to_s.inspect} - TEXT - ) + log_command(command, time_two - time_one, result.metadata[:exit_status], result.to_s, "") result else yield end end + + private + + def log_command(command, duration, status, stdout, stderr) + pid = status.pid if status.respond_to?(:pid) + + RubyShell.log(<<~TEXT + \nExecuted: #{command.to_shell.chomp} + Duration: #{format("%.6f", duration)}s + Pid: #{pid} + Exit code: #{status.respond_to?(:exitstatus) ? status.exitstatus : status.to_i} + Stdout: #{stdout.inspect} + Stderr: #{stderr.inspect} + TEXT + ) + end end end -end +end \ No newline at end of file diff --git a/lib/rubyshell/error.rb b/lib/rubyshell/error.rb index 5f6120e..8b1c1e2 100644 --- a/lib/rubyshell/error.rb +++ b/lib/rubyshell/error.rb @@ -2,7 +2,7 @@ module RubyShell class CommandError < StandardError - attr_reader :command, :status + attr_reader :command, :stdout, :stderr, :status def initialize(command:, stdout: "", stderr: "", status: "", message: nil) @command = command diff --git a/spec/debugger_spec.rb b/spec/debugger_spec.rb index 900f202..990eef6 100644 --- a/spec/debugger_spec.rb +++ b/spec/debugger_spec.rb @@ -41,6 +41,12 @@ expect(log_output.join).to include("Stdout: \"#{expected_output}\"") end + + it "logs the stderr" do + subject_method + + expect(log_output.join).to include("Stderr: \"\"") + end end RSpec.shared_examples "a silent command" do @@ -83,7 +89,73 @@ def subject_method def subject_method sh.echo("hello", _debug: true) end + + def execute_failed_debug_command(stdout: nil, stderr: "bad") + script_parts = [] + script_parts << "STDOUT.write(%q{#{stdout}})" if stdout + script_parts << "STDERR.write(%q{#{stderr}})" if stderr + script_parts << "exit 1" + + sh.ruby("-e", "\"#{script_parts.join("; ")}\"", _debug: true) + end + it_behaves_like "a logged command", "hello" + + it "reraises failed commands" do + expect do + execute_failed_debug_command + end.to raise_error(RubyShell::CommandError) + end + + context "when a failed debug command is rescued" do + before do + begin + execute_failed_debug_command + rescue RubyShell::CommandError + nil + end + end + + it "logs failed command exit code before reraising" do + expect(log_output.join).to include("Exit code: 1") + end + + it "logs failed command stdout before reraising" do + expect(log_output.join).to include("Stdout: \"\"") + end + + it "logs failed command stderr before reraising" do + expect(log_output.join).to include("Stderr: \"bad\"") + end + end + + context "when the caller rescues the command error" do + subject(:rescued_error) do + execute_failed_debug_command(stdout: "out", stderr: "bad") + rescue RubyShell::CommandError => e + e + end + + it "keeps the rescued error type available" do + expect(rescued_error).to be_a(RubyShell::CommandError) + end + + it "keeps the rescued error command available" do + expect(rescued_error.command.to_s).to include("STDOUT.write") + end + + it "keeps the rescued error stdout available" do + expect(rescued_error.stdout).to eq("out") + end + + it "keeps the rescued error stderr available" do + expect(rescued_error.stderr).to eq("bad") + end + + it "keeps the rescued error status available" do + expect(rescued_error.status.exitstatus).to eq(1) + end + end end context "when debug mode is enabled globally" do