Class: SippyCup::Scenario
- Inherits:
-
Object
- Object
- SippyCup::Scenario
- Defined in:
- lib/sippy_cup/scenario.rb
Overview
A representation of a SippyCup scenario from a manifest or created in code. Allows building a scenario from a set of basic primitives, and then exporting to SIPp scenario files, including the XML scenario and PCAP audio.
Constant Summary collapse
- USER_AGENT =
"SIPp/sippy_cup"
- VALID_DTMF =
%w{0 1 2 3 4 5 6 7 8 9 0 * # A B C D}.freeze
- MSEC =
1_000
- DEFAULT_RETRANS =
500
Instance Attribute Summary collapse
-
#errors ⇒ Array<Hash>
readonly
A collection of errors encountered while building the scenario.
-
#scenario_options ⇒ Hash
readonly
The options the scenario was created with, either from a manifest or passed as overrides.
Class Method Summary collapse
-
.from_manifest(manifest, options = {}) ⇒ SippyCup::Scenario
Build a scenario based on either a manifest string or a file handle.
Instance Method Summary collapse
-
#ack_answer(opts = {}) ⇒ Object
Acknowledge a received answer message and start media playback.
-
#answer(opts = {}) ⇒ Object
Helper method to answer an INVITE and expect the ACK.
-
#build(steps) ⇒ Object
Build the scenario steps provided.
-
#call_length_repartition(min, max, interval) ⇒ Object
Create partition table for Call Length.
-
#compile! ⇒ String
Compile the scenario and its media to disk.
-
#hangup(opts = {}) ⇒ Object
Shortcut to send a BYE and wait for the acknowledgement.
-
#initialize(name, args = {}) {|scenario| ... } ⇒ Scenario
constructor
Create a scenario instance.
-
#invite(opts = {}) ⇒ Object
Send an invite message.
-
#okay(opts = {}) ⇒ Object
(also: #ack_bye)
Acknowledge the last request.
- #receive_ack(opts = {}) ⇒ Object
-
#receive_answer(opts = {}) ⇒ Object
Sets an expectation for a SIP 200 message from the remote party as well as storing the record set and the response time duration.
-
#receive_bye(opts = {}) ⇒ Object
Expect to receive a BYE message.
-
#receive_invite(opts = {}) ⇒ Object
(also: #wait_for_call)
Expect to receive a SIP INVITE.
-
#receive_message(regexp = nil) ⇒ Object
Expect to receive a MESSAGE message.
-
#receive_ok(opts = {}, &block) ⇒ Object
(also: #receive_200)
Sets an expectation for a SIP 200 message from the remote party.
-
#receive_progress(opts = {}) ⇒ Object
(also: #receive_183)
Sets an expectation for a SIP 183 message from the remote party.
-
#receive_ringing(opts = {}) ⇒ Object
(also: #receive_180)
Sets an expectation for a SIP 180 message from the remote party.
-
#receive_trying(opts = {}) ⇒ Object
(also: #receive_100)
Sets an expectation for a SIP 100 message from the remote party.
-
#register(user, password = nil, opts = {}) ⇒ Object
Send a REGISTER message with the specified credentials.
-
#response_time_repartition(min, max, interval) ⇒ Object
Create partition table for Response Time.
-
#send_answer(opts = {}) ⇒ Object
Answer an incoming call.
-
#send_bye(opts = {}) ⇒ Object
Send a BYE message.
-
#send_digits(digits) ⇒ Object
Send DTMF digits.
-
#send_ringing(opts = {}) ⇒ Object
(also: #send_180)
Send a “180 Ringing” response.
-
#send_trying(opts = {}) ⇒ Object
(also: #send_100)
Send a “100 Trying” response.
-
#sleep(seconds) ⇒ Object
Insert a pause into the scenario and its media of the specified duration.
-
#to_tmpfiles ⇒ Hash<Symbol => Tempfile>
Write compiled Scenario XML and PCAP media (if applicable) to tempfiles.
-
#to_xml(options = {}) ⇒ String
Dump the scenario to a SIPp XML string.
-
#valid? ⇒ true, false
The validity of the scenario.
-
#wait_for_answer(opts = {}) ⇒ Object
Convenience method to wait for an answer from the called party.
-
#wait_for_hangup(opts = {}) ⇒ Object
Shortcut to set an expectation for a BYE and acknowledge it when received.
Constructor Details
#initialize(name, args = {}) {|scenario| ... } ⇒ Scenario
Create a scenario instance
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/sippy_cup/scenario.rb', line 103 def initialize(name, args = {}, &block) parse_args args @scenario_options = args.merge name: name @filename = args[:filename] || name.downcase.gsub(/\W+/, '_') @filename = File. @filename, Dir.pwd @media = nil @message_variables = 0 # Reference variables don't generate warnings/errors if unused in the scenario @reference_variables = Set.new @media_nodes = [] @errors = [] @adv_ip = args[:advertise_address] || "[local_ip]" instance_eval &block if block_given? end |
Instance Attribute Details
#errors ⇒ Array<Hash> (readonly)
Returns a collection of errors encountered while building the scenario.
72 73 74 |
# File 'lib/sippy_cup/scenario.rb', line 72 def errors @errors end |
#scenario_options ⇒ Hash (readonly)
Returns The options the scenario was created with, either from a manifest or passed as overrides.
69 70 71 |
# File 'lib/sippy_cup/scenario.rb', line 69 def @scenario_options end |
Class Method Details
.from_manifest(manifest, options = {}) ⇒ SippyCup::Scenario
Build a scenario based on either a manifest string or a file handle. Manifests are supplied in YAML format. All manifest keys can be overridden by passing in a Hash of corresponding values.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/sippy_cup/scenario.rb', line 49 def self.from_manifest(manifest, = {}) args = ActiveSupport::HashWithIndifferentAccess.new(Psych.safe_load(manifest)).merge input_name = .has_key?(:input_filename) ? File.basename([:input_filename]).gsub(/\.ya?ml/, '') : nil name = args.delete(:name) || input_name || 'My Scenario' scenario = if args[:scenario] media = args.has_key?(:media) ? File.read(args[:media], mode: 'rb') : nil SippyCup::XMLScenario.new name, File.read(args[:scenario]), media, args else steps = args.delete :steps scenario = Scenario.new name, args scenario.build steps scenario end scenario end |
Instance Method Details
#ack_answer(opts = {}) ⇒ Object
Acknowledge a received answer message and start media playback
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/sippy_cup/scenario.rb', line 445 def ack_answer(opts = {}) msg = <<-BODY ACK [next_url] SIP/2.0 Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch] From: "#{@from_user}" <sip:#{@from_user}@#{@adv_ip}:[local_port]>;tag=[call_number] To: <sip:#{to_addr}>[peer_tag_param] Call-ID: [call_id] CSeq: [cseq] ACK Contact: <sip:[$local_addr];transport=[transport]> Max-Forwards: 100 User-Agent: #{USER_AGENT} Content-Length: 0 [routes] BODY send msg, opts start_media end |
#answer(opts = {}) ⇒ Object
Helper method to answer an INVITE and expect the ACK
344 345 346 347 |
# File 'lib/sippy_cup/scenario.rb', line 344 def answer(opts = {}) send_answer opts receive_ack opts end |
#build(steps) ⇒ Object
Build the scenario steps provided
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/sippy_cup/scenario.rb', line 130 def build(steps) raise ArgumentError, "Must provide scenario steps" unless steps steps.each_with_index do |step, index| begin instruction, args = step.split ' ', 2 args = split_quoted_string args if args && !args.empty? self.__send__ instruction, *args else self.__send__ instruction end rescue => e @errors << {step: index + 1, message: "#{step}: #{e.}"} end end end |
#call_length_repartition(min, max, interval) ⇒ Object
Create partition table for Call Length
636 637 638 |
# File 'lib/sippy_cup/scenario.rb', line 636 def call_length_repartition(min, max, interval) partition_table 'CallLengthRepartition', min.to_i, max.to_i, interval.to_i end |
#compile! ⇒ String
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 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.
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 |
# File 'lib/sippy_cup/scenario.rb', line 696 def compile! unless @media.nil? 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 to_xml(:pcap_path => "#{@filename}.pcap") end puts "done." scenario_filename end |
#hangup(opts = {}) ⇒ Object
Shortcut to send a BYE and wait for the acknowledgement
626 627 628 629 |
# File 'lib/sippy_cup/scenario.rb', line 626 def hangup(opts = {}) send_bye opts receive_ok opts end |
#invite(opts = {}) ⇒ Object
Send an invite message
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/sippy_cup/scenario.rb', line 154 def invite(opts = {}) opts[:retrans] ||= 500 # FIXME: The DTMF mapping (101) is hard-coded. It would be better if we could # get this from the DTMF payload generator from_addr = "#{@from_user}@#{@adv_ip}:[local_port]" msg = <<-MSG INVITE sip:#{to_addr} SIP/2.0 Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch] From: "#{@from_user}" <sip:#{from_addr}>;tag=[call_number] To: <sip:#{to_addr}> Call-ID: [call_id] CSeq: [cseq] INVITE Contact: <sip:#{from_addr};transport=[transport]> Max-Forwards: 100 User-Agent: #{USER_AGENT} Content-Type: application/sdp Content-Length: [len] #{opts.has_key?(:headers) ? opts.delete(:headers).sub(/\n*\Z/, "\n") : ''} v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] #{@adv_ip} s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 MSG send msg, opts do |send| send << doc.create_element('action') do |action| action << doc.create_element('assignstr') do |assignstr| assignstr['assign_to'] = "remote_addr" assignstr['value'] = to_addr end action << doc.create_element('assignstr') do |assignstr| assignstr['assign_to'] = "local_addr" assignstr['value'] = from_addr end action << doc.create_element('assignstr') do |assignstr| assignstr['assign_to'] = "call_addr" assignstr['value'] = to_addr end end end # These variables will only be used if we initiate a hangup @reference_variables += %w(remote_addr local_addr call_addr) end |
#okay(opts = {}) ⇒ Object Also known as: ack_bye
Acknowledge the last request
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'lib/sippy_cup/scenario.rb', line 592 def okay(opts = {}) msg = <<-ACK SIP/2.0 200 OK [last_Via:] [last_From:] [last_To:] [last_Call-ID:] [last_CSeq:] Contact: <sip:[$local_addr];transport=[transport]> Max-Forwards: 100 User-Agent: #{USER_AGENT} Content-Length: 0 [routes] ACK send msg, opts end |
#receive_ack(opts = {}) ⇒ Object
349 350 351 |
# File 'lib/sippy_cup/scenario.rb', line 349 def receive_ack(opts = {}) recv opts.merge request: 'ACK' end |
#receive_answer(opts = {}) ⇒ Object
Sets an expectation for a SIP 200 message from the remote party as well as storing the record set and the response time duration
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/sippy_cup/scenario.rb', line 393 def receive_answer(opts = {}) = { rrs: true, # Record Record Set: Make the Route headers available via [routes] later rtd: true # Response Time Duration: Record the response time } receive_200(.merge(opts)) do |recv| recv << doc.create_element('action') do |action| action << doc.create_element('ereg') do |ereg| ereg['regexp'] = '<sip:(.*)>.*;tag=([^;]*)' ereg['search_in'] = 'hdr' ereg['header'] = 'To:' ereg['assign_to'] = 'dummy,remote_addr,remote_tag' end end end # These variables will only be used if we initiate a hangup @reference_variables += %w(dummy remote_addr remote_tag) end |
#receive_bye(opts = {}) ⇒ Object
Expect to receive a BYE message
583 584 585 |
# File 'lib/sippy_cup/scenario.rb', line 583 def receive_bye(opts = {}) recv opts.merge request: 'BYE' end |
#receive_invite(opts = {}) ⇒ Object Also known as: wait_for_call
Expect to receive a SIP INVITE
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/sippy_cup/scenario.rb', line 235 def receive_invite(opts = {}) recv(opts.merge(request: 'INVITE', rrs: true)) do |recv| action = doc.create_element('action') do |action| action << doc.create_element('ereg') do |ereg| ereg['regexp'] = '<sip:(.*)>.*;tag=([^;]*)' ereg['search_in'] = 'hdr' ereg['header'] = 'From:' ereg['assign_to'] = 'dummy,remote_addr,remote_tag' end action << doc.create_element('ereg') do |ereg| ereg['regexp'] = '<sip:(.*)>' ereg['search_in'] = 'hdr' ereg['header'] = 'To:' ereg['assign_to'] = 'dummy,local_addr' end action << doc.create_element('assignstr') do |assignstr| assignstr['assign_to'] = "call_addr" assignstr['value'] = "[$local_addr]" end end recv << action end # These variables (except dummy) will only be used if we initiate a hangup @reference_variables += %w(dummy remote_addr remote_tag local_addr call_addr) end |
#receive_message(regexp = nil) ⇒ Object
Expect to receive a MESSAGE message
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 |
# File 'lib/sippy_cup/scenario.rb', line 531 def (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 ereg['regexp'] = regexp ereg['search_in'] = 'body' ereg['check_it'] = true var = "message_#{@message_variables += 1}" ereg['assign_to'] = var @reference_variables << var action << ereg recv << action end okay end |
#receive_ok(opts = {}, &block) ⇒ Object Also known as: receive_200
Sets an expectation for a SIP 200 message from the remote party
419 420 421 |
# File 'lib/sippy_cup/scenario.rb', line 419 def receive_ok(opts = {}, &block) recv({ response: 200 }.merge(opts), &block) end |
#receive_progress(opts = {}) ⇒ Object Also known as: receive_183
Sets an expectation for a SIP 183 message from the remote party
381 382 383 |
# File 'lib/sippy_cup/scenario.rb', line 381 def receive_progress(opts = {}) handle_response 183, opts end |
#receive_ringing(opts = {}) ⇒ Object Also known as: receive_180
Sets an expectation for a SIP 180 message from the remote party
370 371 372 |
# File 'lib/sippy_cup/scenario.rb', line 370 def receive_ringing(opts = {}) handle_response 180, opts end |
#receive_trying(opts = {}) ⇒ Object Also known as: receive_100
Sets an expectation for a SIP 100 message from the remote party
359 360 361 |
# File 'lib/sippy_cup/scenario.rb', line 359 def (opts = {}) handle_response 100, opts end |
#register(user, password = nil, opts = {}) ⇒ Object
Send a REGISTER message with the specified credentials
216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/sippy_cup/scenario.rb', line 216 def register(user, password = nil, opts = {}) send_opts = opts.dup send_opts[:retrans] ||= DEFAULT_RETRANS user, domain = parse_user user if password send (domain, user), send_opts recv opts.merge(response: 401, auth: true, optional: false) send register_auth(domain, user, password), send_opts receive_ok opts.merge(optional: false) else send (domain, user), send_opts end end |
#response_time_repartition(min, max, interval) ⇒ Object
Create partition table for Response Time
645 646 647 |
# File 'lib/sippy_cup/scenario.rb', line 645 def response_time_repartition(min, max, interval) partition_table 'ResponseTimeRepartition', min.to_i, max.to_i, interval.to_i end |
#send_answer(opts = {}) ⇒ Object
Answer an incoming call
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/sippy_cup/scenario.rb', line 311 def send_answer(opts = {}) opts[:retrans] ||= DEFAULT_RETRANS msg = <<-MSG SIP/2.0 200 Ok [last_Via:] From: <sip:[$remote_addr]>;tag=[$remote_tag] To: <sip:[$local_addr]>;tag=[call_number] [last_Call-ID:] [last_CSeq:] Server: #{USER_AGENT} Contact: <sip:[$local_addr];transport=[transport]> Content-Type: application/sdp [routes] Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] #{@adv_ip} s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 MSG start_media send msg, opts end |
#send_bye(opts = {}) ⇒ Object
Send a BYE message
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 |
# File 'lib/sippy_cup/scenario.rb', line 560 def send_bye(opts = {}) msg = <<-MSG BYE sip:[$call_addr] SIP/2.0 Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch] From: <sip:[$local_addr]>;tag=[call_number] To: <sip:[$remote_addr]>;tag=[$remote_tag] Contact: <sip:[$local_addr];transport=[transport]> Call-ID: [call_id] CSeq: [cseq] BYE Max-Forwards: 100 User-Agent: #{USER_AGENT} Content-Length: 0 [routes] MSG send msg, opts end |
#send_digits(digits) ⇒ Object
Send DTMF digits
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 |
# File 'lib/sippy_cup/scenario.rb', line 486 def send_digits(digits) raise "Media not started" unless @media 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 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] #{@adv_ip}:[local_port];branch=[branch] From: "#{@from_user}" <sip:#{@from_user}@#{@adv_ip}:[local_port]>;tag=[call_number] To: <sip:#{to_addr}>[peer_tag_param] Call-ID: [call_id] CSeq: [cseq] INFO Contact: <sip:[$local_addr];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 if @dtmf_mode == :rfc2833 pause delay * 2 * digits.size end end |
#send_ringing(opts = {}) ⇒ Object Also known as: send_180
Send a “180 Ringing” response
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/sippy_cup/scenario.rb', line 289 def send_ringing(opts = {}) msg = <<-MSG SIP/2.0 180 Ringing [last_Via:] From: <sip:[$remote_addr]>;tag=[$remote_tag] To: <sip:[$local_addr]>;tag=[call_number] [last_Call-ID:] [last_CSeq:] Server: #{USER_AGENT} Contact: <sip:[$local_addr];transport=[transport]> Content-Length: 0 MSG send msg, opts end |
#send_trying(opts = {}) ⇒ Object Also known as: send_100
Send a “100 Trying” response
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/sippy_cup/scenario.rb', line 267 def (opts = {}) msg = <<-MSG SIP/2.0 100 Trying [last_Via:] From: <sip:[$remote_addr]>;tag=[$remote_tag] To: <sip:[$local_addr]>;tag=[call_number] [last_Call-ID:] [last_CSeq:] Server: #{USER_AGENT} Contact: <sip:[$local_addr];transport=[transport]> Content-Length: 0 MSG send msg, opts end |
#sleep(seconds) ⇒ Object
Insert a pause into the scenario and its media of the specified duration
469 470 471 472 473 |
# File 'lib/sippy_cup/scenario.rb', line 469 def sleep(seconds) milliseconds = (seconds.to_f * MSEC).to_i pause milliseconds @media << "silence:#{milliseconds}" if @media end |
#to_tmpfiles ⇒ Hash<Symbol => Tempfile>
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.
722 723 724 725 726 727 728 729 730 731 732 733 734 735 |
# File 'lib/sippy_cup/scenario.rb', line 722 def to_tmpfiles unless @media.nil? || @media.empty? media_file = Tempfile.new 'media' media_file.binmode media_file.write compile_media.to_s media_file.rewind end scenario_file = Tempfile.new 'scenario' scenario_file.write to_xml(:pcap_path => media_file.try(:path)) scenario_file.rewind {scenario: scenario_file, media: media_file} end |
#to_xml(options = {}) ⇒ String
Dump the scenario to a SIPp XML string
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 |
# File 'lib/sippy_cup/scenario.rb', line 653 def to_xml( = {}) pcap_path = [: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 unless @reference_variables.empty? scenario_node = docdup.xpath('scenario').first scenario_node << docdup.create_element('Reference') do |ref| ref[:variables] = @reference_variables.to_a.join ',' end end docdup.to_xml end |
#valid? ⇒ true, false
Returns the validity of the scenario. Will be false if errors were encountered while building the scenario from a manifest.
121 122 123 |
# File 'lib/sippy_cup/scenario.rb', line 121 def valid? @errors.size.zero? end |
#wait_for_answer(opts = {}) ⇒ Object
Convenience method to wait for an answer from the called party
This sets expectations for optional SIP 100, 180 and 183, followed by a required 200 and sending the acknowledgement.
432 433 434 435 436 437 438 |
# File 'lib/sippy_cup/scenario.rb', line 432 def wait_for_answer(opts = {}) opts receive_ringing opts receive_progress opts receive_answer opts ack_answer opts end |
#wait_for_hangup(opts = {}) ⇒ Object
Shortcut to set an expectation for a BYE and acknowledge it when received
616 617 618 619 |
# File 'lib/sippy_cup/scenario.rb', line 616 def wait_for_hangup(opts = {}) receive_bye(opts) ack_bye(opts) end |