lib/sippy_cup/scenario.rb in sippy_cup-0.3.0 vs lib/sippy_cup/scenario.rb in sippy_cup-0.4.0
- old
+ new
@@ -77,18 +77,21 @@
# @option options [String] :filename The name of the files to be saved to disk.
# @option options [String] :source The source IP/hostname with which to invoke SIPp.
# @option options [String, Numeric] :source_port The source port to bind SIPp to (defaults to 8836).
# @option options [String] :destination The target system at which to direct traffic.
# @option options [String] :from_user The SIP user from which traffic should appear.
+ # @option options [String] :to_user The SIP user to send requests to.
# @option options [Integer] :media_port The RTCP (media) port to bind to locally.
# @option options [String, Numeric] :max_concurrent The maximum number of concurrent calls to execute.
# @option options [String, Numeric] :number_of_calls The maximum number of calls to execute in the test run.
# @option options [String, Numeric] :calls_per_second The rate at which to initiate calls.
# @option options [String] :stats_file The path at which to dump statistics.
# @option options [String, Numeric] :stats_interval The interval (in seconds) at which to dump statistics (defaults to 1s).
# @option options [String] :transport_mode The transport mode over which to direct SIP traffic.
+ # @option options [String] :dtmf_mode The output DTMF mode, either rfc2833 (default) or info.
# @option options [String] :scenario_variables A path to a CSV file of variables to be interpolated with the scenario at runtime.
+ # @option options [Hash] :options A collection of options to pass through to SIPp, as key-value pairs. In cases of value-less options (eg -trace_err), specify a nil value.
# @option options [Array<String>] :steps A collection of steps
#
# @yield [scenario] Builder block to construct scenario
# @yieldparam [Scenario] scenario the initialized scenario instance
#
@@ -97,10 +100,12 @@
@scenario_options = args.merge name: name
@filename = args[:filename] || name.downcase.gsub(/\W+/, '_')
@filename = File.expand_path @filename, Dir.pwd
@media = Media.new '127.0.0.255', 55555, '127.255.255.255', 5060
+ @message_variables = 0
+ @media_nodes = []
@errors = []
instance_eval &block if block_given?
end
@@ -196,65 +201,75 @@
#
# Sets an expectation for a SIP 100 message from the remote party
#
# @param [Hash] opts A set of options to modify the expectation
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to true.
#
def receive_trying(opts = {})
handle_response 100, opts
end
alias :receive_100 :receive_trying
#
# Sets an expectation for a SIP 180 message from the remote party
#
# @param [Hash] opts A set of options to modify the expectation
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to true.
#
def receive_ringing(opts = {})
handle_response 180, opts
end
alias :receive_180 :receive_ringing
#
# Sets an expectation for a SIP 183 message from the remote party
#
# @param [Hash] opts A set of options to modify the expectation
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to true.
#
def receive_progress(opts = {})
handle_response 183, opts
end
alias :receive_183 :receive_progress
#
# Sets an expectation for a SIP 200 message from the remote party
+ # as well as storing the record set and the response time duration
#
# @param [Hash] opts A set of options to modify the expectation
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to false.
#
def receive_answer(opts = {})
options = {
- response: 200,
rrs: true, # Record Record Set: Make the Route headers available via [route] later
rtd: true # Response Time Duration: Record the response time
}
- recv options.merge(opts)
+ receive_200 options.merge(opts)
end
- alias :receive_200 :receive_answer
#
+ # Sets an expectation for a SIP 200 message from the remote party
+ #
+ # @param [Hash] opts A set of options to modify the expectation
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to false.
+ #
+ def receive_200(opts = {})
+ recv({ response: 200 }.merge(opts))
+ end
+ alias :receive_200 :receive_ok
+
+ #
# Shortcut that sets expectations for optional SIP 100, 180 and 183, followed by a required 200.
#
# @param [Hash] opts A set of options to modify the expectations
#
def wait_for_answer(opts = {})
- receive_trying({optional: true}.merge opts)
- receive_ringing({optional: true}.merge opts)
- receive_progress({optional: true}.merge opts)
+ receive_trying opts
+ receive_ringing opts
+ receive_progress opts
receive_answer opts
end
#
# Acknowledge a received answer message (SIP 200) and start media playback
@@ -265,11 +280,11 @@
msg = <<-BODY
ACK [next_url] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
-[last_To:]
+To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: [cseq] ACK
Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
Max-Forwards: 100
User-Agent: #{USER_AGENT}
@@ -305,29 +320,87 @@
def send_digits(digits)
delay = (0.250 * MSEC).to_i # FIXME: Need to pass this down to the media layer
digits.split('').each do |digit|
raise ArgumentError, "Invalid DTMF digit requested: #{digit}" unless VALID_DTMF.include? digit
- @media << "dtmf:#{digit}"
- @media << "silence:#{delay}"
+ case @dtmf_mode
+ when :rfc2833
+ @media << "dtmf:#{digit}"
+ @media << "silence:#{delay}"
+ when :info
+ info = <<-INFO
+
+INFO [next_url] SIP/2.0
+Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
+To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+Call-ID: [call_id]
+CSeq: [cseq] INFO
+Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
+Max-Forwards: 100
+User-Agent: #{USER_AGENT}
+[routes]
+Content-Length: [len]
+Content-Type: application/dtmf-relay
+
+Signal=#{digit}
+Duration=#{delay}
+ INFO
+ send info
+ recv response: 200
+ pause delay
+ end
end
- pause delay * 2 * digits.size
+
+ if @dtmf_mode == :rfc2833
+ pause delay * 2 * digits.size
+ end
end
#
+ # Expect to receive a MESSAGE message
+ #
+ # @param [String] regexp A regular expression (as a String) to match the message body against
+ #
+ def receive_message(regexp = nil)
+ recv = Nokogiri::XML::Node.new 'recv', doc
+ recv['request'] = 'MESSAGE'
+ scenario_node << recv
+
+ if regexp
+ action = Nokogiri::XML::Node.new 'action', doc
+ ereg = Nokogiri::XML::Node.new 'ereg', doc
+ ref = Nokogiri::XML::Node.new 'Reference', doc
+
+ ereg['regexp'] = regexp
+ ereg['search_in'] = 'body'
+ ereg['check_it'] = true
+
+ var = "message_#{@message_variables += 1}"
+ ereg['assign_to'] = ref['variables'] = var
+
+ action << ereg
+ recv << action
+ scenario_node << ref
+ end
+
+ okay
+ end
+
+ #
# Send a BYE message
#
# @param [Hash] opts A set of options to modify the message parameters
#
def send_bye(opts = {})
msg = <<-MSG
BYE [next_url] SIP/2.0
-[last_Via:]
+Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
-[last_To:]
-[last_Call-ID]
+To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+Call-ID: [call_id]
CSeq: [cseq] BYE
Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
Max-Forwards: 100
User-Agent: #{USER_AGENT}
Content-Length: 0
@@ -344,15 +417,15 @@
def receive_bye(opts = {})
recv opts.merge request: 'BYE'
end
#
- # Acknowledge a received BYE message
+ # Acknowledge the last request
#
# @param [Hash] opts A set of options to modify the message parameters
#
- def ack_bye(opts = {})
+ def okay(opts = {})
msg = <<-ACK
SIP/2.0 200 OK
[last_Via:]
[last_From:]
@@ -365,10 +438,11 @@
Content-Length: 0
[routes]
ACK
send msg, opts
end
+ alias :ack_bye :okay
#
# Shortcut to set an expectation for a BYE and acknowledge it when received
#
# @param [Hash] opts A set of options to modify the expectation
@@ -380,18 +454,34 @@
#
# Dump the scenario to a SIPp XML string
#
# @return [String] the SIPp XML scenario
- def to_xml
- doc.to_xml
+ def to_xml(options = {})
+ pcap_path = options[:pcap_path]
+ docdup = doc.dup
+
+ # Not removing in reverse would most likely remove the wrong
+ # nodes because of changing indices.
+ @media_nodes.reverse.each do |nop|
+ nopdup = docdup.xpath(nop.path)
+
+ if pcap_path.nil? or @media.empty?
+ nopdup.remove
+ else
+ exec = nopdup.xpath("./action/exec").first
+ exec['play_pcap_audio'] = pcap_path
+ end
+ end
+
+ docdup.to_xml
end
#
# Compile the scenario and its media to disk
#
- # Writes the SIPp scenario file to disk at {filename}.xml, and the PCAP media to {filename}.pcap.
+ # Writes the SIPp scenario file to disk at {filename}.xml, and the PCAP media to {filename}.pcap if applicable.
# {filename} is taken from the :filename option when creating the scenario, or falls back to a down-snake-cased version of the scenario name.
#
# @return [String] the path to the resulting scenario file
#
# @example Export a scenario to a specified filename
@@ -401,42 +491,46 @@
# @example Export a scenario to a calculated filename
# scenario = Scenario.new 'Test Scenario'
# scenario.compile! # Leaves files at test_scenario.xml and test_scenario.pcap
#
def compile!
+ unless @media.empty?
+ print "Compiling media to #{@filename}.pcap..."
+ compile_media.to_file filename: "#{@filename}.pcap"
+ puts "done."
+ end
+
scenario_filename = "#{@filename}.xml"
print "Compiling scenario to #{scenario_filename}..."
File.open scenario_filename, 'w' do |file|
- file.write doc.to_xml
+ file.write to_xml(:pcap_path => "#{@filename}.pcap")
end
puts "done."
- print "Compiling media to #{@filename}.pcap..."
- compile_media.to_file filename: "#{@filename}.pcap"
- puts "done."
-
scenario_filename
end
#
- # Write compiled Scenario XML and PCAP media to tempfiles.
+ # Write compiled Scenario XML and PCAP media (if applicable) to tempfiles.
#
# These will automatically be closed and deleted once they have gone out of scope, and can be used to execute the scenario without leaving stuff behind.
#
# @return [Hash<Symbol => Tempfile>] handles to created Tempfiles at :scenario and :media
#
# @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/Tempfile.html
#
def to_tmpfiles
+ unless @media.empty?
+ media_file = Tempfile.new 'media'
+ media_file.write compile_media.to_s
+ media_file.rewind
+ end
+
scenario_file = Tempfile.new 'scenario'
- scenario_file.write to_xml
+ scenario_file.write to_xml(:pcap_path => media_file.try(:path))
scenario_file.rewind
- media_file = Tempfile.new 'media'
- media_file.write compile_media.to_s
- media_file.rewind
-
{scenario: scenario_file, media: media_file}
end
private
@@ -450,23 +544,33 @@
end
def doc
@doc ||= begin
Nokogiri::XML::Builder.new do |xml|
- xml.scenario name: @scenario_options[:name]
+ xml.scenario name: @scenario_options[:name] do
+ @scenario_node = xml.parent
+ end
end.doc
end
end
def scenario_node
- @scenario_node = doc.xpath('//scenario').first
+ doc
+ @scenario_node
end
def parse_args(args)
raise ArgumentError, "Must include source IP:PORT" unless args.has_key? :source
raise ArgumentError, "Must include destination IP:PORT" unless args.has_key? :destination
+ if args[:dtmf_mode]
+ @dtmf_mode = args[:dtmf_mode].to_sym
+ raise ArgumentError, "dtmf_mode must be rfc2833 or info" unless [:rfc2833, :info].include?(@dtmf_mode)
+ else
+ @dtmf_mode = :rfc2833
+ end
+
@from_addr, @from_port = args[:source].split ':'
@to_addr, @to_port = args[:destination].split ':'
@from_user = args[:from_user] || "sipp"
end
@@ -509,15 +613,16 @@
Content-Length: 0
AUTH
end
def start_media
- nop = Nokogiri::XML::Node.new 'nop', doc
- action = Nokogiri::XML::Node.new 'action', doc
- nop << action
- exec = Nokogiri::XML::Node.new 'exec', doc
- exec['play_pcap_audio'] = "#{@filename}.pcap"
- action << exec
+ nop = doc.create_element('nop') { |nop|
+ nop << doc.create_element('action') { |action|
+ action << doc.create_element('exec')
+ }
+ }
+
+ @media_nodes << nop
scenario_node << nop
end
def pause(msec)
pause = Nokogiri::XML::Node.new 'pause', doc