lib/process_executer/result.rb
# frozen_string_literal: true require 'delegate' module ProcessExecuter # A decorator for Process::Status that adds the following attributes: # # * `command`: the command that was used to spawn the process # * `options`: the options that were used to spawn the process # * `elapsed_time`: the secs the command ran # * `stdout`: the captured stdout output # * `stderr`: the captured stderr output # * `timed_out?`: true if the process timed out # # @api public # class Result < SimpleDelegator # Create a new Result object # # @param status [Process::Status] the status to delegate to # @param command [Array] the command that was used to spawn the process # @param options [ProcessExecuter::Options] the options that were used to spawn the process # @param timed_out [Boolean] true if the process timed out # @param elapsed_time [Numeric] the secs the command ran # # @example # command = ['sleep 1'] # options = ProcessExecuter::Options.new(timeout_after: 0.5) # start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) # timed_out = false # status = nil # pid = Process.spawn(*command, **options.spawn_options) # Timeout.timeout(options.timeout_after) do # _pid, status = Process.wait2(pid) # rescue Timeout::Error # Process.kill('KILL', pid) # timed_out = true # _pid, status = Process.wait2(pid) # end # elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time # # ProcessExecuter::Result.new(status, command:, options:, timed_out:, elapsed_time:) # # @api public # def initialize(status, command:, options:, timed_out:, elapsed_time:) super(status) @command = command @options = options @timed_out = timed_out @elapsed_time = elapsed_time end # The command that was used to spawn the process # @see Process.spawn # @example # result.command #=> [{ 'GIT_DIR' => '/path/to/repo' }, 'git', 'status'] # @return [Array] attr_reader :command # The options that were used to spawn the process # @see Process.spawn # @example # result.options #=> { chdir: '/path/to/repo', timeout_after: 0.5 } # @return [Hash] # @api public attr_reader :options # The secs the command ran # @example # result.elapsed_time #=> 10 # @return [Numeric, nil] # @api public attr_reader :elapsed_time # @!attribute [r] timed_out? # True if the process timed out and was sent the SIGKILL signal # @example # result = ProcessExecuter.spawn('sleep 10', timeout_after: 0.01) # result.timed_out? # => true # @return [Boolean] # def timed_out? = @timed_out # Overrides the default success? method to return nil if the process timed out # # This is because when a timeout occurs, Windows will still return true. # # @example # result = ProcessExecuter.spawn('sleep 10', timeout_after: 0.01) # result.success? # => nil # @return [true, nil] # def success? return nil if timed_out? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition super end # Return a string representation of the result # @example # result.to_s #=> "pid 70144 SIGKILL (signal 9) timed out after 10s" # @return [String] def to_s "#{super}#{timed_out? ? " timed out after #{options.timeout_after}s" : ''}" end # Return the captured stdout output # # This output is only returned if the `:out` option was set to a # `ProcessExecuter::MonitoredPipe` that includes a writer that implements `#string` # method (e.g. a StringIO). # # @example # # Note that `ProcessExecuter.run` will wrap the given out: object in a # # ProcessExecuter::MonitoredPipe # result = ProcessExecuter.run('echo hello': out: StringIO.new) # result.stdout #=> "hello\n" # # @return [String, nil] # def stdout Array(options.out).each do |pipe| next unless pipe.is_a?(ProcessExecuter::MonitoredPipe) pipe.writers.each do |writer| return writer.string if writer.respond_to?(:string) end end nil end # Return the captured stderr output # # This output is only returned if the `:err` option was set to a # `ProcessExecuter::MonitoredPipe` that includes a writer that implements `#string` # method (e.g. a StringIO). # # @example # # Note that `ProcessExecuter.run` will wrap the given err: object in a # # ProcessExecuter::MonitoredPipe # result = ProcessExecuter.run('echo ERROR 1>&2', err: StringIO.new) # resuilt.stderr #=> "ERROR\n" # # @return [String, nil] # def stderr Array(options.err).each do |pipe| next unless pipe.is_a?(ProcessExecuter::MonitoredPipe) pipe.writers.each do |writer| return writer.string if writer.respond_to?(:string) end end nil end end end