diff --git a/CHANGELOG.md b/CHANGELOG.md index 111a1f9..696b1c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ * Feature: Permit `To` domain to be different from the destination. This permits testing multi-tenant systems more easily. # [0.6.0](https://github.com/mojolingo/sippy_cup/compare/v0.5.0...v0.6.0) - * Change: Call limits (`number_of_calls`, `concurrent_max` and `calls_per_second`) no longer have default values for simplicity of UAS scenarios. The value of `to_user` now defaults to the SIPp default of `s`. - * Feature: Support for setting rate scaling independently of reporting frequency via the new `calls_per_second_interval` option. See also https://github.com/SIPp/sipp/pull/107 and https://github.com/SIPp/sipp/pull/126. + * Change: Call limits (`number_of_calls`, `concurrent_max` and `call_rate`) no longer have default values for simplicity of UAS scenarios. The value of `to_user` now defaults to the SIPp default of `s`. + * Feature: Support for setting rate scaling independently of reporting frequency via the new `call_rate_interval` option. See also https://github.com/SIPp/sipp/pull/107 and https://github.com/SIPp/sipp/pull/126. # [0.5.0](https://github.com/mojolingo/sippy_cup/compare/v0.4.1...v0.5.0) SYNTAX CHANGES! diff --git a/README.markdown b/README.markdown index 2f8f412..069200e 100644 --- a/README.markdown +++ b/README.markdown @@ -6,6 +6,15 @@ # Sippy Cup +## NOTES for this fork + +* New instruction is added to insert wav file into pcap + - `- play_audio ""` + - need spandsp library for encoidng a-law and u-law +* Debug the DTMF packet generation (end of event) + - reduce the duration to 200 milliseconds + - change obsolete rfc2833 to rfc4733 + ## Overview ### The Problem @@ -78,7 +87,7 @@ Using `bundle` will then install the gem dependencies and allow you to run `sipp source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 steps: - invite @@ -223,7 +232,7 @@ Each parameter has an impact on the test, and may either be changed once the XML
By default, SIPp assigns RTP ports dynamically. However, if there is a need for a static RTP port (say, for data collection purposes), it can be done by supplying a port number here. Default: SIPp's default of 6000
dtmf_mode
-
Specify the mechanism by which DTMF is signaled. Valid options are `rfc2833` for within the RTP media, or `info` for SIP INFO. Default: rfc2833
+
Specify the mechanism by which DTMF is signaled. Valid options are `rfc4733` for within the RTP media, or `info` for SIP INFO. Default: rfc4733
scenario_variables
If you're using sippy_cup to run a SIPp XML file, there may be CSV fields in the scenario ([field0], [field1], etc.). Specify a path to a CSV file containing the required information using this option. (File is semicolon delimeted, information can be found [here](http://sipp.sourceforge.net/doc/reference.html#inffile).) Default: unused
@@ -235,19 +244,19 @@ Each parameter has an impact on the test, and may either be changed once the XML
The total number of calls permitted for the entire test. When this limit is reached, the test is over. Defaults to nil.
concurrent_max
-
The maximum number of calls permitted to be active at any given time. When this limit is reached, SIPp will slow down or stop sending new calls until there it falls below the limit. Defaults to SIPp's default: (3 * call_duration (seconds) * calls_per_second)
+
The maximum number of calls permitted to be active at any given time. When this limit is reached, SIPp will slow down or stop sending new calls until there it falls below the limit. Defaults to SIPp's default: (3 * call_duration (seconds) * call_rate)
-
calls_per_second
+
call_rate
The rate at which new calls should be created. Note that SIPp will automatically adjust this downward to stay at or beneath the maximum number of concurrent calls (`concurrent_max`). Defaults to SIP's default of 10 -
calls_per_second_incr
-
When used with `calls_per_second_max`, tells SIPp the amount by which `calls_per_second` should be incremented. CPS rate is adjusted each `calls_per_second_interval`. Default: 1.
+
call_rate_incr
+
When used with `call_rate_max`, tells SIPp the amount by which `call_rate` should be incremented. CPS rate is adjusted each `call_rate_interval`. Default: 1.
-
calls_per_second_interval
-
When used with `calls_per_second_max`, tells SIPp the time interval (in seconds) by which calls-per-second should be incremented. Default: Unset; SIPp's default (60s). NOTE: Requires a development build of SIPp; see https://github.com/SIPp/sipp/pull/107
+
call_rate_interval
+
When used with `call_rate_max`, tells SIPp the time interval (in seconds) by which calls-per-second should be incremented. Default: Unset; SIPp's default (60s). NOTE: Requires a development build of SIPp; see https://github.com/SIPp/sipp/pull/107
-
calls_per_second_max
-
The maximum rate of calls-per-second. Default: unused (`calls_per_second` will not change)
+
call_rate_max
+
The maximum rate of calls-per-second. Default: unused (`call_rate` will not change)
advertise_address
The IP address to advertise in SIP and SDP if different from the bind IP. Default: `source` IP address
diff --git a/examples/navigate_ivr.yml b/examples/navigate_ivr.yml index a58241d..4b8f94d 100644 --- a/examples/navigate_ivr.yml +++ b/examples/navigate_ivr.yml @@ -1,7 +1,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 steps: - invite diff --git a/examples/simple_call.yml b/examples/simple_call.yml index b897a9e..b436d85 100644 --- a/examples/simple_call.yml +++ b/examples/simple_call.yml @@ -1,7 +1,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 steps: - invite diff --git a/examples/wait_for_call.yml b/examples/wait_for_call.yml index 7c4efc7..f78554d 100644 --- a/examples/wait_for_call.yml +++ b/examples/wait_for_call.yml @@ -2,7 +2,7 @@ source: 10.3.18.108 destination: 10.3.18.134 max_concurrent: 1 -calls_per_second: 1 +call_rate: 1 number_of_calls: 1 steps: - wait_for_call diff --git a/lib/sippy_cup/g711.rb b/lib/sippy_cup/g711.rb new file mode 100644 index 0000000..7d88d3b --- /dev/null +++ b/lib/sippy_cup/g711.rb @@ -0,0 +1,36 @@ +# ffi interface to freeswitch's g711 +require 'ffi' + +module SippyCup + module G711 + extend FFI::Library + ffi_lib 'spandsp' + + enum :EncodeState, [ + :G711_ALAW, 0, + :G711_ULAW, 1, + ] + + class G711State < FFI::Struct + layout :mode, :EncodeState + end + + attach_function :g711_encode, [ G711State, :pointer, :pointer, :int ], :int + + def encode(samples) + state = G711State.new + state[:mode] = :G711_ULAW # u-law only + + iptr = FFI::MemoryPointer.new(:int16, samples.size) + optr = FFI::MemoryPointer.new(:uint8, samples.size) + + #puts samples.join(' ') + iptr.write_array_of_type(:int16, :write_int16, samples) + g711_encode(state, optr, iptr, samples.size) + output = optr.read_array_of_type(:uint8, :read_uint8, samples.size) + #puts output.join(' ') + output + end + module_function :encode + end +end diff --git a/lib/sippy_cup/media.rb b/lib/sippy_cup/media.rb index acae59e..54fb67d 100644 --- a/lib/sippy_cup/media.rb +++ b/lib/sippy_cup/media.rb @@ -1,11 +1,14 @@ # encoding: utf-8 require 'ipaddr' +require 'wavefile' +require 'ffi' require 'sippy_cup/media/pcmu_payload' require 'sippy_cup/media/dtmf_payload' +require 'sippy_cup/g711' module SippyCup class Media - VALID_STEPS = %w{silence dtmf}.freeze + VALID_STEPS = %w{silence dtmf play}.freeze USEC = 1_000_000 MSEC = 1_000 attr_accessor :sequence @@ -48,13 +51,11 @@ def compile! (value.to_i / @generator::PTIME).times do packet = new_packet rtp_frame = @generator.new - # The first RTP audio packet should have the marker bit set if first_audio rtp_frame.rtp_marker = 1 first_audio = false end - rtp_frame.rtp_timestamp = timestamp += rtp_frame.timestamp_interval elapsed += rtp_frame.ptime rtp_frame.rtp_sequence_num = sequence_number += 1 @@ -65,24 +66,52 @@ def compile! end when 'dtmf' # value is the DTMF digit to send - # append that RFC2833 digit - # Assume 0.25 second duration for now - count = 250 / DTMFPayload::PTIME + # append that RFC4733 digit + # Assume 0.2 second duration for now + count = 200 / DTMFPayload::PTIME count.times do |i| packet = new_packet dtmf_frame = DTMFPayload.new value - dtmf_frame.rtp_marker = 1 if i == 0 - dtmf_frame.rtp_timestamp = timestamp # Is this correct? This is what Blink does... - #dtmf_frame.rtp_timestamp = timestamp += dtmf_frame.timestamp_interval + # The first RTP audio packet should have the marker bit set + if first_audio + rtp_frame.rtp_marker = 1 + first_audio = false + end + dtmf_frame.rtp_timestamp = timestamp += dtmf_frame.timestamp_interval + elapsed += dtmf_frame.ptime dtmf_frame.rtp_sequence_num = sequence_number += 1 dtmf_frame.rtp_ssrc_id = ssrc_id - dtmf_frame.end_of_event = (count == i) # Last packet? + dtmf_frame.end_of_event = (i == count-1) # Last packet packet.headers.last.body = dtmf_frame.to_bytes packet.recalc @pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed)) end # Now bump up the timestamp to cover the gap timestamp += count * DTMFPayload::TIMESTAMP_INTERVAL + when 'play' + # value is wav file path + wav = WaveFile::Reader.new(value, WaveFile::Format.new(:mono, :pcm_16, 8000)) + duration = wav.total_sample_frames * 1000 / wav.native_format.sample_rate # in milliseconds + (duration / @generator::PTIME).times do |i| + packet = new_packet + rtp_frame = @generator.new + # The first RTP audio packet should have the marker bit set + if first_audio + rtp_frame.rtp_marker = 1 + first_audio = false + end + rtp_frame.rtp_timestamp = timestamp += rtp_frame.timestamp_interval + elapsed += rtp_frame.ptime + rtp_frame.rtp_sequence_num = sequence_number += 1 + rtp_frame.rtp_ssrc_id = ssrc_id + len = wav.native_format.sample_rate * rtp_frame.ptime / 1000 + lin_data = wav.read(len).samples + enc_data = G711::encode(lin_data) + packet.headers.last.body = rtp_frame.header.to_s << enc_data.flatten.pack('c*') + packet.recalc + @pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed)) + end + wav.close else end end diff --git a/lib/sippy_cup/runner.rb b/lib/sippy_cup/runner.rb index 7ffeaa4..7f26ca1 100644 --- a/lib/sippy_cup/runner.rb +++ b/lib/sippy_cup/runner.rb @@ -105,17 +105,19 @@ def command_options max_concurrent = @scenario_options[:concurrent_max] || @scenario_options[:max_concurrent] options[:l] = max_concurrent if max_concurrent options[:m] = @scenario_options[:number_of_calls] if @scenario_options[:number_of_calls] - options[:r] = @scenario_options[:calls_per_second] if @scenario_options[:calls_per_second] + options[:r] = @scenario_options[:call_rate] if @scenario_options[:call_rate] + options[:rp] = @scenario_options[:call_period] if @scenario_options[:call_period] options[:s] = @scenario_options[:to].to_s.split('@').first if @scenario_options[:to] options[:i] = @scenario_options[:source] if @scenario_options[:source] options[:mp] = @scenario_options[:media_port] if @scenario_options[:media_port] + options[:trace_logs] = nil - if @scenario_options[:calls_per_second_max] + if @scenario_options[:call_rate_max] options[:no_rate_quit] = nil - options[:rate_max] = @scenario_options[:calls_per_second_max] - options[:rate_increase] = @scenario_options[:calls_per_second_incr] || 1 - options[:rate_interval] = @scenario_options[:calls_per_second_interval] if @scenario_options[:calls_per_second_interval] + options[:rate_max] = @scenario_options[:call_rate_max] + options[:rate_increase] = @scenario_options[:call_rate_incr] || 1 + options[:rate_interval] = @scenario_options[:call_rate_interval] if @scenario_options[:call_rate_interval] end if @scenario_options[:stats_file] diff --git a/lib/sippy_cup/scenario.rb b/lib/sippy_cup/scenario.rb index cf31b25..6b50d02 100644 --- a/lib/sippy_cup/scenario.rb +++ b/lib/sippy_cup/scenario.rb @@ -88,11 +88,11 @@ def self.from_manifest(manifest, options = {}) # @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, Numeric] :call_rate 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] :dtmf_mode The output DTMF mode, either rfc4733 (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] :steps A collection of steps @@ -175,7 +175,7 @@ def invite(opts = {}) s=- c=IN IP[media_ip_type] [media_ip] t=0 0 -m=audio [media_port] RTP/AVP 0 101 +m=audio [auto_media_port] RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 @@ -472,6 +472,16 @@ def sleep(seconds) @media << "silence:#{milliseconds}" if @media end + # + # add audio to pcap from wav file + # + # @param [String] path of wav file + # + def play_audio(wav_file) + raise "Media not started" unless @media + @media << "play:#{wav_file}" + end + # # Send DTMF digits # @@ -485,12 +495,12 @@ def sleep(seconds) # 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 + delay = (0.2 * 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 + when :rfc4733 @media << "dtmf:#{digit}" @media << "silence:#{delay}" when :info @@ -518,7 +528,7 @@ def send_digits(digits) end end - if @dtmf_mode == :rfc2833 + if @dtmf_mode == :rfc4733 pause delay * 2 * digits.size end end @@ -773,9 +783,9 @@ def scenario_node def parse_args(args) 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) + raise ArgumentError, "dtmf_mode must be rfc4733 or info" unless [:rfc4733, :info].include?(@dtmf_mode) else - @dtmf_mode = :rfc2833 + @dtmf_mode = :rfc4733 end @from_user = args[:from_user] || "sipp" diff --git a/sippy_cup.gemspec b/sippy_cup.gemspec index 1f88f78..cec31cf 100644 --- a/sippy_cup.gemspec +++ b/sippy_cup.gemspec @@ -19,9 +19,11 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] s.add_runtime_dependency 'packetfu', ["= 1.1.11"] # 1.1.12 introduces a breaking change, removing PacketFu::UDPPacket - s.add_runtime_dependency 'nokogiri', ["~> 1.6.0"] + s.add_runtime_dependency 'nokogiri', ["~> 1.7.0"] s.add_runtime_dependency 'activesupport', [">= 3.0"] s.add_runtime_dependency 'psych', ["~> 2.0.1"] unless RUBY_PLATFORM == 'java' + s.add_runtime_dependency 'wavefile', [">= 0.8.0"] + s.add_runtime_dependency 'ffi' s.add_development_dependency 'guard-rspec' s.add_development_dependency 'rspec', ["~> 3.4"] diff --git a/spec/sippy_cup/fixtures/test.yml b/spec/sippy_cup/fixtures/test.yml index df1acc8..0ce6b3d 100644 --- a/spec/sippy_cup/fixtures/test.yml +++ b/spec/sippy_cup/fixtures/test.yml @@ -3,7 +3,7 @@ name: My test scenario source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 steps: - invite diff --git a/spec/sippy_cup/runner_spec.rb b/spec/sippy_cup/runner_spec.rb index c1194c6..6818b32 100644 --- a/spec/sippy_cup/runner_spec.rb +++ b/spec/sippy_cup/runner_spec.rb @@ -97,7 +97,7 @@ def expect_command_execution(command = anything) destination: 'bar.com' to: 1 concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 steps: - invite @@ -124,7 +124,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 options: trace_err: ~ @@ -154,7 +154,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 source_port: 1234 steps: @@ -182,7 +182,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 from_user: pat to: frank@there.com @@ -211,7 +211,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 media_port: 6000 steps: @@ -239,7 +239,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 stats_file: stats.csv steps: @@ -266,7 +266,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 stats_file: stats.csv stats_interval: 3 @@ -310,7 +310,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 summary_report_file: report.txt steps: @@ -338,7 +338,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 errors_report_file: errors.txt steps: @@ -366,10 +366,10 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 -calls_per_second_max: 5 -calls_per_second_incr: 2 -calls_per_second_interval: 20 +call_rate: 2 +call_rate_max: 5 +call_rate_incr: 2 +call_rate_interval: 20 number_of_calls: 10 errors_report_file: errors.txt steps: @@ -397,7 +397,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 scenario_variables: /path/to/vars.csv steps: @@ -427,7 +427,7 @@ def expect_command_execution(command = anything) source: 'dah.com' destination: 'bar.com' concurrent_max: 5 -calls_per_second: 2 +call_rate: 2 number_of_calls: 10 transport_mode: t1 steps: diff --git a/spec/sippy_cup/scenario_spec.rb b/spec/sippy_cup/scenario_spec.rb index 1f7a2ea..f767378 100644 --- a/spec/sippy_cup/scenario_spec.rb +++ b/spec/sippy_cup/scenario_spec.rb @@ -713,7 +713,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 from_user: #{specs_from} steps: @@ -822,7 +822,7 @@ 'source' => '192.0.2.15', 'destination' => '192.0.2.200', 'max_concurrent' => 10, - 'calls_per_second' => 5, + 'call_rate' => 5, 'number_of_calls' => 20, 'from_user' => "#{specs_from}" }) @@ -835,7 +835,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 from_user: #{specs_from} scenario: #{scenario_path} @@ -857,7 +857,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 from_user: #{specs_from} scenario: #{scenario_path} @@ -881,7 +881,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 from_user: #{specs_from} steps: @@ -914,7 +914,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 from_user: #{specs_from} steps: @@ -950,7 +950,7 @@ 'source' => '192.0.2.15', 'destination' => '192.0.2.200', 'max_concurrent' => 10, - 'calls_per_second' => 5, + 'call_rate' => 5, 'number_of_calls' => override_options[:number_of_calls], 'from_user' => "#{specs_from}" }) @@ -963,7 +963,7 @@ source: 192.0.2.15 destination: 192.0.2.200 max_concurrent: 10 -calls_per_second: 5 +call_rate: 5 number_of_calls: 20 from_user: #{specs_from} steps: