lib/sippy_cup/runner.rb in sippy_cup-0.2.3 vs lib/sippy_cup/runner.rb in sippy_cup-0.3.0
- old
+ new
@@ -1,75 +1,173 @@
-require 'yaml'
require 'logger'
-require 'active_support/core_ext/hash'
+#
+# Service object to oversee the execution of a Scenario
+#
module SippyCup
class Runner
attr_accessor :sipp_pid
- attr_accessor :logger
- def initialize(opts = {})
- @options = ActiveSupport::HashWithIndifferentAccess.new opts
+ #
+ # Create a runner from a scenario
+ #
+ # @param [Scenario, XMLScenario] scenario The scenario to execute
+ # @param [Hash] opts Options to modify the runner
+ # @option opts [optional, true, false] :full_sipp_output Whether or not to copy SIPp's stdout/stderr to the parent process. Defaults to true.
+ # @option opts [optional, Logger] :logger A logger to use in place of the internal logger to STDOUT.
+ # @option opts [optional, String] :command The command to execute. This is mostly available for testing.
+ #
+ def initialize(scenario, opts = {})
+ @scenario = scenario
+ @scenario_options = @scenario.scenario_options
+
+ defaults = { full_sipp_output: true }
+ @options = defaults.merge(opts)
+
+ @command = @options[:command]
@logger = @options[:logger] || Logger.new(STDOUT)
end
- def compile
- raise ArgumentError, "Must provide scenario steps" unless @options[:steps]
+ # Runs the loaded scenario using SIPp
+ #
+ # @raises Errno::ENOENT when the SIPp executable cannot be found
+ # @raises SippyCup::ExitOnInternalCommand when SIPp exits on an internal command. Calls may have been processed
+ # @raises SippyCup::NoCallsProcessed when SIPp exit normally, but has processed no calls
+ # @raises SippyCup::FatalError when SIPp encounters a fatal failure
+ # @raises SippyCup::FatalSocketBindingError when SIPp fails to bind to the specified socket
+ # @raises SippyCup::SippGenericError when SIPp encounters another type of error
+ #
+ # @return Boolean true if execution succeeded without any failed calls, false otherwise
+ #
+ def run
+ @input_files = @scenario.to_tmpfiles
- scenario_opts = {source: @options[:source], destination: @options[:destination]}
- scenario_opts[:filename] = @options[:filename] if @options[:filename]
- scenario = SippyCup::Scenario.new @options[:name].titleize, scenario_opts
- @options[:steps].each do |step|
- instruction, arg = step.split ' ', 2
- if arg && !arg.empty?
- # Strip leading/trailing quotes if present
- arg.gsub!(/^'|^"|'$|"$/, '')
- scenario.send instruction.to_sym, arg
- else
- scenario.send instruction
+ @logger.info "Preparing to run SIPp command: #{command}"
+
+ exit_status, stderr_buffer = execute_with_redirected_streams
+
+ final_result = process_exit_status exit_status, stderr_buffer
+
+ if final_result
+ @logger.info "Test completed successfully!"
+ else
+ @logger.info "Test completed successfully but some calls failed."
+ end
+ @logger.info "Statistics logged at #{File.expand_path @scenario_options[:stats_file]}" if @scenario_options[:stats_file]
+
+ final_result
+ ensure
+ cleanup_input_files
+ end
+
+ #
+ # Tries to stop SIPp by killing the target PID
+ #
+ # @raises Errno::ESRCH when the PID does not correspond to a known process
+ # @raises Errno::EPERM when the process referenced by the PID cannot be killed
+ #
+ def stop
+ Process.kill "KILL", @sipp_pid if @sipp_pid
+ end
+
+ private
+
+ def command
+ @command ||= begin
+ command = "sudo sipp"
+ command_options.each_pair do |key, value|
+ command << (value ? " -#{key} #{value}" : " -#{key}")
end
+ command << " #{@scenario_options[:destination]}"
end
- scenario.compile!
end
- def prepare_command
- [:scenario, :source, :destination, :max_concurrent, :calls_per_second, :number_of_calls].each do |arg|
- raise ArgumentError, "Must provide #{arg}!" unless @options[arg]
+ def command_options
+ options = {
+ i: @scenario_options[:source],
+ p: @scenario_options[:source_port] || '8836',
+ sf: @input_files[:scenario].path,
+ l: @scenario_options[:max_concurrent],
+ m: @scenario_options[:number_of_calls],
+ r: @scenario_options[:calls_per_second],
+ s: @scenario_options[:from_user] || '1'
+ }
+
+ options[:mp] = @scenario_options[:media_port] if @scenario_options[:media_port]
+
+ if @scenario_options[:stats_file]
+ options[:trace_stat] = nil
+ options[:stf] = @scenario_options[:stats_file]
+ options[:fd] = @scenario_options[:stats_interval] || 1
end
- command = "sudo sipp"
- source_port = @options[:source_port] || '8836'
- sip_user = @options[:sip_user] || '1'
- command << " -i #{@options[:source]} -p #{source_port} -sf #{File.expand_path @options[:scenario]}.xml"
- command << " -l #{@options[:max_concurrent]} -m #{@options[:number_of_calls]} -r #{@options[:calls_per_second]}"
- command << " -s #{sip_user}"
- if @options[:stats_file]
- stats_interval = @options[:stats_interval] || 1
- command << " -trace_stat -stf #{@options[:stats_file]} -fd #{stats_interval}"
+
+ if @scenario_options[:transport_mode]
+ options[:t] = @scenario_options[:transport_mode]
end
- command << " -inf #{@options[:scenario_variables]}" if @options[:scenario_variables]
- command << " #{@options[:destination]}"
- command << " > /dev/null 2>&1" unless @options[:full_sipp_output]
- command
+
+ if @scenario_options[:scenario_variables]
+ options[:inf] = @scenario_options[:scenario_variables]
+ end
+
+ options
end
- def run
- command = prepare_command
- @logger.info "Preparing to run SIPp command: #{command}"
+ def execute_with_redirected_streams
+ rd, wr = IO.pipe
+ stdout_target = @options[:full_sipp_output] ? $stdout : '/dev/null'
- begin
- @sipp_pid = spawn command
- Process.wait @sipp_pid
- rescue Exception => e
- raise RuntimeError, "Command #{command} failed"
+ @sipp_pid = spawn command, err: wr, out: stdout_target
+
+ stderr_buffer = String.new
+
+ Thread.new do
+ wr.close
+ until rd.eof?
+ buffer = rd.readpartial(1024).strip
+ stderr_buffer += buffer
+ $stderr << buffer if @options[:full_sipp_output]
+ end
end
- @logger.info "Test completed successfully!"
- @logger.info "Statistics logged at #{File.expand_path @options[:stats_file]}" if @options[:stats_file]
+ exit_status = Process.wait2 @sipp_pid.to_i
+
+ rd.close
+
+ [exit_status, stderr_buffer]
end
- def stop
- Process.kill "KILL", @sipp_pid if @sipp_pid
- rescue Exception => e
- raise RuntimeError, "Killing #{@sipp_pid} failed"
+ def process_exit_status(process_status, error_message = nil)
+ exit_code = process_status[1].exitstatus
+ case exit_code
+ when 0
+ true
+ when 1
+ false
+ when 97
+ raise SippyCup::ExitOnInternalCommand, error_message
+ when 99
+ raise SippyCup::NoCallsProcessed, error_message
+ when 255
+ raise SippyCup::FatalError, error_message
+ when 254
+ raise SippyCup::FatalSocketBindingError, error_message
+ else
+ raise SippyCup::SippGenericError, error_message
+ end
end
+
+ def cleanup_input_files
+ @input_files.each_pair do |key, value|
+ value.close
+ value.unlink
+ end
+ end
end
+
+ # The corresponding SIPp error code is listed after the exception
+ class Error < StandardError; end
+ class ExitOnInternalCommand < Error; end # 97
+ class NoCallsProcessed < Error; end # 99
+ class FatalError < Error; end # -1
+ class FatalSocketBindingError < Error; end # -2
+ class SippGenericError < Error; end # 255 and undocumented errors
end